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 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 2016心愿单 | 贾攀的流水账 + + + + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

贾攀的流水账

+ +
+

Panmax's Blog

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+ + +
+
+
+ + +
+ + + + + + + + + + + + +
+ +
+ + + +
+ + + + + + + + +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git "a/2015/Datetime\347\233\270\345\207\217\350\256\241\347\256\227\346\200\273\347\247\222\346\225\260\351\201\207\345\210\260\347\232\204\345\235\221/index.html" "b/2015/Datetime\347\233\270\345\207\217\350\256\241\347\256\227\346\200\273\347\247\222\346\225\260\351\201\207\345\210\260\347\232\204\345\235\221/index.html" new file mode 100644 index 0000000000..903e5b39bd --- /dev/null +++ "b/2015/Datetime\347\233\270\345\207\217\350\256\241\347\256\227\346\200\273\347\247\222\346\225\260\351\201\207\345\210\260\347\232\204\345\235\221/index.html" @@ -0,0 +1,513 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Datetime相减计算总秒数遇到的坑 | 贾攀的流水账 + + + + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

贾攀的流水账

+ +
+

Panmax's Blog

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+ + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ Datetime相减计算总秒数遇到的坑 +

+ + +
+ + + + +
+ + +

一直以为Python里两个datetime类型相减然后获取seconds拿到的是两个时间相差的总秒数,其实并不是。。。

+

正确的获取方法应该是用total_seconds()

+

示例:

1
2
3
4
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 相关的文档中这样写到:

1
2
3
4
5
6
7
8
9
10
11
12
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()

+
+

Return 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.

+
+
+

Note that for very large time intervals (greater than 270 years on most platforms) this method will lose microsecond accuracy.

+
+

timedelta官方示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
>>> 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

+ +
+ + + + + + + +
+ + + + + + +
+ +
+ + + +
+ + + + + + + + +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git "a/2015/flask-bootstrap\351\273\230\350\256\244\344\275\277\347\224\250\345\233\275\345\244\226cdn\350\247\243\345\206\263\346\226\271\346\241\210/index.html" "b/2015/flask-bootstrap\351\273\230\350\256\244\344\275\277\347\224\250\345\233\275\345\244\226cdn\350\247\243\345\206\263\346\226\271\346\241\210/index.html" new file mode 100644 index 0000000000..73e6030d40 --- /dev/null +++ "b/2015/flask-bootstrap\351\273\230\350\256\244\344\275\277\347\224\250\345\233\275\345\244\226cdn\350\247\243\345\206\263\346\226\271\346\241\210/index.html" @@ -0,0 +1,514 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + flask-bootstrap默认使用国外cdn解决方案 | 贾攀的流水账 + + + + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

贾攀的流水账

+ +
+

Panmax's Blog

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+ + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ flask-bootstrap默认使用国外cdn解决方案 +

+ + +
+ + + + +
+ + +

flask-bootstrap 默认走的是国外CDN,所以在天朝使用起来速度奇慢。

+

解决办法:

+
    +
  • 找到在包管理目录中找到flask-bootstrap__init__.py文件,将里边控制CDN部分的代码修改为:
  • +
+
1
2
3
4
5
6
7
8
9
bootstrap = lwrap(
WebCDN('//cdn.bootcss.com/bootstrap/%s/'
% BOOTSTRAP_VERSION),
local)

jquery = lwrap(
WebCDN('//cdn.bootcss.com/jquery/%s/'
% JQUERY_VERSION),
local)
+

只修改这两段即可。

+
    +
  • 直接用我fork的分支进行安装flask-bootstrap,源码我已修改为使用国内CDN:
  • +
+
1
pip install git+https://github.com/Panmax/flask-bootstrap.git
+

+
1
git+https://github.com/Panmax/flask-bootstrap.git
+

将此写在requirements.txt文件中

+ +
+ + + + + + + +
+ + + + + + +
+ +
+ + + +
+ + + + + + + + +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git "a/2015/\344\270\272\344\273\200\344\271\210\350\246\201\345\206\231Blog\357\274\237/index.html" "b/2015/\344\270\272\344\273\200\344\271\210\350\246\201\345\206\231Blog\357\274\237/index.html" new file mode 100644 index 0000000000..e6a81d775c --- /dev/null +++ "b/2015/\344\270\272\344\273\200\344\271\210\350\246\201\345\206\231Blog\357\274\237/index.html" @@ -0,0 +1,519 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 为什么要写Blog? | 贾攀的流水账 + + + + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

贾攀的流水账

+ +
+

Panmax's Blog

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+ + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ 为什么要写Blog? +

+ + +
+ + + + +
+ + +

原文地址:http://www.ruanyifeng.com/blog/2006/12/why_i_keep_blogging.html

+

到今年12月为止,我写Blog已经满3年了,一共写了接近600篇,平均每2天写一篇。今后应该还会继续写下去。

+

3年前,我开始写的时候,并没有想过自己会坚持这么久。3年中,也遇见过几次有人问我”为什么要写Blog?”

+

是啊,为什么要写Blog?毕竟这里没有人支付稿酬,也看不出有任何明显的物质性收益。

+ +

Darren Rowse在他的Blog上,讲到了7个理由,我觉得说得很好。

+

1. 学会写作Blog的技巧(teach you the skills of blogging)

+

没有人天生会写Blog,我刚开始的时候也不知道该怎么写。但是,经过不断的尝试,现在我知道怎么可以写出受欢迎的文章。

+

2. 熟悉Blog工具(familiarize you with the tools of blogging)

+

写作Blog,可以选择自己搭建平台,也可以选择网上的免费Blog提供商。我曾经试用过不少Blog软件,最后才选择了现在的Moveable Type,这本身也是一个学习过程。

+

3. 便于更好地安排时间(help you work out how much time you have)

+

写作Blog花费的时间,要比大家想象的多,甚至也比我自己想象的多。但是,另一方面,每天我们又有很多时间被无谓地浪费了。坚持写作Blog的过程,也是进行更好的时间安排的过程。

+

4. 便于你了解自己是否可以长期做一件喜欢的事情(help you work out if you can sustain blogging for the long term)

+

很多人都有自己的爱好,但是只有当你享受到这种爱好时,你才会长期坚持下去。写作Blog可以帮助你体验到这种感觉。

+

5. 便于体验Blog文化(give you a taste of blogging ‘culture’)

+

Blog的世界有一种无形的礼仪、风格和用语。熟悉它们,会使你更好地表达自己和理解他人。

+

6. 便于你形成和了解自我(help you define a niche)

+

长期写作Blog最大的好处之一就是,写着写着,你的自我会变得越来越清晰。你最终会明白自己是一个什么样的人,以及自己热爱的又是什么东西。

+

7. 帮助你找到读者(help you find a readership)

+

与他人交流是生命最大的乐趣之一。写作Blog可以帮助我们更好地做到这一点。

+

如果你觉得你想说的东西不适宜让他人知道,你可以在自己的电脑里写,不用放到网上。这样除了上面第7点以外,其他6点的好处也还是适用的。
总之,正是因为以上7个理由,所以我强烈建议,每一个朋友都应该有一个自己的Blog,尝试将自己的生活和想法记录下来,留下一些印记。

+ +
+ + + + + + + +
+ + + + + + +
+ +
+ + + +
+ + + + + + + + +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git "a/2015/\346\212\200\346\234\257\351\253\230\346\211\213\345\246\202\344\275\225\347\202\274\346\210\220/1.jpg" "b/2015/\346\212\200\346\234\257\351\253\230\346\211\213\345\246\202\344\275\225\347\202\274\346\210\220/1.jpg" new file mode 100644 index 0000000000..225707d868 Binary files /dev/null and "b/2015/\346\212\200\346\234\257\351\253\230\346\211\213\345\246\202\344\275\225\347\202\274\346\210\220/1.jpg" differ diff --git "a/2015/\346\212\200\346\234\257\351\253\230\346\211\213\345\246\202\344\275\225\347\202\274\346\210\220/2 (1).png" "b/2015/\346\212\200\346\234\257\351\253\230\346\211\213\345\246\202\344\275\225\347\202\274\346\210\220/2 (1).png" new file mode 100644 index 0000000000..935eed2f33 Binary files /dev/null and "b/2015/\346\212\200\346\234\257\351\253\230\346\211\213\345\246\202\344\275\225\347\202\274\346\210\220/2 (1).png" differ diff --git "a/2015/\346\212\200\346\234\257\351\253\230\346\211\213\345\246\202\344\275\225\347\202\274\346\210\220/2.png" "b/2015/\346\212\200\346\234\257\351\253\230\346\211\213\345\246\202\344\275\225\347\202\274\346\210\220/2.png" new file mode 100644 index 0000000000..935eed2f33 Binary files /dev/null and "b/2015/\346\212\200\346\234\257\351\253\230\346\211\213\345\246\202\344\275\225\347\202\274\346\210\220/2.png" differ diff --git "a/2015/\346\212\200\346\234\257\351\253\230\346\211\213\345\246\202\344\275\225\347\202\274\346\210\220/index.html" "b/2015/\346\212\200\346\234\257\351\253\230\346\211\213\345\246\202\344\275\225\347\202\274\346\210\220/index.html" new file mode 100644 index 0000000000..38604a8c14 --- /dev/null +++ "b/2015/\346\212\200\346\234\257\351\253\230\346\211\213\345\246\202\344\275\225\347\202\274\346\210\220/index.html" @@ -0,0 +1,696 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 技术高手如何炼成 | 贾攀的流水账 + + + + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

贾攀的流水账

+ +
+

Panmax's Blog

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+ + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ 技术高手如何炼成 +

+ + +
+ + + + +
+ + +

原文地址:http://zhuanlan.zhihu.com/zhengyun/20270317

+

+

本文档适用人员:技术人员

+

面试的时候,我会问面试者,你日常如何构建自己的知识体系,如何让自己更高更快更强?多数工程师并没有深入地思考过这个问题,基本上是零敲碎打,随机性大。本着不能让你白来一趟的精神,好为人师的我会娓娓道来:

+ +

第一阶段 认真构建完整的知识体系

十几年前我投身软件行业的时候,光是讲解数据库原理、操作系统、TCP/IP、组网、算法等等基础知识的英文原版书摞起来就等身,认认真真看完,各种上手实践,入行后,读遍 C++ 各种经典著作,读遍各种协议原文,认认真真打基础。

+

很多工程师都说自己平常就是在某些 IT 门户上看看推荐的博文或新闻,我说这属于典型的零敲碎打,不够刺激。

+

聊到这时,我会举一个例子,为什么要阅读长篇小说,因为中短篇小说就像用针扎你,而长篇小说就像把你装进一个沙袋里吊起来,从四面八方用狼牙棒打你,酣畅淋漓。构建可用的知识体系,就得读书,书是有体系结构的,你关心不关心,现阶段你用到用不到,它都讲到了,从头到尾看几遍,针扎得透透的。

+

何谓知识体系?

+

几年前,前支付宝架构师姚建东曾经在我们公司做过技术人员如何规划自己的分享讲座,他是这么论述的:

+

技术与技巧包括:

+
    +
  • 计算机基础理论
      +
    • 计算机模型:内存/IO/时钟/CPU……
    • +
    • 算法
    • +
    • 专项技术领域:
        +
      • 数据挖掘
      • +
      • 数据管理
      • +
      • 智能推荐
      • +
      • 搜索
      • +
      • ……
      • +
      • +
      +
    • +
    +
  • +
  • 语言与工具

    +
      +
    • 语言与相关体系
    • +
    • 开发工具,分析工具,代码管理工具
    • +
    • HTML/CSS/JS/Ajax
    • +
    • 常用框架与第三方类库
    • +
    +
  • +
  • 调试与测试

    +
      +
    • 调试方法和哲学
    • +
    • 定位问题
    • +
    • BUG管理工具
    • +
    • 单元测试
    • +
    • 集成测试
    • +
    • 性能测试
    • +
    • 安全测试
    • +
    • 兼容性测试与方法
    • +
    • JS/Ajax测试与方法
    • +
    • 服务层测试
    • +
    • Web层测试
    • +
    +
  • +
  • 网络与系统

    +
      +
    • TCP/IP协议与模型,HTTP/SMTP等协议
    • +
    • Linux系统,网络分析工具,系统分析工具
    • +
    • 容量,流量与负载均衡
    • +
    • 应用部署、规范、规划
    • +
    • 安全
    • +
    • 监控与故障分析
    • +
    • 磁盘与存储
    • +
    • Shell
    • +
    • DNS与域名
    • +
    • 缓存,反向代理
    • +
    • 图片服务器(海量小文件)
    • +
    +
  • +
  • 需求挖掘与分析

    +
      +
    • 需求文档格式
    • +
    • 需求访谈
    • +
    • 需求分析方法,需求分析工具
    • +
    • 领域知识与经验
    • +
    +
  • +
  • 系统分析与设计
      +
    • UML语言与模型
    • +
    • 分析模式
    • +
    • 设计模式,领域驱动
    • +
    • 系统分析文档格式
    • +
    • 系统设计文档格式
    • +
    • 功能性需求与非功能性需求
    • +
    +
  • +
  • 数据与系统
      +
    • 数据库
    • +
    • 可伸缩策略,扩展策略,备份,容灾,性能,安全,高可用……
    • +
    • 数据设计与范式,SQL/NoSQL,Cache,分布式文件
    • +
    +
  • +
  • 架构设计
      +
    • 架构模式,典型互联网公司架构演进历史
    • +
    • 架构原则,常用策略
    • +
    • 架构设计方法
    • +
    • 非功能性理解
        +
      • 扩展性
      • +
      • 伸缩性
      • +
      • 稳定性
      • +
      • 一致性
      • +
      • 性能
      • +
      • 吞吐量
      • +
      +
    • +
    • 容量预测与规划
    • +
    • 架构体系与相关技术
    • +
    +
  • +
  • 过程与管理
      +
    • 分析过程
    • +
    • 研发过程
    • +
    • 评审过程
    • +
    • 测试过程
    • +
    • 发布过程
    • +
    • 回滚过程
    • +
    • 文档管理
    • +
    • 知识管理
    • +
    • 项目管理
    • +
    +
  • +
+

以上其实就是一份从业基础知识清单,你可以按图索骥,阅读相关书籍。

+

第二阶段 顺着一个Topic钻进去,锻炼自己的预研能力

无论公司业务还是自己喜欢做的事,都可以抽象出通用性课题,然后以做论文的方式杀进去。这个事情得反复操练,有意识操练。

+

做事方式为:

+
    +
  1. 抽象出 Topic——如分布式锁,分布式并行计算引擎,防CSRF的FormToken自动生成框架,定时任务管理与调度平台,分布式跟踪,等等
  2. +
  3. 向功课好的学生学习——有针对性地深入了解业界其他公司是如何分析问题和解决问题的,汇总各种方案,站在巨人的肩膀上
  4. +
  5. 分析特定应用场景,技术选型
  6. +
  7. 兼顾高可用性和可伸缩,做设计评审
  8. +
  9. 做测试自证靠谱,梳理知识点,开技术分享会
  10. +
  11. 上线商用,总结经验教训,开经验分享会
  12. +
+

其中一个重点是汇总和分享。05年时,应电信级统一消息业务需要,我去研究了 SIP 协议,做了各种试验,分析报文,写了一系列的幻灯片,做了公开分享,一时间还颇受欢迎:

+
    +
  1. SIP_to_Freshman_by_zhengyun.ppt
  2. +
  3. SIP之穿越NAT_by_zhengyun.ppt
  4. +
  5. SIP体系架构讲义及消息交互演示_by_zhengyun.ppt
  6. +
  7. SIP多方会话消息之实例讲解_by_zhengyun.ppt
  8. +
  9. SIP安全框架之认证[NTLM和Kerberos]_by_zhengyun.ppt
  10. +
  11. SIP消息之逐项讲解_by_zhengyun.ppt
  12. +
+

为什么要写出来、讲出来呢?
因为有一个学习金字塔理论,如下图所示:

+

+

我们读过的事情能够记住学习内容的10%,

+

我们听过的事情能够记住20%,

+

我们看过的事情能够记住30%,

+

我们听过和看过的事情能够记住50%——如看影像/看展览/看演示/现场观摩,

+

我们说过的事情能够记住70%——如参与讨论/发言,

+

我们说过和做过的事情能够记住90%——如做报告,给别人讲,亲身体验,动手做。

+

这也就是我在《窝窝研发过去几年做对了哪些事》中阐述的管理方法:我们从入职之后就有意识地训练大家,让大家能够公开陈述、清晰表达。所以,试用期内,新人必须做一次技术分享和一次技术评审,面对各方的 challenge;预研的中间和结尾都要有分享会;平时也要定期组织技术讲座。

+

第三阶段 疯狂回答技术问题

知识体系慢慢构建,与业务相关的抽象 Topic 也在探索中。
但这还不够。

+

因为你亲身接触到的世界太小,可能不足以构成挑战,你可能意识不到自己缺多少知识和技能,不利于你分析问题、提出问题和解决问题的能力培养。

+

所以,要主动出击:

+

疯狂回答问题。

+

我曾经在入行的头几年里几乎把我关注的垂直领域(包括语言领域和业务领域)里的所有问题都回答了一遍。我对外宣扬知无不言言无不尽,放出邮件地址和 MSN(那时候 MSN 很高大上),很多网友都会发邮件或者加我好友,问各种开发疑难问题,平均每天都有几个,然后我把解决问题的过程写成微软 KB(KnowledgeBase) 文体发表在我的博客上。

+

你想想看,工作中的问题你平均每隔几天才能遇到一个,而这么做,每天你都会遇到几个乃至于十几个,第一让你脑力激荡,第二接触到更多新知。

+

05年到06年期间,我因工作需要学习了 JavaME(或古老的称呼 J2ME),早年间 Symbian 手机上的客户端开发。那段时间我天天扫中文论坛的帖子,力求回答所有问题,尤其是那些 BUG 或故障。对于那些暂时没有人解决的,如流媒体实时播放,如仿 OperaMini 二级菜单界面,都上下求索,最后放出思路以及源码。

+

同时,我经常整理常见问题,梳理成册并发布。譬如我整理过的 J2ME 疑难问题:

+
    +
  1. [J2ME Q&A]真机报告MontyThread -n的错误之解释
  2. +
  3. [J2MEQ&A]WTK初始化WMAClient报错XXX has no IP address的解释
  4. +
  5. [J2ME Q&A]untrusted domain is not configured问题回应
  6. +
  7. [J2ME]“Cannot open socket for LIME events”错误解决
  8. +
  9. 几个月后,我成为 J2ME 中文论坛超级版主。通过这个历程,我想告诉大家,回答网友问题,技巧得当的话,比如别老是重复回答新手问题,试着攻克那些疑难问题,或者离奇故障,绝对不会浪费你的时间。
  10. +
+

为什么?

+

因为你要信奉:

+
+

你学过的每一样东西,你遭受的每一次苦难,都会在你一生中的某个时候派上用场。
——佩内洛普·菲兹杰拉德 《离岸》

+
+
+

Everything that you’ve learnt and all the hardships you’ve suffered will all come in handy at some point in your life.

+
+

第四阶段 RCA/总结

现在是你把经验教训变为财富的时刻了。

+

什么是好的技术 Leader?

+

随便一个业务需求或业务场景讲出来,你立刻把它抽象为几个模块/系统/Topic,然后侃侃而谈,业界都是怎么解决的,我们以前又是怎么分析怎么解决的,现在咱们这种情况下应该如何设计,可能会遇到什么问题,我们应该做哪些预防设计,blabla。

+

怎么做到这一点?

+

第一,写 RCA 报告。

+

我以前说过,『窝窝从 2011 年开始,一直坚持每错必查、错了又错就整改、每错必写,用身体力行告诉每一个新员工直面错误、公开技术细节、分享给所有人,长此以往,每一次事故和线上漏测都会变为我们的财富。这就是我们的 RCA(Root Cause Analysis)制度,截止到目前已经收集整理了近两百个详尽的 RCA 报告。』

+

RCA 报告格式为:

+
    +
  1. 背景知识(Optional)
  2. +
  3. 问题现象
  4. +
  5. 影响范围
  6. +
  7. 问题原因
  8. +
  9. 问题分析过程(Optional)
  10. +
  11. 解决办法
  12. +
  13. 后续处理措施:如线上脏数据如何修复,如对用户造成的影响如何弥补等(Optional)
  14. +
  15. 经验教训
  16. +
  17. RCA类型:如代码问题、实施问题、配置问题、设计问题、测试问题
  18. +
+

这样,作为一名合格的老兵,你见过了足够多的血,并且把它们变成了你的人生财富。

+

第二,写总结。

+

话说,要经常拉清单。

+

侃侃而谈得有资料,这些都得是你自己写才能印象深刻,关键时刻想得起来。

+

好了,这就是我告诉面试者的高手炼成四个阶段。

+ +
+ + + + + + + +
+ + + + + + +
+ +
+ + + +
+ + + + + + + + +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git "a/2015/\346\227\266\351\227\264\346\210\263\350\275\254\346\215\242\346\226\271\346\263\225/1.png" "b/2015/\346\227\266\351\227\264\346\210\263\350\275\254\346\215\242\346\226\271\346\263\225/1.png" new file mode 100644 index 0000000000..0e891e6db5 Binary files /dev/null and "b/2015/\346\227\266\351\227\264\346\210\263\350\275\254\346\215\242\346\226\271\346\263\225/1.png" differ diff --git "a/2015/\346\227\266\351\227\264\346\210\263\350\275\254\346\215\242\346\226\271\346\263\225/index.html" "b/2015/\346\227\266\351\227\264\346\210\263\350\275\254\346\215\242\346\226\271\346\263\225/index.html" new file mode 100644 index 0000000000..011edf076d --- /dev/null +++ "b/2015/\346\227\266\351\227\264\346\210\263\350\275\254\346\215\242\346\226\271\346\263\225/index.html" @@ -0,0 +1,504 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 时间戳转换方法 | 贾攀的流水账 + + + + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

贾攀的流水账

+ +
+

Panmax's Blog

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+ + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ 时间戳转换方法 +

+ + +
+ + + + +
+ + +
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
datetime = datetime.datetime.now()
timestamp = time.time()
datetime_format = %Y-%m-%d %H:%M:%S

# str -> datetime
datetime.datetime.strptime(string, datetime_format)

# datetime -> str
datetime.datetime.strftime(datetime_format)
type(a) -> datetime.datetime
a.strftime(datetime_format)

# datetime -> timestamp
time.mktime(datetime.datetime.timetuple())

# timestamp -> datetime
datetime.datetime.fromtimestamp(timestamp)
datetime.datetime.fromtimestamp(timestamp).strftime(datetime_format)

# timestamp -> time
time.localtime(timestamp)
+

+ +
+ + + + + + + +
+ + + + + + +
+ +
+ + + +
+ + + + + + + + +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git "a/2015/\347\216\257\345\242\203/1.png" "b/2015/\347\216\257\345\242\203/1.png" new file mode 100644 index 0000000000..d42031784c Binary files /dev/null and "b/2015/\347\216\257\345\242\203/1.png" differ diff --git "a/2015/\347\216\257\345\242\203/index.html" "b/2015/\347\216\257\345\242\203/index.html" new file mode 100644 index 0000000000..be31862451 --- /dev/null +++ "b/2015/\347\216\257\345\242\203/index.html" @@ -0,0 +1,517 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + explore flask 环境 | 贾攀的流水账 + + + + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

贾攀的流水账

+ +
+

Panmax's Blog

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+ + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ explore flask 环境 +

+ + +
+ + + + +
+ + +

环境

+

版本控制

选择一个版本控制系统并且使用它。我推荐Git。在我看来,Git是最近新项目最流行的选择。能够删除代码而不用担心造成不可逆的错误是很宝贵的。你可以让你的项目中大量被注释的代码快自由了,因为你现在可以删除它们并且在随后需要的时候恢复这些改变。另外,你将拥有完整的项目备份在GitHub,Bitbucket或者你自己的Gitolite服务上。

+ +

避开版本控制

我通常把一个文件放在版本控制之外因为两个原因之一。其中一个是杂乱的东西另一个是秘密的东西。例如编译后的.pyc文件和虚拟环境(如果你因为某些原因没有使用virtualenvwrapper)是杂乱的东西。它们不需要在版本控制中,因为它们可以各自通过.py文件和你的requirements.txt文件被重新创建。

+

例如API密钥,应用密钥和数据库证书是秘密的东西。它们不应该在版本控制中因为它们的暴露将会造成很大的安全问题。

+

调试

调试模式

Flask自带一个很好用的功能叫调试模式。你只需要在你的开发配置中设置debug = True就可以打开它。当调试模式打开,服务器将会在代码发生变化时重新加载并且带有堆栈跟踪和交互控制台。

+

Flask-DebugToolbar

Flask-DebugToolbar是另外一个很棒的工具用来调试你应用中的问题。在调试模式中,它在你的应用中的每一个页面上覆盖一个边栏。这个边栏给你关于SQL查询,日志,版本,模板,配置的信息和其他有趣的东西用来更容易的跟踪问题。

+

总结

    +
  • 使用virtualenv保持你的应用的依赖在一起。
  • +
  • 使用virtualenvwrapper保持你的虚拟环境依赖在一起
  • +
  • 保持一个或多个文本文件的追踪依赖。
  • +
  • 使用版本控制系统。我推荐Git。
  • +
  • 使用’.gitignore’来让杂乱的东西和秘密的东西置于版本控制之外。
  • +
  • 调试模式可以给你关于开发中遇到的问题的信息。
  • +
  • Flask-DebugToolbar扩展将给你更多的信息。
  • +
+ +
+ + + + + + + +
+ + + + + + +
+ +
+ + + +
+ + + + + + + + +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git "a/2015/\347\273\204\347\273\207\344\275\240\347\232\204\351\241\271\347\233\256/1.png" "b/2015/\347\273\204\347\273\207\344\275\240\347\232\204\351\241\271\347\233\256/1.png" new file mode 100644 index 0000000000..ae82ef3aa7 Binary files /dev/null and "b/2015/\347\273\204\347\273\207\344\275\240\347\232\204\351\241\271\347\233\256/1.png" differ diff --git "a/2015/\347\273\204\347\273\207\344\275\240\347\232\204\351\241\271\347\233\256/index.html" "b/2015/\347\273\204\347\273\207\344\275\240\347\232\204\351\241\271\347\233\256/index.html" new file mode 100644 index 0000000000..360466c49b --- /dev/null +++ "b/2015/\347\273\204\347\273\207\344\275\240\347\232\204\351\241\271\347\233\256/index.html" @@ -0,0 +1,578 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + explore flask 组织你的项目 | 贾攀的流水账 + + + + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

贾攀的流水账

+ +
+

Panmax's Blog

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+ + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ explore flask 组织你的项目 +

+ + +
+ + + + +
+ + +

组织你的项目

+

Flask将你的应用的组织工作有你来决定。这也是我像初学者一样喜欢Flask的原因之一,但是这确实意味着你必须对如何构建你的代码做一番思考。你可以把你整个应用放在一个文件中,或者把它分散在多个包中。这里有一些你可以遵循的组织模式,可以更轻松的开发和部署。

+ +

定义

让我们来定义一些在本章中会遇到的术语

+

版本库 - 这是放置你的项目的基础文件夹。这个术语传统上指的是版本控制系统,你应该使用版本控制系统。当我在本章中提到你的存放库,我将说的是你的项目的跟目录。当在你的应用中工作时,你可能将不会离开这个目录。

+

- 这个指的是一个包含你的应用代码的Python包。我将会在本章谈论更多的关于设置你的作为一个包,但是现在只需要知道包是版本库的一个子目录。

+

模块 - 一个模块是一个单独的Python文件可以被其他Python文件所导入。一个包本质上是多个模块被包裹在一起。

+
注意
+

组织模式

单模块

你会遇到很多Flask示例将所有的代码放在一个文件中,常常是app.py。这对于快速项目是很好的(比如一个用来教学的项目),你只需要在这个文件里提供一些路由并且你已经获得少于几百行的应用代码了。

+
1
2
3
4
5
app.py
config.py
requirements.txt
static/
templates/
+

应用逻辑将放在清单的app.py中。

+

当你工作的项目稍微有些复杂时,一个单独的模块会导致混乱。你将需要为模型和表单定义类,并且他们将与你的路由和配置代码混合起来。所有的这些会阻碍开发。为了解决这个问题,我们能够把我们应用的不同的组件分解出来进行分组形成相互连接的模块 – 一个包。

+
1
2
3
4
5
6
7
8
9
10
11
12
config.py
requirements.txt
run.py
instance/
config.py
yourapp/
__init__.py
views.py
models.py
forms.py
static/
templates/
+

在这个列表中展示的这种结构允许你将应用中不同的组件按照符合逻辑的方式进行分组。定义模型的类一起放在models.py中,路由定义放在views.py中,表单定义放在forms.py中(我们稍后有整章来介绍表单)。

+

下边的表提供一个基本的组件概述,你会在大多数的Flask应用中见到。你可能最终在你的版本库中有很多其它文件,但是这些是大多数Flask应用中最普遍的。

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
文件描述
run.py这个文件被调用启动开发服务器。它从你的包中获得应用的副本并且运行它。这个不能用在生产中,但是它将在开发中有很多的用途。
requirements.txt这个文件列出所有你的应用所依赖的Python包。你可能为生产和开发依赖准备了各自的文件。
config.py这个文件包含了大多数你的应用所需要的配置变量。
/instance/config.py这个文件包含不应该在版本控制中的配置变量。这里包括的东西比如API密钥和数据库RUIs包含的密码。这个也包含你的应用特定情况下的特殊的变量。比如你可能在config.py中有DEBUG = False,但是设置DEBUG = True在 instance/config.py在你作为开发的本地设备中。因为这个文件会在config.py后读取,这将会覆盖它并且设置DEBUG = True
/yourapp/这是一个包含你应用程序的包。
/yourapp/__init__.py这个文件初始化你的应用并且汇集把所有不同的组件汇集在一起。
/yourapp/views.py这是路由被定义的地方。它可能切分为它自己的包(yourapp/views)与有联系的视图组合在一起成为一个模块。
/yourapp/models.py这是定义你的应用模型的地方。这个可能以views.py相同的方式分成多个模块。
/yourapp/static/这个文件包含公开的CSS,JavaScript,图片和其他你想通过应用程序公开的文件。这些默认是可以通过 yourapp.com/static/ 来访问的。
/yourapp/templates/这是将要放置你的应用Jinja2模板的地方。
+

蓝图

有时你可能会发现你有很多相关的路由。如果你像我一样,你的第一个想法是将views.py切分成一个包并且将这些视图分组到一个模块中。这种情况下,可能是时候考虑将你的应用放在蓝图中了。

+

蓝图本质上是有点自包含方式定义你的应用的组件。他们作为你应用程序中的应用。你可能为管理员控制台、前后端和用户仪表盘有不同的蓝图。这让你通过组件来分组视图、静态文件和模板,同时来人让你分享模型、表单和其他你的应用程序在这些组件之间的部分。我们马上将会谈论使用蓝图组织你的应用。

+

总结

    +
  • 为你的应用使用单一的模块,对于快速项目来说是很好的。
  • +
  • 为你的项目使用包,对于项目中的视图、模型、表单和其他组件来说是很好的。
  • +
  • 蓝图是用几个不同的组件来组织项目很好的方式。
  • +
+

【完】

+
+ + + + + + + +
+ + + + + + +
+ +
+ + + +
+ + + + + + + + +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git "a/2015/\350\207\252\345\267\261\346\200\273\347\273\223feed\346\265\201\347\232\204\344\272\247\347\224\237/index.html" "b/2015/\350\207\252\345\267\261\346\200\273\347\273\223feed\346\265\201\347\232\204\344\272\247\347\224\237/index.html" new file mode 100644 index 0000000000..981456dc7e --- /dev/null +++ "b/2015/\350\207\252\345\267\261\346\200\273\347\273\223feed\346\265\201\347\232\204\344\272\247\347\224\237/index.html" @@ -0,0 +1,522 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 自己总结feed流的产生 | 贾攀的流水账 + + + + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

贾攀的流水账

+ +
+

Panmax's Blog

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+ + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ 自己总结feed流的产生 +

+ + +
+ + + + +
+ + +
    +
  • 推模式:被关注者产生一条news,会给所有的关注者每人生成一条feed数据。

    +
      +
    • 优点:查询速度快,性能高
    • +
    • 缺点:产生数据条目多,写入量大

      +
    • +
    • 最严重缺点:这种模式类似朋友圈,只有关注时开始,才给关注者产生feed数据,之前的被关注者发布的news是收不到feed的(其实也可以收到,不过相当麻烦,假如之前被关注者已经发布了很多news了,需要逐个为之前的news生成feed)

      +
    • +
    +
  • +
+ +
+
    +
  • 拉模式:被关注者产生一条news,只产生一条和被关注者相关的feed数据,其他用户在看自己关注的人feed流时,逐个查询他所关注人的feed数据,然后展示结果。

    +
      +
    • 优点:写入量小,关注后即可看到关注者之前产生的feed数据
    • +
    • 缺点:查询时性能低
    • +
    +
  • +
+
+

以上都没有考虑使用缓存进行优化

+

如有错误请指正~

+ +
+ + + + + + + +
+ + + + + + +
+ +
+ + + +
+ + + + + + + + +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git "a/2015/\350\247\206\345\233\276\345\222\214\350\267\257\347\224\261\347\232\204\351\253\230\347\272\247\346\250\241\345\274\217/1.png" "b/2015/\350\247\206\345\233\276\345\222\214\350\267\257\347\224\261\347\232\204\351\253\230\347\272\247\346\250\241\345\274\217/1.png" new file mode 100644 index 0000000000..fd32152fa6 Binary files /dev/null and "b/2015/\350\247\206\345\233\276\345\222\214\350\267\257\347\224\261\347\232\204\351\253\230\347\272\247\346\250\241\345\274\217/1.png" differ diff --git "a/2015/\350\247\206\345\233\276\345\222\214\350\267\257\347\224\261\347\232\204\351\253\230\347\272\247\346\250\241\345\274\217/index.html" "b/2015/\350\247\206\345\233\276\345\222\214\350\267\257\347\224\261\347\232\204\351\253\230\347\272\247\346\250\241\345\274\217/index.html" new file mode 100644 index 0000000000..f87cdbb775 --- /dev/null +++ "b/2015/\350\247\206\345\233\276\345\222\214\350\267\257\347\224\261\347\232\204\351\253\230\347\272\247\346\250\241\345\274\217/index.html" @@ -0,0 +1,591 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + explore flask 视图和路由的高级模式 | 贾攀的流水账 + + + + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

贾攀的流水账

+ +
+

Panmax's Blog

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+ + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ explore flask 视图和路由的高级模式 +

+ + +
+ + + + +
+ + +

视图和路由的高级模式

+

视图装饰器

Python装饰器是用来改变其他函数的函数。当装饰函数被调用,这个装饰器被调用替代。然后装饰器能够才去行动,修改参数,停止执行或者调用原函数。我们能使用装饰器来包装视图来运行他们执行前的代码。

+
1
2
3
@decorator_function
def decorated():
pass
+

如果你浏览过Flask的教程,你可能很熟悉这个代码块中的语法。`@app.route`Flask应用中是用来匹配URL到视图函数的装饰器。

+

来看一些其他你能够在你的Flask应用中使用的装饰器。

+ +

认证

Flask-Login扩展使可以很容易的实现一个登录系统。出了处理用户认证的细节,Flask-Login给了我们一个装饰器用来限制某些视图给已经认证的用户:@login_required

+
1
2
3
4
5
6
7
8
9
10
11
12
13
# app.py

from flask import render_template
from flask.ext.login import login_required, current_user

@app.route('/')
def index():
return render_template("index.html")

@app.route('/dashboard')
@login_required
def account():
return render_template("account.html")
+
警告

@app.route应该永远是最远的视图装饰器。

+
+

只有被认证的用户将能够访问/dashboard路由。我们能配置Flask-Login让没有认证的用户跳转到登录页面,返回一个HTTP 401状态或者任何其他我们希望他们做的。

+
注意

官方文档阅读更多关于Flask-Login的使用。

+
+

缓存

想象一篇提及到我们应用的文章刚刚发表在CNN和其他新闻站点。我们每秒钟获得成千上万的请求。我们的主页为每个请求前往数据库多次,所以这一切注意力都放慢下来到爬行。我们如何让速度快速加快,因此所有这些访问者就不会错过我们的站点。

+

这里有很多好的回答,但是这个部分是关于缓存的,所以我们将要谈谈关于缓存的东西。明确来说,我们将要使用Flask-Cache扩展。这个扩展提供给我们一个装饰器,我们可以用在我们的主页视图上用来在一段时间内缓存响应。

+

Flask-Cache 能够被配置和很多不同的缓存后端一起工作。一个流行的选择是Redis,这个我们可以简答设置和使用。假定Flask-Cache已经被配置完成,这段代码块展示我们的装饰器视图是什么样子的。

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# app.py

from flask.ext.cache import Cache
from flask import Flask

app = Flask()

# We'd normally include configuration settings in this call
cache = Cache(app)

@app.route('/')
@cache.cached(timeout=60)
def index():
[...] # Make a few database calls to get the information we need
return render_template(
'index.html',
latest_posts=latest_posts,
recent_users=recent_users,
recent_photos=recent_photos
)
+

现在这个函数将会每60秒只运行一次,这时候缓存过期。这个响应将会被保存在我们的缓存中并且为任何有障碍的请求从这里获取响应。

+
注意

Flask-Cache 也让我们memoize函数或者缓存用确定参数调用的函数的结果。我们甚至能够缓存计算昂贵的Jinja2模板片段。

+
+

自定义装饰器

在这部分,让我们想象我们有一个应用来让我们的用户每个月付费,如果一个用户的账户到期了,我们将会跳转他们到结账页面并且告诉他们去升级。

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# myapp/util.py

from functools import wraps
from datetime import datetime

from flask import flash, redirect, url_for

from flask.ext.login import current_user

def check_expired(func):
@wraps(func)
def decorated_function(*args, **kwargs):
if datetime.utcnow() > current_user.account_expires:
flash("Your account has expired. Update your billing info.")
return redirect(url_for('account_billing'))
return func(*args, **kwargs)

return decorated_function
+ + + + + + + + + + + + + + + + + + + + + + + + + +
行数注释
10当一个函数被@check_expried装饰,check_expried()被调用并且被装饰的函数被作为参数传递。
11@warps是一个装饰器用来做一些簿记,使被装饰的函数()显示为func()的文档和调试的目的。这使这个函数的行为更正常一点。
12。。。
16。。。
+

当我们把装饰器叠在一起时,最上边的装饰器将会第一个运行,然后调用下一行的函数:视图函数或者下一个装饰器之一。装饰器语法只是一点点语法糖。

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# This code:
@foo
@bar
def one():
pass

r1 = one()

# is the same as this code:
def two():
pass

two = foo(bar(two))
r2 = two()

r1 == r2 # True
+

这个代码块使用我们自定义装饰器和来自Flask-Login扩展的@login_required装饰器展示一个例子。我们能使用多个装饰器通过把他们叠在一起。

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# myapp/views.py

from flask import render_template

from flask.ext.login import login_required

from . import app
from .util import check_expired

@app.route('/use_app')
@login_required
@check_expired
def use_app():
"""Use our amazing app."""
# [...]
return render_template('use_app.html')

@app.route('/account/billing')
@login_required
def account_billing():
"""Update your billing info."""
# [...]
return render_template('account/billing.html')
+

现在当一个用户尝试访问 /user_appcheck_expired()将会在运行这个视图函数前确定他们的账户没有过期。

+
注意

Python docs了解更多关于warps()函数的作用。

+
+

URL转换器

内置转换器

当你在Flask里定义一个路由,你能够指定它的一部分转换成Python变量并且传递给视图函数。

+
1
2
3
@app.route('/user/<username>')
def profile(username):
pass
+

无论。。URL标签<username>将会被传递到视图作为username参数。你也能够指定一个转换器在变量被传到视图前过滤它。

+
1
2
3
@app.route('/user/id/<int:user_id>')
def profile(user_id):
pass
+

在这个代码块,这个URL http://myapp.com/user/id/Q29kZUxlc3NvbiEh 将会返回404代码-未找到。这是因为这个URL的部分被支持变成整型实际上是一个字符串。

+

我们还可以有第二个视图来查找字符串。这会被/user/id/Q29kZUxlc3NvbiEh/调用,与此同时第一个会被/user/id/124调用。

+

这个表展示Flask的内置URL转换器。

+ + + + + + + + + + + + + + + + + + + + + + + + + +
名称描述
string。。。
int。。。
float。。。
path。。。
+

自定义转换器

我们同样可以自定义转换器来满足我们度需要。在Reddit上(一个流行的连接分享站点),用户创建和主持主题讨论型社区和链接分享。一些例子是/r/python/r/flask,被表示为URL的路径:分别是reddit.com/r/pythonreddit.com/r/flask。一个Reddit有意思的功能是你能够观看来自多个子reddits的文章通过加号作为连接多个名字,例如reddit.com/r/python+flask

+

我们可以在我们自己的Flask应用中使用自定义转换器实线这个功能。

+ +
+ + + + + + + +
+ + + + + + +
+ +
+ + + +
+ + + + + + + + +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git "a/2015/\350\256\260\344\270\200\346\254\241\344\270\232\345\212\241\351\200\273\350\276\221\344\274\230\345\214\226/index.html" "b/2015/\350\256\260\344\270\200\346\254\241\344\270\232\345\212\241\351\200\273\350\276\221\344\274\230\345\214\226/index.html" new file mode 100644 index 0000000000..e501d37a61 --- /dev/null +++ "b/2015/\350\256\260\344\270\200\346\254\241\344\270\232\345\212\241\351\200\273\350\276\221\344\274\230\345\214\226/index.html" @@ -0,0 +1,514 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 记一次业务逻辑优化 | 贾攀的流水账 + + + + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

贾攀的流水账

+ +
+

Panmax's Blog

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+ + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ 记一次业务逻辑优化 +

+ + +
+ + + + +
+ + +

我们之前新鲜的逻辑是这样的:每个用户在redis中有一个自己的队列,队列中记录的照片的ID,当有新照片产生的时候,不区分性别,往每个人队列最前边插入(lpush)这条数据,每个人的队列最大值为1000,超出部分被截断,当用户看过队列中某张照片后,这张照片会从队列中移除(lrem)

+

这种模式刚开始没有问题,后来新鲜增加了可以筛选性别的需求。因为队列中所有性别都是混在一起的,所以每次从队列中取出数据后,需要把Photo实体取出来,然后进行性别过滤,把过滤出来的结果再返回给客户端。如果筛选结果后发现数量不足(通常是20),就重新从库中重新查询,拿出前2000张,过滤掉我要的性别(因为我们用的leancloud平台,他们不支持关联查询,我们的photo不记录性别,需要先取出后再通过user才能知道性别),再过滤掉我看过的,能找到就返回,找不到就算了。假如我把筛选改为女,然后我看啊看,看啊看,早晚我会把队列中的女性照片看完。因为我们只查询前2000张,所以后边的照片我根本看不到,除非等着有女性用户新发照片。而且看的照片越多,查询速度越慢。

+

下边讲一下优化的方法

+ +

这个优化是基于假设用户很少切换性别的基础上进行的:

+
    +
  1. 我为照片表中的数据新增了sex列,为每张照片标记了性别(和发布者性别相同,实为下策略,不过没办法。。。)
  2. +
  3. 每个用户队列中只保存他所选择性别的照片(全部、男性、女性)
  4. +
  5. cache中为每个用户保存上次选择的性别 cache:feed:last:sex:u_id,并且记录上次查询到最后那张照片的ID cache:feed:last:photo:p_id
  6. +
  7. 如果用户再次请求,并且性别不变,那么使用上次查询到的那个照片id继续往后查询m张,因为照片表中有了性别,所以查询效率提升很多。如果中间满足条件的张数大于n,停止查询,并记录最后张ID。
  8. +
  9. 如果用户切换性别,重新开始查询,更新用户最后一次查询的性别,记录最后查询到的照片ID。从头重新查询。
  10. +
  11. 在有用户创建新照片以后,需要发通知给offline(redis的发布/订阅实现),之前只发送照片id,现在改为发送照片id和照片性别。
  12. +
  13. offline收到通知后,将这张照片插入到符合性别和不进行性别过滤的那些用户队列里。
  14. +
+

这样做之后性能提升了很多很多~

+ +
+ + + + + + + +
+ + + + + + +
+ +
+ + + +
+ + + + + + + + +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git "a/2015/\350\256\260\345\275\225\347\254\254\344\270\200\346\254\241\347\216\251\351\230\277\351\207\214\344\272\221/index.html" "b/2015/\350\256\260\345\275\225\347\254\254\344\270\200\346\254\241\347\216\251\351\230\277\351\207\214\344\272\221/index.html" new file mode 100644 index 0000000000..f1f82776b9 --- /dev/null +++ "b/2015/\350\256\260\345\275\225\347\254\254\344\270\200\346\254\241\347\216\251\351\230\277\351\207\214\344\272\221/index.html" @@ -0,0 +1,525 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 记录第一次玩阿里云 | 贾攀的流水账 + + + + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

贾攀的流水账

+ +
+

Panmax's Blog

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+ + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ 记录第一次玩阿里云 +

+ + +
+ + + + +
+ + +

看到阿里云的美国硅谷区的ECS正在高活动,而且最近正好想学习学习Linux服务器相关的只是,所以就购入了一部。

+

我选择的是1G内存,1G CPU,按流量计费。

+

支付宝付完款后,居然没有自动进入完成付款页面。。。

+

首先进入系统后,我先更改了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 -)"

+

弱弱的说一句,硅谷区下载国外的资源真心快。。。

+

按照池建强的教程,进行了一些zsh简单的配置:http://macshuo.com/?p=676

+

本来想直接安装virtualenvwrapper结果发现,python原生不带pip,所以进行pip的安装:

+
    +
  1. 先从官网把安装源文件下载下来:curl "https://bootstrap.pypa.io/get-pip.py" -o "get-pip.py"
  2. +
  3. 安装 sudo python get-pip.py
  4. +
+

发现curl也没装。。。所以先安装curl。

+
    +
  1. sudo apt-get update
  2. +
  3. sudo apt-get install curl
  4. +
+

终于可以安装virtualenvwrapper了:sudo pip install virtualenvwrapper

+

安装完成后,将下边内容放在~/.bashrc

+
1
2
3
4
5
6
# where to store our virtual envs
export WORKON_HOME=$HOME/virtenvs
# where projects will reside
export PROJECT_HOME=$HOME/Projects-Active
# where is the virtualenvwrapper.sh
source $HOME/.local/bin/virtualenvwrapper.sh
+

然后执行source ~/.zshrc

+

安装完成!然后就可以创建虚拟环境搞Python开发了~

+

今天就到这里。。。

+ +
+ + + + + + + +
+ + + + + + +
+ +
+ + + +
+ + + + + + + + +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git "a/2015/\351\205\215\347\275\256/1.png" "b/2015/\351\205\215\347\275\256/1.png" new file mode 100644 index 0000000000..6d9983282e Binary files /dev/null and "b/2015/\351\205\215\347\275\256/1.png" differ diff --git "a/2015/\351\205\215\347\275\256/index.html" "b/2015/\351\205\215\347\275\256/index.html" new file mode 100644 index 0000000000..ecc276f9c4 --- /dev/null +++ "b/2015/\351\205\215\347\275\256/index.html" @@ -0,0 +1,579 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + explore flask 配置 | 贾攀的流水账 + + + + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

贾攀的流水账

+ +
+

Panmax's Blog

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+ + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ explore flask 配置 +

+ + +
+ + + + +
+ + +

配置

原文地址: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"]

+
1
2
3
4
5
6
7
# app.py or app/__init__.py
from flask import Flask

app = Flask(__name__)
app.config.from_object('config')

# 现在我们能够通过app.config["变量名"]来访问配置变量。
+ + + + + + + + + + + + + + + + + + + + + +
变量描述
DEBUGtodo…
SECRET_KEYtodo…
BCRYPT_LEVELtodo…
+
!警告
+请确定在生产环境下`DEBUG`设置为`False`。不然将会允许用户在你的服务器上运行任何Python代码。
+

实例文件夹

有时你需要定义包含敏感信息的配置变量。我们想将这些变量从config.py分离出来并且将他们保存在代码库之外。你可能要隐藏保密的东西比如数据库密码和API密钥或者给机器定义详细的变量。为了让这容易,Flask给我们提供了一个叫实例文件夹的功能。实例文件夹是代码库根目录的子目录并且为这个应用实例包含了一个特殊的配置文件。我们不想在版本控制中提交它。

+
1
2
3
4
5
6
7
8
9
10
11
config.py
requirements.txt
run.py
instance/
config.py
yourapp/
__init__.py
models.py
views.py
templates/
static/
+

使用实例文件夹

为了加载来自实例文件夹的配置变量,我们使用app.config.from_pyfile()。当我们创建我们的应用并用Flask()调用时,如果我们设置instance_relative_config=Trueapp.config.from_pyfile()将会通过instance/directory加载指定的文件。

+
1
2
3
4
5
# app.py or app/__init__.py

app = Flask(__name__, instance_relative_config=True)
app.config.from_object('config')
app.config.from_pyfile('config.py')
+

密钥

实例文件夹的私有本质为定义密钥而不想暴露在版本库中提供了很好的候选。这些可能包含你应用的密钥或者第三方API的密钥。如果你的应用是开源的这尤其重要或者也许会在未来某一时刻开源。我们通常想让其他用户或者贡献者使用他们自己的密钥。

+
1
2
3
4
5
6
# instance/config.py

SECRET_KEY = 'Sm9obiBTY2hyb20ga2lja3MgYXNz'
STRIPE_API_KEY = 'SmFjb2IgS2FwbGFuLU1vc3MgaXMgYSBoZXJv'
SQLALCHEMY_DATABASE_URI= \
"postgresql://user:TWljaGHFgiBCYXJ0b3N6a2lld2ljeiEh@localhost/databasename"
+

较小的环境基础配置

如果你的生产环境和开发环境之间的不同很小,你可能想使用你的实例文件夹处理配置的改变。定义在instance/config.py文件中的变量能够覆盖config.py中的变量。你只需要在app.config.from_object()之后调用app.config.from_pyfile()。利用这个方法是改变你的应用在不同机器配置的一种方式。

+
1
2
3
4
5
6
7
8
9
# config.py

DEBUG = False
SQLALCHEMY_ECHO = False


# instance/config.py
DEBUG = True
SQLALCHEMY_ECHO = True
+

在生产中,我们将离开列表中的值,在instance/config.py之外,并且它将会回落到config.py定义的值。

+

配置基于环境变量

实例文件夹不应该在版本控制中。这意味着你将无法跟踪你实例配置的变化。这可能对于一两个值来说不是问题,但是如果你在不同环境中(生产、升级、开发等)有微调的配置,你不想冒险失去这些。

+

Flask给我们基于环境变量的值去选择一个配置文件来加载的能力。这意味着我们能有多个配置文件在我们的代码库中并且总是加载正确的那个。每次我们有几个不同的配置文件,我们能够移动他们到他们自己的config目录中。

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
requirements.txt
run.py
config/
__init__.py # Empty, just here to tell Python that it's a package.
default.py
production.py
development.py
staging.py
instance/
config.py
yourapp/
__init__.py
models.py
views.py
static/
templates/
+

在这个列表中我们有少量不同的配置文件。

+ + + + + + + + + + + + + + + + + + + + + + + + + +
文件介绍
config/default.py
config/development.py
config/production.py
config/staging.py
+

为了决定加载哪个配置文件,我们将调用app.config.from_envvar()

+
1
2
3
4
5
6
7
8
9
10
11
12
13
# yourapp/__init__.py

app = Flask(__name__, instance_relative_config=True)

# Load the default configuration
app.config.from_object('config.default')

# Load the configuration from the instance folder
app.config.from_pyfile('config.py')

# Load the file specified by the APP_CONFIG_FILE environment variable
# Variables defined here will override those in the default configuration
app.config.from_envvar('APP_CONFIG_FILE')
+

环境变量的值应该是配置文件的绝对路径。

+

我们如何设置这个环境变量,取决于我们的应用正在运行的平台。如果我们运行在一个普通的Linux服务器上,我们能够设置一个shell脚本来设置我们的环境变量并且运行run.py

+
1
2
3
4
# start.sh

APP_CONFIG_FILE=/var/www/yourapp/config/production.py
python run.py
+

start.sh在每个环境中是唯一的,所以它应该离开版本控制。在Herok中,我们想要使用Heroku工具设置环境变量。相同的思路应用于其他PaaS平台上。

+

总结

    +
  • 一个简单的应用可能只需要一个配置文件:config.py
  • +
  • 实例文件夹能够帮助我们隐藏保密的配置值。
  • +
  • 实例文件夹能够用于应用的配置之后为了特定的环境。
  • +
  • 我们应该使用环境变量并且为更复杂的基于环境的配置使用app,config.from_envvar()
  • +
+ +
+ + + + + + + +
+ + + + + + +
+ +
+ + + +
+ + + + + + + + +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git "a/2016/AWS-\346\220\255\345\273\272SS\346\234\215\345\212\241\345\231\250/index.html" "b/2016/AWS-\346\220\255\345\273\272SS\346\234\215\345\212\241\345\231\250/index.html" new file mode 100644 index 0000000000..5e1d82be0f --- /dev/null +++ "b/2016/AWS-\346\220\255\345\273\272SS\346\234\215\345\212\241\345\231\250/index.html" @@ -0,0 +1,586 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + AWS 搭建SS服务器 | 贾攀的流水账 + + + + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

贾攀的流水账

+ +
+

Panmax's Blog

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+ + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ AWS 搭建SS服务器 +

+ + +
+ + + + +
+ + +

今天看到亚马逊云服务的广告,说现在开通AWS可以免费用一年。然后我爱占小便宜心又犯了,所以绑定信用卡,开通了AWS,在这过程中,不知道为什么扣了我两笔6.59的钱,想联系客服也找不到人。。

+

进入AWS主页后看到有很多服务可以用,实在眼花缭乱,先一个一个来,我猜测EC2的意思就和阿里云主机是一个意思,所以就开通了一个,选择的是新加坡节点,果然开通时提示我可以免费用一年。既然这样的话,不如拿来搭一个ss服务器吧,哈哈哈~(因为其他暂时没想到做什么用,也许以后我会在这上边部署一个爬虫之类的)

+

在此过程中,让我下载了一个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+,有些略失望~

+ +

更新和安装需要用到的包:

+

sudo 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密码。

+

接下来创建数据库:

+

mysql -u root -p 输入安装mysql时设置的root密码。

+

创建shadowsocks数据库:

+

create database shadowsocks

+

然后建立一个名为ss,密码为ss的MySQL用户,因为这个用户只能本地登录,所以密码简单点也无所谓:

+

grant all privileges on shadowsocks.* to ss@localhost identified by 'ss';

+

到这步,我们的数据库已经完成了,,下面我们来安装shadowsocks ss-panel supervisor,一次执行下面的命令:

+

sudo 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

+
1
2
3
4
5
6
7
8
9
10
11
#Config
MYSQL_HOST = 'localhost'
MYSQL_PORT = 3306
MYSQL_USER = 'ss'
MYSQL_PASS = 'ss'
MYSQL_DB = 'shadowsocks'
MANAGE_PASS = 'ss233333333'
#if you want manage in other server you should set this value to global ip
MANAGE_BIND_IP = '127.0.0.1'
#make sure this port is idle
MANAGE_PORT = 23333
+

然后我们还要修改这个文件/root/shadowsocks/shadowsocks/config.json

+
1
2
3
4
5
6
7
8
9
10
{
"server":"0.0.0.0",
"server_ipv6": "[::]",
"server_port":8388,
"local_address": "127.0.0.1",
"local_port":1080,
"password":"m",
"timeout":300,
"method":"aes-256-cfb"
}
+

然后我们来导入数据库。进入MySQL:

+

mysql -u root -p

+

use shadowsocks;

+

source ~/shadowsocks/shadowsocks/shadowsocks.sql;

+

exit

+

导入数据库之后,我们在shadowsocks目录下运行一下server.py,python server.py

+

没有error的话,ctrl + c结束进程,我们进行下一步,安装守护进程,这样重启以后或者程序崩了还能自己重启。

+

sudo apt-get install python-pip python-m2crypto supervisor

+

然后我们需要新建两个文件,具体如下:

+

sudo vim /etc/supervisor/conf.d/shadowsocks.conf

+

内容:

+
1
2
3
4
[program:shadowsocks]
command=python /home/panmax/shadowsocks/shadowsocks/server.py -c /home/panmax/shadowsocks/shadowsocks/config.json
autorestart=true
user=root
+

再创建一个文件:

+

sudo vim /etc/supervisor/conf.d/cgi.conf

+

内容:

+
1
2
3
4
[program:cgi]
command=php5-cgi -b localhost:9000
autorestart=true
user=root
+

然后命令:

+

cd shadowsocks/shadowsocks

+

service supervisor start

+

supervisorctl reload

+

在以下两个文件/etc/profile和 /etc/default/supervisor结尾添加如下代码(/etc/default/supervisor不存在,直接sudo vi /etc/default/supervisor 即可):

+
1
2
3
ulimit -n 51200  
ulimit -Sn 4096
ulimit -Hn 8192
+

至此ss的后端服务已经搞定了,现在我们来整前端界面:

+

cd /usr/share/nginx/

+

wget -b v2 https://github.com/orvice/ss-panel/archive/master.zip

+

安装解压软件:

+

sudo apt-get install unzip

+

解压文件:

+

sudo unzip master.zip

+

然后重命名文件夹,

+

mv ss-panel-master ss

+

现在来修改文件夹权限,

+

cd /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:

+

mysql -u root -p

+

use shadowsocks;

+

source /usr/share/nginx/ss/sql/invite_code.sql;

+

然后我们需要将ss-pane中的数据库导入我们刚刚创建的数据库中,查看/usr/share/nginx/ss/sql下的内容,把里边的文件导入:

+

例如:

+
1
2
3
use shadowsocks;
source /usr/share/nginx/ss/sql/invite_code.sql;
...
+

然后我们来修改配置文件

+
1
2
cd /usr/share/nginx 
mv /usr/share/nginx/ss/lib/config-simple.php /usr/share/nginx/ss/lib/config.php
+

修改congfig.php里边的数据库相关配置信息

+

到此,ss-panel前端界面也安装完毕,然后我们需要修改一下Nginx配置文件

+
1
2
cd /etc/nginx/sites-available/
sudo vim default
+

修改为

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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;
}
}

+

然后重启一下

+
1
root@ubuntu-512mb-sfo1-01:~# shutdown -r now
+

完成。

+

/admin 进入管理员界面。

+

默认帐号:first@blood.com

+

默认密码:1993

+

绑定域名:

+

新增CNAME,主机记录ss,记录值为AWS EC2的公有 DNS

+ +
+ + + + + + + +
+ + + + + + +
+ +
+ + + +
+ + + + + + + + +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git "a/2016/Airbnb-React-\347\274\226\347\240\201\350\247\204\350\214\203/index.html" "b/2016/Airbnb-React-\347\274\226\347\240\201\350\247\204\350\214\203/index.html" new file mode 100644 index 0000000000..aeb002c1f0 --- /dev/null +++ "b/2016/Airbnb-React-\347\274\226\347\240\201\350\247\204\350\214\203/index.html" @@ -0,0 +1,645 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Airbnb React 编码规范 | 贾攀的流水账 + + + + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

贾攀的流水账

+ +
+

Panmax's Blog

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+ + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ Airbnb React 编码规范 +

+ + +
+ + + + +
+ + +

英文原文地址: Airbnb React/JSX Style Guide

+

Airbnb React/JSX Style Guide

+

用更合理的方式书写 React 和 JSX

+
+

基本规则

    +
  • 一个文件内只包含一个 React 组件。

    + +
  • +
  • 总是使用 JSX 语法。

    +
  • +
  • 不要使用 React.createElement,除非你从一个不是 JSX 的文件初始化你的应用。
  • +
+

Class vs React.createClass vs stateless

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// bad
const Listing = React.createClass({
// ...
render() {
return <div>{this.state.hello}</div>;
}
});

// good
class Listing extends React.Component {
// ...
render() {
return <div>{this.state.hello}</div>;
}
}
+
    +
  • 如果没有内部 state 或者 refs,那么普通函数 (非箭头函数) 比类的写法更好:
  • +
+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// bad
class Listing extends React.Component {
render() {
return <div>{this.props.hello}</div>;
}
}

// bad (since arrow functions do not have a "name" property)
const Listing = ({ hello }) => (
<div>{hello}</div>
);

// good
function Listing({ hello }) {
return <div>{hello}</div>;
}
+

命名

    +
  • 扩展名:React 组件使用.jsx扩展名
  • +
  • 文件名:文件名使用帕斯卡命名。 例如: ReservationCard.jsx
  • +
  • 引用命名:React 组件使用帕斯卡命名,引用实例采用骆驼命名。 eslint: react/jsx-pascal-case
  • +
+
1
2
3
4
5
6
7
8
9
10
11
// bad
import reservationCard from './ReservationCard';

// good
import ReservationCard from './ReservationCard';

// bad
const ReservationItem = <ReservationCard />;

// good
const reservationItem = <ReservationCard />;
+

命名

    +
  • 扩展: 使用 .jsx React 组件的扩展名。
  • +
  • 文件名: 为文件使用帕斯卡命名方式(PascalCase)。 例如: ReservationCard.jsx
  • +
  • 引用命名:为 React组件 使用帕斯卡命名方式(PascalCase),为他们的实例使用驼峰方式命名(camelCase)。eslint: react/jsx-pascal-case
  • +
+
1
2
3
4
5
6
7
8
9
10
11
// bad
import reservationCard from './ReservationCard';

// good
import ReservationCard from './ReservationCard';

// bad
const ReservationItem = <ReservationCard />;

// good
const reservationItem = <ReservationCard />;
+
    +
  • 组件命名:组件名称应该和文件名一致。例如: ReservationCard.jsx 应该有一个 ReservationCard 的引用名称。 然而,如果是在目录中的组件, 应该使用 index.jsx 作为文件名并且使用目录名称作为组件名:
  • +
+
1
2
3
4
5
6
7
8
// bad
import Footer from './Footer/Footer';

// bad
import Footer from './Footer/index';

// good
import Footer from './Footer';
+

声明

    +
  • 不要使用 displayName 属性来命名组件,应该使用类的引用名称。
  • +
+
1
2
3
4
5
6
7
8
9
// bad
export default React.createClass({
displayName: 'ReservationCard',
// stuff goes here
});

// good
export default class ReservationCard extends React.Component {
}
+

对齐

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// bad
<Foo superLongParam="bar"
anotherSuperLongParam="baz" />

// good
<Foo
superLongParam="bar"
anotherSuperLongParam="baz"
/>

// if props fit in one line then keep it on the same line
<Foo bar="bar" />

// children get indented normally
<Foo
superLongParam="bar"
anotherSuperLongParam="baz"
>
<Quux />
</Foo>
+

引号

    +
  • JSX 的属性都采用双引号("),其他的 JS 都使用单引号。eslint: jsx-quotes
  • +
+
+

为什么这样做?JSX 属性 不能包含转义的引号, 所以当输入 "don't" 这类的缩写的时候用双引号会更方便。标准的 HTML 属性通常也会使用双引号替代单引号,所以 JSX 属性也会遵守这样的约定。

+
+
1
2
3
4
5
6
7
8
9
10
11
// bad
<Foo bar='bar' />

// good
<Foo bar="bar" />

// bad
<Foo style={{ left: "20px" }} />

// good
<Foo style={{ left: '20px' }} />
+

空格

    +
  • 总是在你的自闭标签内包含一个空格。
  • +
+
1
2
3
4
5
6
7
8
9
10
11
12
// bad
<Foo/>

// very bad
<Foo />

// bad
<Foo
/>

// good
<Foo />
+

属性

    +
  • 总是为你的属性名使用驼峰命名(camelCase)。
  • +
+
1
2
3
4
5
6
7
8
9
10
11
// bad
<Foo
UserName="hello"
phone_number={12345678}
/>

// good
<Foo
userName="hello"
phoneNumber={12345678}
/>
+ +
1
2
3
4
5
6
7
8
9
// bad
<Foo
hidden={true}
/>

// good
<Foo
hidden
/>
+

大括号

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// bad
render() {
return <MyComponent className="long body" foo="bar">
<MyChild />
</MyComponent>;
}

// good
render() {
return (
<MyComponent className="long body" foo="bar">
<MyChild />
</MyComponent>
);
}

// good, when single line
render() {
const body = <div>hello</div>;
return <MyComponent>{body}</MyComponent>;
}
+

标签

+
1
2
3
4
5
// bad
<Foo className="stuff"></Foo>

// good
<Foo className="stuff" />
+ +
1
2
3
4
5
6
7
8
9
10
// bad
<Foo
bar="bar"
baz="baz" />

// good
<Foo
bar="bar"
baz="baz"
/>
+

方法

    +
  • 使用箭头函数关闭本地变量。
  • +
+
1
2
3
4
5
6
7
8
9
10
11
12
function ItemList(props) {
return (
<ul>
{props.items.map((item, index) => (
<Item
key={item.key}
onClick={() => doSomethingWith(item.name, index)}
/>
))}
</ul>
);
}
+
    +
  • 为 render 方法的处理事件在构造函数中进行绑定。 eslint: react/jsx-no-bind
  • +
+
+

为什么这样做? 在 render 方法中的 bind 调用每次调用 render 的时候都会创建一个全新的函数。

+
+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// bad
class extends React.Component {
onClickDiv() {
// do stuff
}

render() {
return <div onClick={this.onClickDiv.bind(this)} />
}
}

// good
class extends React.Component {
constructor(props) {
super(props);

this.onClickDiv = this.onClickDiv.bind(this);
}

onClickDiv() {
// do stuff
}

render() {
return <div onClick={this.onClickDiv} />
}
}
+
    +
  • 不要使用下划线前缀为 React 组件的内部方法命名。
  • +
+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// bad
React.createClass({
_onClickSubmit() {
// do stuff
},

// other stuff
});

// good
class extends React.Component {
onClickSubmit() {
// do stuff
}

// other stuff
}
+

排序

    +
  • class extends React.Component 的顺序:
  • +
+
    +
  1. 可选的 static 方法
  2. +
  3. constructor
  4. +
  5. getChildContext
  6. +
  7. componentWillMount
  8. +
  9. componentDidMount
  10. +
  11. componentWillReceiveProps
  12. +
  13. shouldComponentUpdate
  14. +
  15. componentWillUpdate
  16. +
  17. componentDidUpdate
  18. +
  19. componentWillUnmount
  20. +
  21. 点击回调或者事件回调 比如 onClickSubmit() 或者 onChangeDescription()
  22. +
  23. render 函数中的 getter 方法 比如 getSelectReason() 或者 getFooterContent()
  24. +
  25. 可选的 render 方法 比如 renderNavigation() 或者 renderProfilePicture()
  26. +
  27. render
  28. +
+
    +
  • 怎样定义 propTypes, defaultProps, contextTypes等……
  • +
+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
import React, { PropTypes } from 'react';

const propTypes = {
id: PropTypes.number.isRequired,
url: PropTypes.string.isRequired,
text: PropTypes.string,
};

const defaultProps = {
text: 'Hello World',
};

class Link extends React.Component {
static methodsAreOk() {
return true;
}

render() {
return <a href={this.props.url} data-id={this.props.id}>{this.props.text}</a>
}
}

Link.propTypes = propTypes;
Link.defaultProps = defaultProps;

export default Link;
+

React.createClass的排序:eslint: react/sort-comp

+
    +
  1. displayName
  2. +
  3. propTypes
  4. +
  5. contextTypes
  6. +
  7. childContextTypes
  8. +
  9. mixins
  10. +
  11. statics
  12. +
  13. defaultProps
  14. +
  15. getDefaultProps
  16. +
  17. getInitialState
  18. +
  19. getChildContext
  20. +
  21. componentWillMount
  22. +
  23. componentDidMount
  24. +
  25. componentWillReceiveProps
  26. +
  27. shouldComponentUpdate
  28. +
  29. componentWillUpdate
  30. +
  31. componentDidUpdate
  32. +
  33. componentWillUnmount
  34. +
  35. 点击回调或者事件回调 比如 onClickSubmit() or onChangeDescription()
  36. +
  37. getter methods for render like getSelectReason() or getFooterContent()
  38. +
  39. Optional render methods like renderNavigation()or renderProfilePicture()
  40. +
  41. render
  42. +
+

isMounted

+
+

为什么? isMounted是一种反模式,当使用 ES6 类风格声明 React 组件时该属性不可用,并且即将被官方弃用。

+
+ +
+ + + + + + + +
+ + + + + + +
+ +
+ + + +
+ + + + + + + + +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git "a/2016/App\346\236\266\346\236\204\350\256\276\350\256\241\347\273\217\351\252\214\350\260\210-\346\216\245\345\217\243\347\232\204\350\256\276\350\256\241/index.html" "b/2016/App\346\236\266\346\236\204\350\256\276\350\256\241\347\273\217\351\252\214\350\260\210-\346\216\245\345\217\243\347\232\204\350\256\276\350\256\241/index.html" new file mode 100644 index 0000000000..6e9e1b8932 --- /dev/null +++ "b/2016/App\346\236\266\346\236\204\350\256\276\350\256\241\347\273\217\351\252\214\350\260\210-\346\216\245\345\217\243\347\232\204\350\256\276\350\256\241/index.html" @@ -0,0 +1,567 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + App架构设计经验谈:接口的设计 | 贾攀的流水账 + + + + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

贾攀的流水账

+ +
+

Panmax's Blog

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+ + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ App架构设计经验谈:接口的设计 +

+ + +
+ + + + +
+ + +

原文地址:http://keeganlee.me/post/architecture/20160107

+

App与服务器的通信接口如何设计得好,需要考虑的地方挺多的,在此根据我的一些经验做一些总结分享,旨在抛砖引玉。

+

安全机制的设计

现在,大部分App的接口都采用RESTful架构,RESTFul最重要的一个设计原则就是,客户端与服务器的交互在请求之间是无状态的,也就是说,当涉及到用户状态时,每次请求都要带上身份验证信息。实现上,大部分都采用token的认证方式,一般流程是:

+
    +
  1. 用户用密码登录成功后,服务器返回token给客户端;
  2. +
  3. 客户端将token保存在本地,发起后续的相关请求时,将token发回给服务器;
  4. +
  5. 服务器检查token的有效性,有效则返回数据,若无效,分两种情况:
      +
    • token错误,这时需要用户重新登录,获取正确的token
    • +
    • token过期,这时客户端需要再发起一次认证请求,获取新的token
    • +
    +
  6. +
+

然而,此种验证方式存在一个安全性问题:当登录接口被劫持时,黑客就获取到了用户密码和token,后续则可以对该用户做任何事情了。用户只有修改密码才能夺回控制权。

+

如何优化呢?第一种解决方案是采用HTTPS。HTTPS在HTTP的基础上添加了SSL安全协议,自动对数据进行了压缩加密,在一定程序可以防止监听、防止劫持、防止重发,安全性可以提高很多。不过,SSL也不是绝对安全的,也存在被劫持的可能。另外,服务器对HTTPS的配置相对有点复杂,还需要到CA申请证书,而且一般还是收费的。而且,HTTPS效率也比较低。一般,只有安全要求比较高的系统才会采用HTTPS,比如银行。而大部分对安全要求没那么高的App还是采用HTTP的方式。

+

我们目前的做法是给每个接口都添加签名。给客户端分配一个密钥,每次请求接口时,将密钥和所有参数组合成源串,根据签名算法生成签名值,发送请求时将签名一起发送给服务器验证。类似的实现可参考OAuth1.0的签名算法。这样,黑客不知道密钥,不知道签名算法,就算拦截到登录接口,后续请求也无法成功操作。不过,因为签名算法比较麻烦,而且容易出错,只适合对内的接口。如果你们的接口属于开放的API,则不太适合这种签名认证的方式了,建议还是使用OAuth2.0的认证机制。

+

我们也给每个端分配一个appKey,比如Android、iOS、微信三端,每个端分别分配一个appKey和一个密钥。没有传appKey的请求将报错,传错了appKey的请求也将报错。这样,安全性方面又加多了一层防御,同时也方便对不同端做一些不同的处理策略。

+

另外,现在越来越多App取消了密码登录,而采用手机号+短信验证码的登录方式,我在当前的项目中也采用了这种登录方式。这种登录方式有几种好处:

+
    +
  1. 不需要注册,不需要修改密码,也不需要因为忘记密码而重置密码的操作了;
  2. +
  3. 用户不再需要记住密码了,也不怕密码泄露的问题了;
  4. +
  5. 相对于密码登录其安全性明显提高了。
  6. +
+

接口数据的设计

接口的数据一般都采用JSON格式进行传输,不过,需要注意的是,JSON的值只有六种数据类型:

+
    +
  • Number:整数或浮点数
  • +
  • String:字符串
  • +
  • Boolean:true 或 false
  • +
  • Array:数组包含在方括号[]中
  • +
  • Object:对象包含在大括号{}中
  • +
  • Null:空类型
  • +
+

所以,传输的数据类型不能超过这六种数据类型。以前,我们曾经试过传输Date类型,它会转为类似于”2016年1月7日 09时17分42秒 GMT+08:00”这样的字符串,这在转换时会产生问题,不同的解析库解析方式可能不同,有的可能会转乱,有的可能直接异常了。要避免出错,必须做特殊处理,自己手动去做解析。为了根除这种问题,最好的解决方案是用毫秒数表示日期。

+

另外,以前的项目中还出现过字符串的”true”和”false”,或者字符串的数字,甚至还出现过字符串的”null”,导致解析错误,尤其是”null”,导致App奔溃,后来查了好久才查出来是该问题导致的。这都是因为服务端对数据没处理好,导致有些数据转为了字符串。所以,在客户端,也不能完全信任服务端传回的数据都是对的,需要对所有异常情况都做相应处理。

+

服务器返回的数据结构,一般为:

+
1
2
3
4
5
{
code:0
message: "success"
data: { key1: value1, key2: value2, ... }
}
+
    +
  • code: 状态码,0表示成功,非0表示各种不同的错误
  • +
  • message: 描述信息,成功时为”success”,错误时则是错误信息
  • +
  • data: 成功时返回的数据,类型为对象或数组
  • +
+

不同错误需要定义不同的状态码,属于客户端的错误和服务端的错误也要区分,比如1XX表示客户端的错误,2XX表示服务端的错误。这里举几个例子:

+
    +
  • 0:成功
  • +
  • 100:请求错误
  • +
  • 101:缺少appKey
  • +
  • 102:缺少签名
  • +
  • 103:缺少参数
  • +
  • 200:服务器出错
  • +
  • 201:服务不可用
  • +
  • 202:服务器正在重启
  • +
+

错误信息一般有两种用途:一是客户端开发人员调试时看具体是什么错误;二是作为App错误提示直接展示给用户看。主要还是作为App错误提示,直接展示给用户看的。所以,大部分都是简短的提示信息。

+

data字段只在请求成功时才会有数据返回的。数据类型限定为对象或数组,当请求需要的数据为单个对象时则传回对象,当请求需要的数据是列表时,则为某个对象的数组。这里需要注意的就是,不要将data传入字符串或数字,即使请求需要的数据只有一个,比如token,那返回的data应该为:

+
1
2
3
4
5
// 正确
data: { token: 123456 }

// 错误
data: 123456
+

接口版本的设计

接口不可能一成不变,在不停迭代中,总会发生变化。接口的变化一般会有几种:

+
    +
  • 数据的变化,比如增加了旧版本不支持的数据类型
  • +
  • 参数的变化,比如新增了参数
  • +
  • 接口的废弃,不再使用该接口了
  • +
+

为了适应这些变化,必须得做接口版本的设计。实现上,一般有两种做法:

+
    +
  1. 每个接口有各自的版本,一般为接口添加个version的参数。
  2. +
  3. 整个接口系统有统一的版本,一般在URL中添加版本号,比如http://api.domain.com/v2。
  4. +
+

大部分情况下会采用第一种方式,当某一个接口有变动时,在这个接口上叠加版本号,并兼容旧版本。App的新版本开发传参时则将传入新版本的version。

+

如果整个接口系统的根基都发生变动的话,比如微博API,从OAuth1.0升级到OAuth2.0,整个API都进行了升级。

+

有时候,一个接口的变动还会影响到其他接口,但做的时候不一定能发现。因此,最好还要有一套完善的测试机制保证每次接口变更都能测试到所有相关层面。

+ +
+ + + + + + + +
+ + + + + + +
+ +
+ + + +
+ + + + + + + + +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git "a/2016/Counter\347\232\204elements-\346\272\220\347\240\201\351\230\205\350\257\273\347\254\224\350\256\260/index.html" "b/2016/Counter\347\232\204elements-\346\272\220\347\240\201\351\230\205\350\257\273\347\254\224\350\256\260/index.html" new file mode 100644 index 0000000000..995209f83e --- /dev/null +++ "b/2016/Counter\347\232\204elements-\346\272\220\347\240\201\351\230\205\350\257\273\347\254\224\350\256\260/index.html" @@ -0,0 +1,538 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Counter的elements()源码阅读笔记 | 贾攀的流水账 + + + + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

贾攀的流水账

+ +
+

Panmax's Blog

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+ + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ Counter的elements()源码阅读笔记 +

+ + +
+ + + + +
+ + +

Counterelements() 方法返回一个迭代器。元素被重复了多少次,在该迭代器中就包含多少个该元素。所有元素按照字母序排序,个数小于1的元素不被包含。

+

举例:

+
1
2
3
>>> c = Counter('ABCABC')
>>> sorted(c.elements())
['A', 'A', 'B', 'B', 'C', 'C']
+

源码如下:

+
1
2
def elements(self):
return _chain.from_iterable(_starmap(_repeat, self.iteritems()))
+

好!!!精!!!简!!!

+

从里往外看这行代码吧:

+
1
_starmap(_repeat, self.iteritems())
+

_starmapitertools 模块中的一个实现了 __iter__ 方法的类,构造器接收两个参数:一个函数(function)和一个序列(sequence),作用是创建一个迭代器,生成值function(*item),其中item来自sequence,只有当sequence生成的项适用于这种调用函数的方式时,此函数才有效。

+

itertools.starmap(function, iterable) 等价于:

+
1
2
3
def starmap(function, iterable):
for args in iterable:
yield function(*args)
+

举例:

+
1
2
3
4
5
6
7
8
9
10
11
from itertools import starmap

values = [(0, 5), (1, 6), (2, 7), (3, 8), (4, 9)]
for i in starmap(lambda x,y:(x, y, x*y), values):
print '%d * %d = %d' % i

0 * 5 = 0
1 * 6 = 6
2 * 7 = 14
3 * 8 = 24
4 * 9 = 36
+

所以 _starmap(_repeat, self.iteritems()) 等价于下边的代码:

+
1
2
for item in self.iteritems():
yield _repeat(*item)
+

也就是返回一个迭代器,迭代器的每一项是使用item 解包作为参数来调用 _repeat 的结果。

+

下边再来看_repeatitertools.repeat(object[, times]),同样也是实现了__iter__方法的类,作用是创建一个迭代器,重复生成object,times(如果已提供)指定重复计数,如果未提供times,将无止尽返回该对象。

+

等价于:

+
1
2
3
4
5
6
7
def repeat(object, times=None):
if times is None:
while True:
yield object
else:
for i in xrange(times):
yield object
+

举例:

+
1
2
3
4
5
6
7
8
9
10
from itertools import *

for i in repeat('over-and-over', 5):
print i

over-and-over
over-and-over
over-and-over
over-and-over
over-and-over
+

repeat 很容易理解就不用解释了。

+

下边我们返回去看_starmap(_repeat, self.iteritems()), 这些完这些,得到的结果是一个迭代器里边每一项依然是个迭代器,每个内层迭代器迭代出的结果是重复生成的项。

+

可以想象成这样:

+
1
2
3
>>> _starmap(_repeat, [{'A': 2, 'B': 3, 'C': 4}])

['AA', 'BBB', 'CCCC']
+

再来看一下chainitertools.chain(*iterables), 将多个迭代器作为参数, 但只返回单个迭代器, 它产生所有参数迭代器的内容, 就好像他们是来自于一个单一的序列。

+

等价于:

+
1
2
3
4
def chain(*iterables):
for it in iterables:
for element in it:
yield element
+

举例:

+
1
2
3
4
5
6
7
8
for i in chain([1, 2, 3], ['a', 'b', 'c']):
print i
1
2
3
a
b
c
+

chain 的 类函数 from_iterable 可以理解成接收一个参数,然后将这个参数解包后调用构造器。

+

以上例子也可以写成:

+
1
2
3
4
5
6
7
8
for i in chain.from_iterable([[1, 2, 3], ['a', 'b', 'c']]):
print i
1
2
3
a
b
c
+

所以,用 chain 来合并 _starmap(_repeat, self.iteritems()) 得到的嵌套迭代器后得到的就是我们需要的结果了!

+

最后再次感叹下Python代码的精简!

+
+

更正前几篇中的出现过的一个错误:

+

字典调用 iteritems 方法得到的并不是一个列表,而是一个迭代器。

+

之前把 iteritems 一直当成 items 了。

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
>>> x = {'title':'python web site','url':'www.iplaypython.com'}
>>> x.items()

[('url', 'www.iplaypython.com'), ('title', 'python web site')]
>>> a
[('url', 'www.iplaypython.com'), ('title', 'python web site')]
>>> type(a)
<type 'list'>

>>> f = x.iteritems()
>>> f
<dictionary-itemiterator object at 0xb74d5e3c>
>>> type(f)
<type 'dictionary-itemiterator'> #字典项的迭代器
>>> list(f)
[('url', 'www.iplaypython.com'), ('title', 'python web site')]
+
+ + + + + + + +
+ + + + + + +
+ +
+ + + +
+ + + + + + + + +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git "a/2016/Counter\347\232\204most-common-\345\222\214elements-\346\272\220\347\240\201/index.html" "b/2016/Counter\347\232\204most-common-\345\222\214elements-\346\272\220\347\240\201/index.html" new file mode 100644 index 0000000000..df6760aa64 --- /dev/null +++ "b/2016/Counter\347\232\204most-common-\345\222\214elements-\346\272\220\347\240\201/index.html" @@ -0,0 +1,512 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Counter的most_common()源码阅读笔记 | 贾攀的流水账 + + + + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

贾攀的流水账

+ +
+

Panmax's Blog

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+ + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ Counter的most_common()源码阅读笔记 +

+ + +
+ + + + +
+ + +

这个方法可以传一个可选参数 n, 代表获取数量最多的前 n 个元素。如果不传参数,则返回所有结果。

+

反回的结果是一个列表,里边的元素是一个元组,元组第0位是被计数的具体元素,元组第1位是出现的次数。如:[('a', 5), ('b', 4), ('c', 3)],当多个元素计数值相同时,按照字母序排列。

+

下边是 most_common 的源码:

+
1
2
3
4
def most_common(self, n=None):
if n is None:
return sorted(self.iteritems(), key=_itemgetter(1), reverse=True)
return _heapq.nlargest(n, self.iteritems(), key=_itemgetter(1))
+

先来看n是None的情况,因为Counter类继承自dict,所以 self.iteritems 得到的是键值对元组的列表,用 sorted对这个列表进行排序,因为是要按照元组的第1位的数字从大到小的顺序来排序,所以key应该是元组的第1位。代码中用 _itemgetter(1)来取出元组的第1位,_itemgetteroperator 模块里的 itemgetter 类,这个类重写了 __call__ 方法,所以这个类的实例可以当做函数来调用。 具体用法如下:

+
1
2
After f = itemgetter(2), the call f(r) returns r[2].
After g = itemgetter(2, 5, 3), the call g(r) returns (r[2], r[5], r[3])
+

现在 key=itemgetter(1), 即 key(r) 就是 r[1] 这样就可以取到我们想要的那个值了,如果换作之前,我可能会重新定义一个函数,然后赋值给key,最多写一个lambda表达式: lambda x:x[1]赋值给key,这些都是重造轮子的例子。。。不好不好。。。

+

此时我们实际要进行的是整数之间的比较,就不用再给 sortedcmp 参数赋值了,因为我们要得到一个从大到小排列的结果,所以最后 reverse=True

+

如果 n 不为 None ,调用了 heapq(最上边导入时将heapq as 重命名成了 _heapq) 模块中的 nlargest 函数,这个函数的实现有些略微复杂,等以后有时间再去看,直接看下函数的介绍:

+
1
2
Find the n largest elements in a dataset.
Equivalent to: sorted(iterable, key=key, reverse=True)[:n]
+

这个函数的调用结果和用 sorted 排序后再取出前n个结果等价。

+

也就是 sorted(self.iteritems(), key=_itemgetter(1), reverse=True)[:n]

+

下一篇写Counter的elements()方法

+ +
+ + + + + + + +
+ + + + + + +
+ +
+ + + +
+ + + + + + + + +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git "a/2016/GitHub\344\275\277\347\224\250SSH\346\250\241\345\274\217\344\270\215\350\203\275\347\224\250sudo/index.html" "b/2016/GitHub\344\275\277\347\224\250SSH\346\250\241\345\274\217\344\270\215\350\203\275\347\224\250sudo/index.html" new file mode 100644 index 0000000000..7c23237e89 --- /dev/null +++ "b/2016/GitHub\344\275\277\347\224\250SSH\346\250\241\345\274\217\344\270\215\350\203\275\347\224\250sudo/index.html" @@ -0,0 +1,505 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + GitHub使用SSH模式不能用sudo | 贾攀的流水账 + + + + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

贾攀的流水账

+ +
+

Panmax's Blog

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+ + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ GitHub使用SSH模式不能用sudo +

+ + +
+ + + + +
+ + +

今天在AWS上用git时一直报错:

+
1
2
3
4
5
Permission denied (publickey).
fatal: Could not read from remote repository.

Please make sure you have the correct access rights
and the repository exists.
+

因为我在/home/www下进行的操作,这个目录当前用户是没有写权限的,所以需要在操作git时前边加sudo,所以才会导致这个问题。

+

官方说明:

+

You 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。

+ +
+ + + + + + + +
+ + + + + + +
+ +
+ + + +
+ + + + + + + + +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git "a/2016/JavaScript-\346\237\245\346\274\217\350\241\245\347\274\272/index.html" "b/2016/JavaScript-\346\237\245\346\274\217\350\241\245\347\274\272/index.html" new file mode 100644 index 0000000000..20b8330a09 --- /dev/null +++ "b/2016/JavaScript-\346\237\245\346\274\217\350\241\245\347\274\272/index.html" @@ -0,0 +1,551 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + JavaScript 查漏补缺 | 贾攀的流水账 + + + + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

贾攀的流水账

+ +
+

Panmax's Blog

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+ + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ JavaScript 查漏补缺 +

+ + +
+ + + + +
+ + +

实际上,JavaScript允许对任意数据类型做比较:

+
1
2
false == 0; // true
false === 0; // false
+

要特别注意相等运算符==。JavaScript在设计时,有两种比较运算符:

+

第一种是==比较,它会自动转换数据类型再比较,很多时候,会得到非常诡异的结果;

+

第二种是===比较,它不会自动转换数据类型,如果数据类型不一致,返回false,如果一致,再比较。

+

由于JavaScript这个设计缺陷,不要使用==比较,始终坚持使用===比较。

+

另一个例外是NaN这个特殊的Number与所有其他值都不相等,包括它自己:

+
1
NaN === NaN; // false
+

唯一能判断NaN的方法是通过isNaN()函数:

+
1
isNaN(NaN); // true
+
+

在其他语言中,也有类似JavaScript的null的表示,例如Java也用null,Swift用nil,Python用None表示。但是,在JavaScript中,还有一个和null类似的undefined,它表示“未定义”。

+

JavaScript的设计者希望用null表示一个空的值,而undefined表示值未定义。事实证明,这并没有什么卵用,区分两者的意义不大。大多数情况下,我们都应该用nullundefined仅仅在判断函数参数是否传递的情况下有用。

+
+

strict模式

+

JavaScript在设计之初,为了方便初学者学习,并不强制要求用var申明变量。这个设计错误带来了严重的后果:如果一个变量没有通过var申明就被使用,那么该变量就自动被申明为全局变量:

+
1
i = 10; // i现在是全局变量
+

在同一个页面的不同的JavaScript文件中,如果都不用var申明,恰好都使用了变量i,将造成变量i互相影响,产生难以调试的错误结果。

+

使用var申明的变量则不是全局变量,它的范围被限制在该变量被申明的函数体内(函数的概念将稍后讲解),同名变量在不同的函数体内互不冲突。

+

为了修补JavaScript这一严重设计缺陷,ECMA在后续规范中推出了strict模式,在strict模式下运行的JavaScript代码,强制通过var申明变量,未使用var申明变量就使用的,将导致运行错误。

+

启用strict模式的方法是在JavaScript代码的第一行写上:

+
1
'use strict';
+

这是一个字符串,不支持strict模式的浏览器会把它当做一个字符串语句执行,支持strict模式的浏览器将开启strict模式运行JavaScript。

+ +
+

unshiftshift

+

如果要往Array的头部添加若干元素,使用unshift()方法,shift()方法则把Array的第一个元素删掉:

+
1
2
3
4
5
6
7
8
9
var arr = [1, 2];
arr.unshift('A', 'B'); // 返回Array新的长度: 4
arr; // ['A', 'B', 1, 2]
arr.shift(); // 'A'
arr; // ['B', 1, 2]
arr.shift(); arr.shift(); arr.shift(); // 连续shift 3次
arr; // []
arr.shift(); // 空数组继续shift不会报错,而是返回undefined
arr; // []
+
+

splice()方法是修改Array的“万能方法”,它可以从指定的索引开始删除若干元素,然后再从该位置添加若干元素:

+
1
2
3
4
5
6
7
8
9
10
var arr = ['Microsoft', 'Apple', 'Yahoo', 'AOL', 'Excite', 'Oracle'];
// 从索引2开始删除3个元素,然后再添加两个元素:
arr.splice(2, 3, 'Google', 'Facebook'); // 返回删除的元素 ['Yahoo', 'AOL', 'Excite']
arr; // ['Microsoft', 'Apple', 'Google', 'Facebook', 'Oracle']
// 只删除,不添加:
arr.splice(2, 2); // ['Google', 'Facebook']
arr; // ['Microsoft', 'Apple', 'Oracle']
// 只添加,不删除:
arr.splice(2, 0, 'Google', 'Facebook'); // 返回[],因为没有删除任何元素
arr; // ['Microsoft', 'Apple', 'Google', 'Facebook', 'Oracle']
+
+

由于多行字符串用\n写起来比较费事,所以最新的ES6标准新增了一种多行字符串的表示方法,用...表示:

+
1
2
3
`这是一个
多行
字符串`;
+
+

for … in

+

for循环的一个变体是for ... in循环,它可以把一个对象的所有属性依次循环出来:

+
1
2
3
4
5
6
7
8
var o = {
name: 'Jack',
age: 20,
city: 'Beijing'
};
for (var key in o) {
alert(key); // 'name', 'age', 'city'
}
+

要过滤掉对象继承的属性,用hasOwnProperty()来实现:

+
1
2
3
4
5
6
7
8
9
10
var o = {
name: 'Jack',
age: 20,
city: 'Beijing'
};
for (var key in o) {
if (o.hasOwnProperty(key)) {
alert(key); // 'name', 'age', 'city'
}
}
+

由于Array也是对象,而它的每个元素的索引被视为对象的属性,因此,for ... in循环可以直接循环出Array的索引:

+
1
2
3
4
5
var a = ['A', 'B', 'C'];
for (var i in a) {
alert(i); // '0', '1', '2'
alert(a[i]); // 'A', 'B', 'C'
}
+

请注意,for ... inArray的循环得到的是String而不是Number

+
+

iterable内置的forEach方法,它接收一个函数,每次迭代就自动回调该函数。以Array为例:

+
1
2
3
4
5
6
7
var a = ['A', 'B', 'C'];
a.forEach(function (element, index, array) {
// element: 指向当前元素的值
// index: 指向当前索引
// array: 指向Array对象本身
alert(element);
});
+

SetArray类似,但Set没有索引,因此回调函数的前两个参数都是元素本身:

+
1
2
3
4
var s = new Set(['A', 'B', 'C']);
s.forEach(function (element, sameElement, set) {
alert(element);
});
+

Map的回调函数参数依次为valuekeymap本身:

+
1
2
3
4
var m = new Map([[1, 'x'], [2, 'y'], [3, 'z']]);
m.forEach(function (value, key, map) {
alert(value);
});
+

如果对某些参数不感兴趣,由于JavaScript的函数调用不要求参数必须一致,因此可以忽略它们。例如,只需要获得Arrayelement

+
1
2
3
4
var a = ['A', 'B', 'C'];
a.forEach(function (element) {
alert(element);
});
+

待续

+
+ + + + + + + +
+ + + + + + +
+ +
+ + + +
+ + + + + + + + +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git "a/2016/Mac\344\270\213MySQL\345\256\211\350\243\205\344\270\216\345\215\270\350\275\275/index.html" "b/2016/Mac\344\270\213MySQL\345\256\211\350\243\205\344\270\216\345\215\270\350\275\275/index.html" new file mode 100644 index 0000000000..7a5953e5c7 --- /dev/null +++ "b/2016/Mac\344\270\213MySQL\345\256\211\350\243\205\344\270\216\345\215\270\350\275\275/index.html" @@ -0,0 +1,504 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Mac下MySQL安装与卸载 | 贾攀的流水账 + + + + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

贾攀的流水账

+ +
+

Panmax's Blog

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+ + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ Mac下MySQL安装与卸载 +

+ + +
+ + + + +
+ + +

写这篇博客的原因是,我刚用 Mac 的时候 MySQL 是在官网下载的安装包进行的安装,那时候不知道有 Homebrew 这个神器。用官方的包安装好后又做何很多的配置最终才能正常使用。但还是有不少问题,比如无法在终端启动MySQL,只能在系统偏好里,通过官方的启动器启动。

+

后来有很长一段时间没有用过MySQL,今天再用的时候发现无法启动了,于是就把之前的进行了卸载,重新用Homebrew安装了一遍。

+

卸载过程:

1
2
3
4
5
6
7
8
9
sudo rm /usr/local/mysql
sudo rm -rf /usr/local/mysql*
sudo rm -rf /Library/StartupItems/MySQLCOM
sudo rm -rf /Library/PreferencePanes/My*
sudo vim /etc/hostconfig 按a进入编辑模式 然后手动删除 MYSQLCOM=-YES- 然后按Esc退出编辑模式 输入:wq!保存并退出
rm -rf ~/Library/PreferencePanes/My*
sudo rm -rf /Library/Receipts/mysql*
sudo rm -rf /Library/Receipts/MySQL*
sudo rm -rf /var/db/receipts/com.mysql.*
+

安装过程:

1
2
brew install mysql
export PATH=$PATH:/usr/local/mysql/bin
+

启动:

1
2
export PATH=$PATH:/usr/local/mysql/bin
mysql.server start
+

首次登录:

1
mysql -uroot
+
+ + + + + + + +
+ + + + + + +
+ +
+ + + +
+ + + + + + + + +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git "a/2016/Mac\345\256\211\350\243\205MySqldb\345\222\214pylibmc\346\227\266\351\201\207\345\210\260\347\232\204\351\227\256\351\242\230/index.html" "b/2016/Mac\345\256\211\350\243\205MySqldb\345\222\214pylibmc\346\227\266\351\201\207\345\210\260\347\232\204\351\227\256\351\242\230/index.html" new file mode 100644 index 0000000000..22c530a27a --- /dev/null +++ "b/2016/Mac\345\256\211\350\243\205MySqldb\345\222\214pylibmc\346\227\266\351\201\207\345\210\260\347\232\204\351\227\256\351\242\230/index.html" @@ -0,0 +1,511 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Mac安装MySqldb和pylibmc时遇到的问题 | 贾攀的流水账 + + + + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

贾攀的流水账

+ +
+

Panmax's Blog

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+ + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ Mac安装MySqldb和pylibmc时遇到的问题 +

+ + +
+ + + + +
+ + +

今天在 Mac 上安装 MySqldb 和 pylibmc 的时候遇到两个问题,在这里记录一下。

+

安装 MySqldb 时,报:

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
Failed building wheel for MySQL-python
Running setup.py clean for MySQL-python
Failed to build MySQL-python
Installing collected packages: MySQL-python
Running setup.py install for MySQL-python ... error
Complete output from command /Users/jiapan/PycharmProjects/hodoor/venv/bin/python -u -c "import setuptools, tokenize;__file__='/private/var/folders/9j/c68zmjy53fq4t0j5y82wphr40000gn/T/pip-build-HrfNXN/MySQL-python/setup.py';exec(compile(getattr(tokenize, 'open', open)(__file__).read().replace('\r\n', '\n'), __file__, 'exec'))" install --record /var/folders/9j/c68zmjy53fq4t0j5y82wphr40000gn/T/pip-DdwCnb-record/install-record.txt --single-version-externally-managed --compile --install-headers /Users/jiapan/PycharmProjects/hodoor/venv/bin/../include/site/python2.7/MySQL-python:
running install
running build
running build_py
creating build
creating build/lib.macosx-10.11-x86_64-2.7
copying _mysql_exceptions.py -> build/lib.macosx-10.11-x86_64-2.7
creating build/lib.macosx-10.11-x86_64-2.7/MySQLdb
copying MySQLdb/__init__.py -> build/lib.macosx-10.11-x86_64-2.7/MySQLdb
copying MySQLdb/converters.py -> build/lib.macosx-10.11-x86_64-2.7/MySQLdb
copying MySQLdb/connections.py -> build/lib.macosx-10.11-x86_64-2.7/MySQLdb
copying MySQLdb/cursors.py -> build/lib.macosx-10.11-x86_64-2.7/MySQLdb
copying MySQLdb/release.py -> build/lib.macosx-10.11-x86_64-2.7/MySQLdb
copying MySQLdb/times.py -> build/lib.macosx-10.11-x86_64-2.7/MySQLdb
creating build/lib.macosx-10.11-x86_64-2.7/MySQLdb/constants
copying MySQLdb/constants/__init__.py -> build/lib.macosx-10.11-x86_64-2.7/MySQLdb/constants
copying MySQLdb/constants/CR.py -> build/lib.macosx-10.11-x86_64-2.7/MySQLdb/constants
copying MySQLdb/constants/FIELD_TYPE.py -> build/lib.macosx-10.11-x86_64-2.7/MySQLdb/constants
copying MySQLdb/constants/ER.py -> build/lib.macosx-10.11-x86_64-2.7/MySQLdb/constants
copying MySQLdb/constants/FLAG.py -> build/lib.macosx-10.11-x86_64-2.7/MySQLdb/constants
copying MySQLdb/constants/REFRESH.py -> build/lib.macosx-10.11-x86_64-2.7/MySQLdb/constants
copying MySQLdb/constants/CLIENT.py -> build/lib.macosx-10.11-x86_64-2.7/MySQLdb/constants
running build_ext
building '_mysql' extension
creating build/temp.macosx-10.11-x86_64-2.7
clang -fno-strict-aliasing -fno-common -dynamic -g -O2 -DNDEBUG -g -fwrapv -O3 -Wall -Wstrict-prototypes -Dversion_info=(1,2,5,'final',1) -D__version__=1.2.5 -I/usr/local/Cellar/mysql/5.7.12/include/mysql -I/usr/local/Cellar/python/2.7.11/Frameworks/Python.framework/Versions/2.7/include/python2.7 -c _mysql.c -o build/temp.macosx-10.11-x86_64-2.7/_mysql.o -fno-omit-frame-pointer
_mysql.c:1589:10: warning: comparison of unsigned expression < 0 is always false [-Wtautological-compare]
if (how < 0 || how >= sizeof(row_converters)) {
~~~ ^ ~
1 warning generated.
clang -bundle -undefined dynamic_lookup build/temp.macosx-10.11-x86_64-2.7/_mysql.o -L/usr/local/Cellar/mysql/5.7.12/lib -lmysqlclient -lssl -lcrypto -o build/lib.macosx-10.11-x86_64-2.7/_mysql.so
ld: library not found for -lssl
clang: error: linker command failed with exit code 1 (use -v to see invocation)
error: command 'clang' failed with exit status 1
+

产生原因,升级了Xcode,但是Xcode还没有启动过,里边的一些插件还没有升级。

+

解决方法,启动Xcode,让Xcode完成升级。

+

然后执行 xcode-select --install 等待完成后即可。

+

UPDATE AT: 2018-05-02

+

今天解决时问题时,上边的方法也不起作用了,stackoverflow 找到了一个方法:

+

sudo env LDFLAGS="-I/usr/local/opt/openssl/include -L/usr/local/opt/openssl/lib" pip install MySQL-python

+

第二个问题是再安装 pylibmc 的时候报

+
1
2
3
4
5
6
7
8
9
./_pylibmcmodule.h:42:10: fatal error: 'libmemcached/memcached.h' file not found

#include <libmemcached/memcached.h>

^

1 error generated.

error: command 'clang' failed with exit status 1
+

产生原因,Mac 使用 brew 安装的 memcached 的路径和 pylibmc 认为的默认路径不一致,所以需要指定一下路径。

+
1
2
3
4
5
$ which memcached
/usr/local/bin/memcached

$ export LIBMEMCACHED=/usr/local
$ pip install pylibmc
+ +
+ + + + + + + +
+ + + + + + +
+ +
+ + + +
+ + + + + + + + +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git "a/2016/MySQL\345\210\233\345\273\272utf-8\346\240\274\345\274\217\346\225\260\346\215\256\345\272\223/index.html" "b/2016/MySQL\345\210\233\345\273\272utf-8\346\240\274\345\274\217\346\225\260\346\215\256\345\272\223/index.html" new file mode 100644 index 0000000000..0591f80415 --- /dev/null +++ "b/2016/MySQL\345\210\233\345\273\272utf-8\346\240\274\345\274\217\346\225\260\346\215\256\345\272\223/index.html" @@ -0,0 +1,500 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + MySQL创建utf-8格式数据库 | 贾攀的流水账 + + + + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

贾攀的流水账

+ +
+

Panmax's Blog

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+ + +
+
+
+ + +
+ + + + + + + + + + + + +
+ +
+ + + +
+ + + + + + + + +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git "a/2016/Python-kill\347\274\226\347\240\201\351\227\256\351\242\230/index.html" "b/2016/Python-kill\347\274\226\347\240\201\351\227\256\351\242\230/index.html" new file mode 100644 index 0000000000..d9018740f2 --- /dev/null +++ "b/2016/Python-kill\347\274\226\347\240\201\351\227\256\351\242\230/index.html" @@ -0,0 +1,524 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Python kill编码问题 | 贾攀的流水账 + + + + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

贾攀的流水账

+ +
+

Panmax's Blog

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+ + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ Python kill编码问题 +

+ + +
+ + + + +
+ + +
+

之前在遇到字符串编码问题的时候都是跑到网上现去查资料,而且一直分不清 decodeencode 到底那个是解码,那个是编码,每次用的时候还要查一下文档,本次就做一个了断吧!

+
+

Python 内部字符串编码为 unicode,因此在编码转换时,通常使用 unicode 作为中间编码, 先将其他编码的字符串解码(decode)成 unicode,再从 unicode 编码(encode)成另一种编码。

+

unicode

世界上存在着多种编码方式,同一个二进制数字可以被解释成不同的符号。因此,要想打开一个文本文件,就必须知道它的编码方式,否则用错误的编码方式解读,就会出现乱码。为什么电子邮件常常出现乱码?就是因为发信人和收信人使用的编码方式不一样。

+

可以想象,如果有一种编码,将世界上所有的符号都纳入其中。每一个符号都给予一个独一无二的编码,那么乱码问题就会消失。这就是unicode,就像它的名字都表示的,这是一种所有符号的编码。

+

decode

decode 的作用是将其他编码的字符串解码成 unicode 编码。

+

str.decode('gbk'),表示将 gbk 编码的字符串解码成 unicode 编码。

+

encode

encode 的作用是将 unicode 字符串编码成其他编码的字符串。

+

str.encode('gbk'), 表示将 unicode 编码的字符串编码成 gbk 编码。

+
+

因此转码的时候一定先搞明白,字符串是什么编码,然后 decodeunicode,再然后 encode 成其他编码。

+
+

代码中字符串的编码与代码文件的编码一致

如果这样写 s = u'中国' ,该字符串的编码就被指定为了 unicode 了,即 Python 的内部编码,而与代码文件本身编码无关,因此对于这种情况做编码转换,只需直接使用 encode 方法将其转换成指定编码即刻。

+

如果对一个 unicode 字符串进行解码将会报错,所以可以使用 isinstance(s, unicode) 来判断是否为 unicode

+

unicode 编码的字符串使用 encode 会报错。

+

unicode(str, 'gbk')str.decode('gbk') 是一样的,都是将 gbk 编码的字符串转为 unicode 编码。

+
+

唉,想起来之前有个非常耻辱的事,就是有次面试的时候对方问我 Python 中默认字符编码是什么,我回答的是: ascii

+

还问我Python中如何进行编码和解码,其实我知道是用 decodeencode 但当时真的分不清楚哪个是做什么用的,所以忘了当时回答的对不对了。

+

为了让自己把 decodeencode 分清楚,我想了个办法, decode 开头发音是 弟(di),所以弟就应该有个姐姐,所以 decode 就是解码,另外一个 encode 就自然是编码了。哈哈哈。

+

对方还问了我,utf8unicode 有什么区别,后来查了下资料,结论是: UTF-8是Unicode的实现方式之一。 详情见阮一峰老师的博客

+ +
+ + + + + + + +
+ + + + + + +
+ +
+ + + +
+ + + + + + + + +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git "a/2016/Python-\346\237\245\346\274\217\350\241\245\347\274\272/index.html" "b/2016/Python-\346\237\245\346\274\217\350\241\245\347\274\272/index.html" new file mode 100644 index 0000000000..9aff78431d --- /dev/null +++ "b/2016/Python-\346\237\245\346\274\217\350\241\245\347\274\272/index.html" @@ -0,0 +1,527 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Python 查漏补缺 | 贾攀的流水账 + + + + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

贾攀的流水账

+ +
+

Panmax's Blog

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+ + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ Python 查漏补缺 +

+ + +
+ + + + +
+ + +

在Python中当函数被定义时,默认参数只会运算一次,而不是每次被调用时都会重新运算。

+
1
2
3
4
5
6
7
8
9
10
11
12
def add_to(num, target=[]):
target.append(num)
return target

add_to(1)
# Output: [1]

add_to(2)
# Output: [1, 2]

add_to(3)
# Output: [1, 2, 3]
+

你应该永远不要定义可变类型的默认参数,除非你知道你正在做什么。你应该像这样做:

+
1
2
3
4
5
def add_to(element, target=None):
if target is None:
target = []
target.append(element)
return target
+
+

另一种三元运算符:

+
1
2
#(返回假,返回真)[真或假]
(if_test_is_false, if_test_is_true)[test]
+

例子:

+
1
2
3
4
fat = True
fitness = ("skinny", "fat")[fat]
print("Ali is ", fitness)
#输出: Ali is fat
+

这之所以能正常工作,是因为在Python中,True等于1,而False等于0,这就相当于在元组中使用0和1来选取数据。

+

上面的例子没有被广泛使用,而且Python玩家一般不喜欢那样,因为没有Python味儿(Pythonic)。这样的用法很容易把真正的数据与true/false弄混。

+

另外一个不使用元组条件表达式的缘故是因为在元组中会把两个条件都执行,而 if-else 的条件表达式不会这样。

+

例如:

+
1
2
3
4
5
6
condition = True
print(2 if condition else 1/0)
#输出: 2

print((1/0, 2)[condition])
#输出ZeroDivisionError异常
+

这是因为在元组中是先建数据,然后用True(1)/False(0)来索引到数据。 而if-else条件表达式遵循普通的if-else逻辑树, 因此,如果逻辑中的条件异常,或者是重计算型(计算较久)的情况下,最好尽量避免使用元组条件表达式。

+
+ +

当你在一个字典中对一个键进行嵌套赋值时,如果这个键不存在,会触发keyError异常。 defaultdict允许我们用一个聪明的方式绕过这个问题。 首先我分享一个使用dict触发KeyError的例子,然后提供一个使用defaultdict的解决方案。

+

问题:

+
1
2
3
4
some_dict = {}
some_dict['colours']['favourite'] = "yellow"

## 异常输出:KeyError: 'colours'
+

解决方案:

+
1
2
3
4
5
6
import collections
tree = lambda: collections.defaultdict(tree)
some_dict = tree()
some_dict['colours']['favourite'] = "yellow"

## 运行正常
+

你可以用json.dumps打印出some_dict,例如:

+
1
2
3
4
import json
print(json.dumps(some_dict))

## 输出: {"colours": {"favourite": "yellow"}}
+
+

列表辗平

+

您可以通过使用itertools包中的itertools.chain.from_iterable轻松快速的辗平一个列表。下面是一个简单的例子:

+
1
2
3
4
5
6
7
a_list = [[1, 2], [3, 4], [5, 6]]
print(list(itertools.chain.from_iterable(a_list)))
# Output: [1, 2, 3, 4, 5, 6]

# or
print(list(itertools.chain(*a_list)))
# Output: [1, 2, 3, 4, 5, 6]
+

待续

+
+ + + + + + + +
+ + + + + + +
+ +
+ + + +
+ + + + + + + + +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git "a/2016/Python\344\270\255\345\220\210\345\271\266\344\270\244\344\270\252\345\255\227\345\205\270/index.html" "b/2016/Python\344\270\255\345\220\210\345\271\266\344\270\244\344\270\252\345\255\227\345\205\270/index.html" new file mode 100644 index 0000000000..27a1d203e7 --- /dev/null +++ "b/2016/Python\344\270\255\345\220\210\345\271\266\344\270\244\344\270\252\345\255\227\345\205\270/index.html" @@ -0,0 +1,518 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Python中合并两个字典 | 贾攀的流水账 + + + + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

贾攀的流水账

+ +
+

Panmax's Blog

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+ + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ Python中合并两个字典 +

+ + +
+ + + + +
+ + +

有两个字典:

+
1
2
3
user = {'name': "Trey", 'website': "http://treyhunner.com"}

defaults = {'name': "Anonymous User", 'page_name': "Profile Page"}
+

现在想合并两个字典,得到一个新的字典,要求:

+
    +
  1. 如果存在重复的键,user字典中的值应覆盖defaults字典中的值;
  2. +
  3. defaults和user中的键可以是任意合法的键;
  4. +
  5. defaults和user中的值可以是任意值;
  6. +
  7. 在创建context字典时,defaults和user的元素不能出现变化;
  8. +
  9. 更新context字典时,不能更改defaults或user字典。
  10. +
+

以上两个字典合并结果为:

+
1
{'website': 'http://treyhunner.com', 'name': 'Trey', 'page_name': 'Profile Page'}
+

Python 3 中最优雅的实现方法:

+
1
context = {**defaults, **user}
+
+

Python 2 中:

+
多次更新
1
2
3
context = {}
context.update(defaults)
context.update(user)
+

这里我们创建了一个新的空字典,并使用其update方法从其他字典中添加元素。请注意,我们首先添加的是defaults字典中的元素,以保证user字典中的重复键会覆盖掉defaults中的键。

+
复制,然后更新
1
2
context = defaults.copy()
context.update(user)
+
ChainMap转换成字典
1
context = dict(ChainMap(user, defaults))
+
+ + + + + + + +
+ + + + + + +
+ +
+ + + +
+ + + + + + + + +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git "a/2016/Python\345\217\252\350\203\275\344\273\245\345\205\263\351\224\256\345\255\227\345\275\242\345\274\217\346\214\207\345\256\232\345\217\202\346\225\260/index.html" "b/2016/Python\345\217\252\350\203\275\344\273\245\345\205\263\351\224\256\345\255\227\345\275\242\345\274\217\346\214\207\345\256\232\345\217\202\346\225\260/index.html" new file mode 100644 index 0000000000..0099c3ba68 --- /dev/null +++ "b/2016/Python\345\217\252\350\203\275\344\273\245\345\205\263\351\224\256\345\255\227\345\275\242\345\274\217\346\214\207\345\256\232\345\217\202\346\225\260/index.html" @@ -0,0 +1,512 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Python只能以关键字形式指定参数 | 贾攀的流水账 + + + + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

贾攀的流水账

+ +
+

Panmax's Blog

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+ + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ Python只能以关键字形式指定参数 +

+ + +
+ + + + +
+ + +

Python 3 可以声明只能通过关键字来作为参数的函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
def safe_division(number, divisor, *, ignore_overflow=False, ingore_zero_division=False):  
try:
return number / divisor
except OverflowError:
if ignore_overflow:
return 0
else:
raise
except ZeroDivisionError:
if ingore_zero_division:
return float('int')
else:
raise
+

参数列表里的 * 号,标志着位置参数结束,之后的参数都只能以关键字形式来指定。

+

save_division(10, 0, False, True) # error

+

save_division(10, 0, ignore_zero_division=True) # ok

+
+

Python 2 中实现以关键字来指定的参数:

Python 2 并没有明确的语法来定义这种只能以关键字形式指定的参数。不过我们可以在参数列表中使用 ** 操作符,并且领函数遇到无效的调用时抛出TypeErrors,这样就可以实现与Python 3 相同的功能了。

+

为了使Python 2 版本的safe_division函数具备只能以关键字形式来指定的参数,我们可以先令该函数接受 **kwargs 参数,然后用 pop 方法把期望的关键字从 kwargs 字典里取走,如果字典的键里面没有那个关键字,那么 pop 方法的第二个参数就会成为默认值。最后为了防止调用者提供无效参数值,我们需要确认 kwargs 字典里面已经没有关键字参数了。

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# Python 2
def safe_division(number, divisor, **kwargs):
ignore_overflow = kwargs.pop('ignore_overflow', False)
ingore_zero_division = kwargs.pop('ingore_zero_division', False)
if kwargs:
raise TypeError('Unexpected **kwargs: %r' % kwargs)
try:
return number / divisor
except OverflowError:
if ignore_overflow:
return 0
else:
raise
except ZeroDivisionError:
if ingore_zero_division:
return float('int')
else:
raise
+

safe_division(1, 0, False, False) # error

+

safe_division(0, 0, unexpected=True) # error

+

save_division(10, 0, ignore_zero_division=True) # ok

+ +
+ + + + + + + +
+ + + + + + +
+ +
+ + + +
+ + + + + + + + +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git "a/2016/Python\346\234\200\344\275\263\345\256\236\350\267\265\346\214\207\345\215\227-\351\230\205\350\257\273\347\254\224\350\256\260/index.html" "b/2016/Python\346\234\200\344\275\263\345\256\236\350\267\265\346\214\207\345\215\227-\351\230\205\350\257\273\347\254\224\350\256\260/index.html" new file mode 100644 index 0000000000..01880554d2 --- /dev/null +++ "b/2016/Python\346\234\200\344\275\263\345\256\236\350\267\265\346\214\207\345\215\227-\351\230\205\350\257\273\347\254\224\350\256\260/index.html" @@ -0,0 +1,564 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Python最佳实践指南 阅读笔记 | 贾攀的流水账 + + + + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

贾攀的流水账

+ +
+

Panmax's Blog

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+ + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ Python最佳实践指南 阅读笔记 +

+ + +
+ + + + +
+ + +

创建将0到19连接起来的字符串

1
2
3
4
5
6
7
8
nums = []
for n in range(20):
nums.append(str(n))
print "".join(nums)

# 更好的写法
nums = [str(n) for n in range(20)]
print "".join(nums)
+

拼接多个已有的字符串

1
2
3
4
5
6
7
foo = 'foo'
bar = 'bar'

foobar = foo + bar # 好的做法

foo += 'ooo' # 不好的做法, 应该这么做:
foo = ''.join([foo, 'ooo'])
+

也可以使用 % 格式运算符来连接确定数量的字符串,但 PEP 3101 建议使用 str.format() 替代 % 操作符。

1
2
3
4
5
6
foo = 'foo'
bar = 'bar'

foobar = '%s%s' % (foo, bar) # 可行
foobar = '{0}{1}'.format(foo, bar) # 更好
foobar = '{foo}{bar}'.format(foo=foo, bar=bar) # 最好
+

不要重复使用命名

1
2
3
items = 'a b c d'  # 首先指向字符串...
items = items.split(' ') # ...变为列表
items = set(items) # ...再变为集合
+
+

重复使用命名对效率并没有提升:赋值时无论如何都要创建新的对象。然而随着复杂度的 提升,赋值语句被其他代码包括 ‘if’ 分支和循环分开,使得更难查明指定变量的类型。 在某些代码的做法中,例如函数编程,推荐的是从不重复对同一个变量命名赋值。Java 内的实现方式是使用 ‘final’ 关键字。Python并没有 ‘final’ 关键字而且这与它的哲学 相悖。尽管如此,避免给同一个变量命名重复赋值仍是是个好的做法,并且有助于掌握 可变与不可变类型的概念。

+
+

考虑该不该用任意参数列表(*args)

+

如果一个函数接受的参数列表具有 相同的性质,通常把它定义成一个参数,这个参数是一个列表或者其他任何序列会更清晰。

+
+

函数单个出口可能更好

+

当一个函数在其正常过程中有多个主要出口点时,它会变得难以调试和返回其 结果,所以保持单个出口点可能会更好。这也将有助于提取某些代码路径,而且多个出口点 很有可能意味着这里需要重构。

+
+
1
2
3
4
5
6
7
8
9
10
11
def complex_function(a, b, c):
if not a:
return None # 抛出一个异常可能会更好
if not b:
return None # 抛出一个异常可能会更好

# 一些复杂的代码试着用a,b,c来计算x
# 如果成功了,抵制住返回x的诱惑
if not x:
# 一些关于x的计算的Plan-B
return x
+

常见Python习语

    +
  • 解包
  • +
+
1
2
3
4
5
6
for index, item in enumerate(some_list):
# 使用index和item做一些工作

a, b = b, a

a, (b, c) = 1, (2, 3)
+
    +
  • 创建一个被忽略的变量
  • +
+
1
2
filename = 'foobar.txt'
basename, __, ext = filename.rpartition('.')
+
    +
  • 创建一个含N个对象的列表
  • +
+
1
four_nones = [None] * 4
+
    +
  • 创建一个含N个列表的列表
  • +
+
1
four_lists = [[] for __ in xrange(4)]
+
    +
  • 根据列表来创建字符串
  • +
+
1
2
letters = ['s', 'p', 'a', 'm']
word = ''.join(letters)
+
    +
  • 在集合体(collection)中查找一个项
  • +
+
1
2
3
4
5
6
7
8
s = set(['s', 'p', 'a', 'm'])
l = ['s', 'p', 'a', 'm']

def lookup_set(s):
return 's' in s

def lookup_list(l):
return 's' in l
+

在下列场合在使用集合或者字典而不是列表,通常会是个好主意:

+

集合体中包含大量的项
你将在集合体中重复地查找项
你没有重复的项

+

你不需要明确地比较一个值是True,或者None,或者0

糟糕

1
2
3
4
5
if attr == True:
print 'True!'

if attr == None:
print 'attr is None!'
+

优雅

1
2
3
4
5
6
7
8
9
10
11
# 检查值
if attr:
print 'attr is truthy!'

# 或者做相反的检查
if not attr:
print 'attr is falsey!'

# or, since None is considered false, explicitly check for it
if attr is None:
print 'attr is None!'
+

访问字典元素

糟糕

1
2
3
4
5
d = {'hello': 'world'}
if d.has_key('hello'):
print d['hello'] # 打印 'world'
else:
print 'default_value'
+

优雅

1
2
3
4
5
6
7
8
d = {'hello': 'world'}

print d.get('hello', 'default_value') # 打印 'world'
print d.get('thingy', 'default_value') # 打印 'default_value'

# Or:
if 'hello' in d:
print d['hello']
+

在每次函数调用中,通过使用指示没有提供参数的默认参数 None 通常是 个好选择),来创建一个新的对象。

举例:

1
2
3
def append_to(element, to=[]):
to.append(element)
return to
+

你可能认为

+
1
2
3
4
5
my_list = append_to(12)
print my_list # [12]

my_other_list = append_to(42)
print my_other_list # [42]
+

实际结果为

+
1
2
3
# [12]

# [12, 42]
+

当函数被定义时,一个新的列表就被创建一次 ,而且同一个列表在每次成功的调用中都被使用。

+

当函数被定义时,Python的默认参数就被创建 一次,而不是每次调用函数的时候创建。 这意味着,如果你使用一个可变默认参数并改变了它,你 将会 在未来所有对此函数的 调用中改变这个对象。

+

迟绑定闭包

举例

1
2
3
4
5
def create_multipliers():
return [lambda x : i * x for i in range(5)]

for multiplier in create_multipliers():
print multiplier(2)
+

你期望的结果

+
1
2
3
4
5
0
2
4
6
8
+

实际结果

+
1
2
3
4
5
8
8
8
8
8
+

五个函数被创建了,它们全都用4乘以 x 。

+

Python的闭包是 迟绑定 。 这意味着闭包中用到的变量的值,是在内部函数被调用时查询得到的。

+

这里,不论 任何 返回的函数是如何被调用的, i 的值是调用时在周围作用域中查询到的。 接着,循环完成, i 的值最终变成了4。

+

这个陷阱并不和 lambda 有关,不通定义也会这样

1
2
3
4
5
6
7
8
9
def create_multipliers():
multipliers = []

for i in range(5):
def multiplier(x):
return i * x
multipliers.append(multiplier)

return multipliers
+

解决方案

最一般的解决方案可以说是有点取巧(hack)。由于 Python 拥有为函数默认参数 赋值的行为,你可以创建一个立即绑定参数的闭包,像下面这样:

+
1
2
def create_multipliers():
return [lambda x, i=i : i * x for i in range(5)]
+

或者,可以使用 function.partial 函数

+
1
2
3
4
5
from functools import partial
from operator import mul

def create_multipliers():
return [partial(mul, i) for i in range(5)]
+ +
+ + + + + + + +
+ + + + + + +
+ +
+ + + +
+ + + + + + + + +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2016/WakaTime/1.png b/2016/WakaTime/1.png new file mode 100644 index 0000000000..c9c008612a Binary files /dev/null and b/2016/WakaTime/1.png differ diff --git a/2016/WakaTime/index.html b/2016/WakaTime/index.html new file mode 100644 index 0000000000..cd11025bac --- /dev/null +++ b/2016/WakaTime/index.html @@ -0,0 +1,507 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + WakaTime | 贾攀的流水账 + + + + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

贾攀的流水账

+ +
+

Panmax's Blog

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+ + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ WakaTime +

+ + +
+ + + + +
+ + +

今天没啥好写的,记一个我刚刚发现的新东西吧。

+

WakaTime

网站宣传语是:Quantify your coding - Metrics, insights, and time tracking automatically generated from your programming activity

+

用来量化工作量,我用的PyCharm,只要安装官方提供的插件后,就能统计我当天为每个项目敲代码的时间,而且能统计我都谢了那些代码,比如JS占10%,Python占90%

+

看一眼我的Dashboard

+

+

具体有什么高端功能还没研究。

+ +
+ + + + + + + +
+ + + + + + +
+ +
+ + + +
+ + + + + + + + +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git "a/2016/collections-Counter\346\272\220\347\240\201/index.html" "b/2016/collections-Counter\346\272\220\347\240\201/index.html" new file mode 100644 index 0000000000..8556a9c7c8 --- /dev/null +++ "b/2016/collections-Counter\346\272\220\347\240\201/index.html" @@ -0,0 +1,523 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + collections.Counter源码阅读笔记 | 贾攀的流水账 + + + + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

贾攀的流水账

+ +
+

Panmax's Blog

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+ + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ collections.Counter源码阅读笔记 +

+ + +
+ + + + +
+ + +

这几天用到 collections.Counter 的次数挺多的(比如热门标签、热门愿望、热门城市),看文档的时候就很好奇,这个类初始化的时候是如何实现既可以不传参数,可以传一个可迭代的值,可以传字典,而且还可以传关键字。

+

文档范例:

+
1
2
3
4
>>> c = Counter()                           # a new, empty counter
>>> c = Counter('gallahad') # a new counter from an iterable
>>> c = Counter({'a': 4, 'b': 2}) # a new counter from a mapping
>>> c = Counter(a=4, b=2) # a new counter from keyword args
+

先来看他的 __init__方法

+
1
2
3
4
5
6
7
8
9
10
def __init__(*args, **kwds):
if not args:
raise TypeError("descriptor '__init__' of 'Counter' object "
"needs an argument")
self = args[0]
args = args[1:]
if len(args) > 1:
raise TypeError('expected at most 1 arguments, got %d' % len(args))
super(Counter, self).__init__()
self.update(*args, **kwds)
+

刚开始不明白,明明判断了 args 不存在的话就抛出异常,但是却可以是使用无参来初始化这个类,后来想明白了,类初始化的时候会默认给一个参数,我们通常把这个参数命名为 self,所以才有了下边的语句,将 selfargs[0] 中取出。所以 args[1:] 就是剩下的参数,注意这时候 args 还是一个元组,现在来判断 args 的长度,因为除了关键字参数外,只能使用一个可迭代的值或者一个字典来初始化,所以如果args长度大于1,说明除了关键字参数外,给了超过一个以上的参数,这时候程序抛出异常。

+

这时候我就想既然是一个参数,为什么不直接给个固定值,比如像这样:

+

__init(self, iterable_or_mapping, **kwds)__

+

而是非要作为一个可变参数传入,然后再判断长度,后来想到因为还需要接受无参调用,所以不能用这样写。

+

这时候又想到能不能这样写呢:

+

__init(self, iterable_or_mapping=None, **kwds)__

+

答案是不能,因为你不知道调用者在调用时关键字参数要传什么,万一调用者要传 iterable_or_mapping 作为关键字参数怎么办。

+

最后想到能不能这样:

+

__init(self, *args, **kwds)__

+

觉得好像没什么问题。。。不知道阅读者有没有看出来问题,如果看出这样写有什么问题麻烦告诉我,我把评论关闭了,可以通过邮箱: `jiapan.china@gmail.com`

+

接下来然后调用他的 update 方法:

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
def update(*args, **kwds):
if not args:
raise TypeError("descriptor 'update' of 'Counter' object "
"needs an argument")
self = args[0]
args = args[1:]
if len(args) > 1:
raise TypeError('expected at most 1 arguments, got %d' % len(args))
iterable = args[0] if args else None
if iterable is not None:
if isinstance(iterable, Mapping):
if self:
self_get = self.get
for elem, count in iterable.iteritems():
self[elem] = self_get(elem, 0) + count
else:
super(Counter, self).update(iterable) # fast path when counter is empty
else:
self_get = self.get
for elem in iterable:
self[elem] = self_get(elem, 0) + 1
if kwds:
self.update(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) + countself 中尝试获取 elem 的数量,如果没有就初始化为0然后加上字典的值,这样写而不是直接 self[elem] = count是有原因的,因为我们还可能给关键字参数,这样写的好处是,一会用关键字参数再递归调用一下这个方法,就能把关键字参数的值也更新上去啦。

+

iterable 不是字典的时候,就会把这个值进行迭代取出每个元素计算出现的次数,具体代码就不解释了。

+
+

最后的疑惑:我不明白的是为什么要判断 self 存不存在,在什么情况下初始化一个类会没有 self

+
+

下一篇准备写写 Counter 中两个常用方法 most_commonelements。因为现在我还没看。。。

+ +
+ + + + + + + +
+ + + + + + +
+ +
+ + + +
+ + + + + + + + +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git "a/2016/collections-Counter\347\232\204\344\275\277\347\224\250/index.html" "b/2016/collections-Counter\347\232\204\344\275\277\347\224\250/index.html" new file mode 100644 index 0000000000..32a6950518 --- /dev/null +++ "b/2016/collections-Counter\347\232\204\344\275\277\347\224\250/index.html" @@ -0,0 +1,522 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + collections.Counter的使用 | 贾攀的流水账 + + + + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

贾攀的流水账

+ +
+

Panmax's Blog

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+ + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ collections.Counter的使用 +

+ + +
+ + + + +
+ + +

有一个需求是获取所有存在愿望的城市,并且找出热门城市。

+

刚好前几天看到了Python的计数器:collections.Counter, 于是就拿来用了用。

+
    +
  • 首先导入 Counter:
  • +
+
1
from collections import Counter
+
    +
  • 通过查询数据库获取到所有满足条件的愿望放在wishes数组中,然后遍历所有数据,把所有城市的adcode存在一个数组中:
  • +
+
1
cities = [wish.adcode for wish in wishes]
+
+

(我发现我最近越来越用推导式了。。。

+
+
    +
  • 然后使用Counter进行操作:
  • +
+
1
c = Counter(cities).most_common()
+

这样得到的结果是一个数组,每个元素是一个长度为2的元组,元组第0位保存的是数据,第1位保存的是这个数据出现的次数,整个数组是按照元素出现次数排序的,most_common()还可以带int型的参数,表示获取排名前n个的结果。不带参数表示获取所有结果。

+

以上得到的结果为:

+
1
[(110000, 4), (510800, 3)],...
+

获取热门城市只需拿前4个数据就行(需求是4个热门城市),其他城市拿剩下的数据即可(注意,c 里每个元素是一个元组,元组的一个0位才是我们要的数据):

+
1
2
top4 = [_[0] for _ in c[:4]]
others = [_[0] for _ in c[4:]]
+

现在 top4 里存放的就是排名前四的城市adcode,others 里存放的是其他有愿望地区的城市adcode按照愿望数量排序的数组。

+ +
+ + + + + + + +
+ + + + + + +
+ +
+ + + +
+ + + + + + + + +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2016/functools-partial/index.html b/2016/functools-partial/index.html new file mode 100644 index 0000000000..37e8838266 --- /dev/null +++ b/2016/functools-partial/index.html @@ -0,0 +1,517 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + functools.partial | 贾攀的流水账 + + + + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

贾攀的流水账

+ +
+

Panmax's Blog

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+ + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ functools.partial +

+ + +
+ + + + +
+ + +

今天在看flask源码时看到了这样的写法:

+

request = LocalProxy(partial(_lookup_req_object, 'request'))

+

第一次见partial的使用,所以查了查资料学习了下。

+

我们可以简单的理解为 partial(func, ‘request’) 就是使用 ‘request’ 作为func的第一个默认参数来产生另外一个function。

+

所以, partial(_lookup_req_object, ‘request’) 我们可以理解为:

+

生成一个callable的function,这个function主要是从 _request_ctx_stack 这个LocalStack对象获取堆栈顶部的第一个RequestContext对象,然后返回这个对象的request属性。

+
+

functools.partial 通过包装手法,允许我们 “重新定义” 函数签名

+

用一些默认参数包装一个可调用对象,返回结果是可调用对象,并且可以像原始对象一样对待

+

冻结部分函数位置函数或关键字参数,简化函数,更少更灵活的函数参数调用

+
1
2
3
4
5
6
7
8
9
10
#args/keywords 调用partial时参数
def partial(func, *args, **keywords):
def newfunc(*fargs, **fkeywords):
newkeywords = keywords.copy()
newkeywords.update(fkeywords)
return func(*(args + fargs), **newkeywords) #合并,调用原始函数,此时用了partial的参数
newfunc.func = func
newfunc.args = args
newfunc.keywords = keywords
return newfunc
+

声明:

urlunquote = functools.partial(urlunquote, encoding='latin1')

+

当调用 urlunquote(args, *kargs)

+

相当于 urlunquote(args, *kargs, encoding='latin1')

+

应用:

典型的,函数在执行时,要带上所有必要的参数进行调用。

+

然后,有时参数可以在函数被调用之前提前获知。

+

这种情况下,一个函数有一个或多个参数预先就能用上,以便函数能用更少的参数进行调用。

+ +
+ + + + + + + +
+ + + + + + +
+ +
+ + + +
+ + + + + + + + +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git "a/2016/functools-wraps-\347\232\204\344\275\234\347\224\250/index.html" "b/2016/functools-wraps-\347\232\204\344\275\234\347\224\250/index.html" new file mode 100644 index 0000000000..9f6168114d --- /dev/null +++ "b/2016/functools-wraps-\347\232\204\344\275\234\347\224\250/index.html" @@ -0,0 +1,510 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + functools.wraps 的作用 | 贾攀的流水账 + + + + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

贾攀的流水账

+ +
+

Panmax's Blog

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+ + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ functools.wraps 的作用 +

+ + +
+ + + + +
+ + +

本文翻译自Stackoverflow:What does functools.wraps do?

+

当你使用一个装饰器,就是用另一个函数替换当前函数。换一种说法,如果你有一个装饰器:

+
1
2
3
4
5
def logged(func):
def with_logging(*args, **kwargs):
print func.__name__ + " was called"
return func(*args, **kwargs)
return with_logging
+

你这样使用它:

+
1
2
3
4
@logged
def f(x):
"""does some math"""
return x + x * x
+

实际上和这种用法相同:

+
1
2
3
4
def f(x):
"""does some math"""
return x + x * x
f = logged(f)
+

而且你的函数 fwith_loging 替换。不幸的是,这意味着当你使用:

+
1
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 是它自己的装饰器,下边的代码将会做正确的事情。

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
from functools import wraps
def logged(func):
@wraps(func)
def with_logging(*args, **kwargs):
print func.__name__ + " was called"
return func(*args, **kwargs)
return with_logging

@logged
def f(x):
"""does some math"""
return x + x * x

print f.__name__ # prints 'f'
print f.__doc__ # prints 'does some math'
+ +
+ + + + + + + +
+ + + + + + +
+ +
+ + + +
+ + + + + + + + +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git "a/2016/getattr-\345\222\214-getattribute-\346\226\271\346\263\225\347\232\204\345\214\272\345\210\253/index.html" "b/2016/getattr-\345\222\214-getattribute-\346\226\271\346\263\225\347\232\204\345\214\272\345\210\253/index.html" new file mode 100644 index 0000000000..a41b784c2e --- /dev/null +++ "b/2016/getattr-\345\222\214-getattribute-\346\226\271\346\263\225\347\232\204\345\214\272\345\210\253/index.html" @@ -0,0 +1,531 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + __getattr__() 和 __getattribute__() 方法的区别 | 贾攀的流水账 + + + + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

贾攀的流水账

+ +
+

Panmax's Blog

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+ + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ __getattr__() 和 __getattribute__() 方法的区别 +

+ + +
+ + + + +
+ + +

python 在访问属性的方法上定义了__getattr__()__getattribute__() 2种方法,其区别非常细微,但非常重要。

+
    +
  • 如果某个类定义了 __getattribute__() 方法,在 每次引用属性或方法名称时 Python 都调用它(特殊方法名称除外,因为那样将会导致讨厌的无限循环)。
  • +
  • 如果某个类定义了 __getattr__() 方法,Python 将只在正常的位置查询属性时才会调用它。如果实例 x 定义了属性 colorx.color 将 不会 调用x.__getattr__('color');而只会返回 x.color 已定义好的值。
  • +
+ +

下边举几个栗子:

+

当一个类没有定义__getattr____getattribute__时,在访问类的实例一个不存在的属性时会报错

1
2
3
4
5
6
7
8
class GetAttrClass(object):
def __init__(self):
pass


if __name__ == '__main__':
gac = GetAttrClass()
print gac.x
+

上边程序运行后得到一下错误:

+
1
2
3
4
Traceback (most recent call last):
File "/Users/pan/PycharmProjects/test/getattr.py", line 13, in <module>
print gac.x
AttributeError: 'GetAttrClass' object has no attribute 'x'
+

当一个类定义__getattr____getattribute__时,在访问类的实例一个不存在的属性时会返回None

1
2
3
4
5
6
7
8
9
10
11
class GetAttrClass(object):
def __init__(self):
pass

def __getattr__(self, item):
pass


if __name__ == '__main__':
gac = GetAttrClass()
print gac.x
+

程序运行结果为:None

+

当存在已定义好的值后,不再调用__getattr__而是直接返回定义好的值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class GetAttrClass(object):
def __init__(self):
pass

def __getattr__(self, item):
if item == 'color':
return 'red'


if __name__ == '__main__':
gac = GetAttrClass()
print gac.color
gac.color = 'green'
print gac.color
+

程序运行结果为:

+
1
2
red
green
+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
class GetAttrClass(object):
def __init__(self):
self.color = 'black'

def __getattr__(self, item):
if item == 'color':
return 'red'


if __name__ == '__main__':
gac = GetAttrClass()
print gac.color
gac.color = 'green'
print gac.color
+

程序运行结果为:

+
1
2
black
green
+

当程序定义__getattribute__后,每次引用属性和方法都会调用它

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class GetAttrClass(object):
def __init__(self):
self.color = 'black'

def __getattribute__(self, item):
if item == 'color':
return 'red'


if __name__ == '__main__':
gac = GetAttrClass()
print gac.color
gac.color = 'green'
print gac.color
+

程序运行结果为:

+
1
2
red
red
+

即便已经显式地设置 gac.color,在获取 gac.color 的值时, 仍将调用 __getattribute__() 方法。如果存在 __getattribute__() 方法,将在每次查找属性和方法时 无条件地调用 它,哪怕在创建实例之后已经显式地设置了属性。

+
+

如果定义了类的 __getattribute__() 方法,你可能还想定义一个 __setattr__() 方法,并在两者之间进行协同,以跟踪属性的值。否则,在创建实例之后所设置的值将会消失在黑洞中。

+
+

必须特别小心 __getattribute__() 方法,因为 Python 在查找类的方法名称时也将对其进行调用。

1
2
3
4
5
6
7
8
9
10
11
12
class GetAttrClass(object):

def __getattribute__(self, item):
raise AttributeError

def hello(self):
print 'hello world!'


if __name__ == '__main__':
gac = GetAttrClass()
gac.hello()
+

以上程序报错:

+
1
2
3
4
5
6
Traceback (most recent call last):
File "/Users/pan/PycharmProjects/test/getattr.py", line 17, in <module>
gac.hello()
File "/Users/pan/PycharmProjects/test/getattr.py", line 9, in __getattribute__
raise AttributeError
AttributeError
+
    +
  • 该类定义了一个总是引发 AttributeError 异常的 __getattribute__() 方法。没有属性或方法的查询会成功。
  • +
  • 调用 gac.hello() 时,Python 将在 GetAttrClass 类中查找 hello() 方法。该查找将执行整个 __getattribute__()方法,因为所有的属性和方法查找都通过__getattribute__() 方法。在此例中, __getattribute__() 方法引发 AttributeError 异常,因此该方法查找过程将会失败,而方法调用也将失败。
  • +
+ +
+ + + + + + + +
+ + + + + + +
+ +
+ + + +
+ + + + + + + + +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git "a/2016/jQuery\345\256\236\347\216\260\347\275\221\351\241\265\346\227\240\351\231\220\344\270\212\346\213\211/index.html" "b/2016/jQuery\345\256\236\347\216\260\347\275\221\351\241\265\346\227\240\351\231\220\344\270\212\346\213\211/index.html" new file mode 100644 index 0000000000..3dad63ac53 --- /dev/null +++ "b/2016/jQuery\345\256\236\347\216\260\347\275\221\351\241\265\346\227\240\351\231\220\344\270\212\346\213\211/index.html" @@ -0,0 +1,519 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + jQuery实现网页无限上拉 | 贾攀的流水账 + + + + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

贾攀的流水账

+ +
+

Panmax's Blog

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+ + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ jQuery实现网页无限上拉 +

+ + +
+ + + + +
+ + +

无限上拉,说起来很高端,实际就是 APP 里边的上拉加载更多。

+

现在做的一个小 Web 项目里刚好有这个需求,之前我做的 Web 应用都是通过翻页来查看其他内容的,没有做过这种加载更多的功能,所以刚好借这个机会接触下。

+

在没做之前,想的是加载更多可能就跟手机 APP 那样,通过 js 异步加载 json 数据,然后更改 DOM 来完成这个操作。于是我就昂首阔步开始做了,刚开始想通过 vue.js 来完成,整个页面都是通过 json 数据来渲染,后来遇到各种问题,(比如,通过url来过滤数据,/tag/xxx 过滤出 tag 为 xxx 的数据,但是没有找到非常便捷的方法来传递这个值给接口)所以放弃,再然后想要不就第一页通过 jinja 来渲染出来,剩下的页通过 jQuery 来加载,然后还是感觉各种麻烦。

+

这时候想到了谷歌,查找资料后发现一个 jQuery 插件是专门来实现这个需求的,而且实现方法跟我设想的完全不一样,大致原理是:就像做普通翻页那样,告诉它下一页的地址,再告诉它需要加载更多部分的节点,这个插件会异步请求那个页面,然后把相应部分取出,加载到当前页面的底部。

+

我了个擦,我居然没有想到这种方法,我之前设想方法,还要单独去写个用来翻页的接口,增加了很多工作量,这种方法简直是棒呆了!

+

下边来介绍一下这个插件的使用:

+

插件名称:infinite-scroll

+

项目地址: Infinite Scroll jQuery Plugin

+

参考地址: http://ifxoxo.com/jquery-infinite-scroll.html

+

首先要在页面底部新增一个类似于下一页按钮的部分,这个部分用什么包裹都可以,但最里边需要有个a标签,href对应的是下一页的地址。例如: <a id="next" href="?page=2"></a> 我这里什么也没有包裹,而且 a 标签里也没加文字,这样翻到底部时看不到任何提示信息。这里可以自行写个 div 什么的,里边写着上拉加载更多这样的提示信息。当加载更多被触发时,这个部分会自动隐藏掉。

+

下边来看看 js 代码部分:

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
$(document).ready(function () {
$("#masonny-div").infinitescroll({
navSelector: "#next:last", // 页面分页元素(成功后会被隐藏)
nextSelector: "a#next:last", // 需要点击的下一页链接
itemSelector: "div.section", // ajax回来之后,每一项的selecter
animate: true, //加载完毕是否采用动态效果
extraScrollPx: 100, //向下滚动的像素,必须开启动态效果
debug: true, //调试的时候,可以打开,
path: function (index) {

return "?page=" + index;
},
loading: {
finished: undefined,
finishedMsg: '没有更多内容了', //当加载失败,或者加载不出内容之后的提示语
img: '/static/pic/loading-new.gif', //自定义loadding的动画图
msgText: '正在加载中...', //加载时的提示语
}
});

})
;
+

因为我这里需要提取出来加载更多的部分是这样的:

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<div class="col-xs-12 section">
<div onclick="location.href='{{ artcle.url }}'">
<div><h4><a>{{ artcle.title }}</a></h4></div>
<div class="content">{{ artcle.abstract }}</div>
</div>
<div class="row tag">
<div class="col-xs-6">
<div class="row" style="white-space:nowrap;">
{% for tag in artcle._tags[:3] %}
<div class="col-xs-4" onclick="location.href='/tag/{{ tag.tag_id }}'">#{{ tag.name }}</div>
{% endfor %}
</div>
</div>
<div class="col-xs-4 col-xs-offset-2"
style="text-align: end">{{ artcle.publish_date | datetime('date') }}</div>
</div>
</div>
+

所以我的 itemSelector 的值为 div.section

+

还有一点,我这个用这个插件的时候,刚开始的时候一直有问题,是因为没有给 path 写值, path 的作用是每次加载下一页的时候所对应的地址。

+

还可以给加载更多时候的 loading 编写样式,例如:

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#infscr-loading {
text-align: center;
z-index: 100;
position: fixed;
left: 45%;
bottom: 40px;
width: 200px;
padding: 10px;
background: #000;
opacity: 0.8;
color: #FFF;
-webkit-border-radius: 10px;
-moz-border-radius: 10px;
border-radius: 10px;
}
+

至此,我又 get 到一个新技能。

+ +
+ + + + + + + +
+ + + + + + +
+ +
+ + + +
+ + + + + + + + +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git "a/2016/\344\275\277\347\224\250\351\200\202\351\205\215\345\231\250\345\222\214-slots-\345\260\217\350\256\260/index.html" "b/2016/\344\275\277\347\224\250\351\200\202\351\205\215\345\231\250\345\222\214-slots-\345\260\217\350\256\260/index.html" new file mode 100644 index 0000000000..fd5dbbb765 --- /dev/null +++ "b/2016/\344\275\277\347\224\250\351\200\202\351\205\215\345\231\250\345\222\214-slots-\345\260\217\350\256\260/index.html" @@ -0,0 +1,502 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 使用适配器模式和__slots__优化代码小记 | 贾攀的流水账 + + + + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

贾攀的流水账

+ +
+

Panmax's Blog

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+ + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ 使用适配器模式和__slots__优化代码小记 +

+ + +
+ + + + +
+ + +

今天看了一篇关于设计模式方面的资料,再加上前几天看的 __slots__ 的用法,想起项目中的更新用户资料相关代码可以用上边的知识(适配器模式, __slots____setattr__)优化一下:

修改前:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
class UserModel(object):

def __init__(self, user_id=None):
self.user_id = user_id
self.query = Query(_User)

...

def update_profile(self, avatar=None, nickname=None, birth=None,
mind=None, height=None, weight=None, sexuality=None, emotion=None,
haunt=None, been=None, company=None, position=None, graduated=None,
industry=None, salary=None, hometown=None):
u = self.get_by_id()
user_profile = UserProflieModel.get_or_create_user_profile(u)

if avatar is not None:
avatar_file = Query(_File).get(avatar)
u.set('avatar', avatar_file)
if nickname is not None:
u.set('nickname', nickname)
if birth is not None:
birthday = datetime.fromtimestamp(birth)
u.set('birthday', birthday)
if mind is not None:
if len(mind) > 300:
raise LeanCloudError(20504, errmsg.ERRMSG[20504])
u.set('mind', mind)
if height is not None:
user_profile.set('height', height)
if weight is not None:
user_profile.set('weight', weight)
if sexuality is not None:
user_profile.set('sexuality', sexuality)
if emotion is not None:
user_profile.set('emotion', emotion)
if haunt is not None:
user_profile.set('haunt', haunt)
if been is not None:
user_profile.set('been', been)
if company is not None:
user_profile.set('company', company)
if position is not None:
user_profile.set('position', position)
if graduated is not None:
user_profile.set('graduated', graduated)
if industry is not None:
user_profile.set('industry', industry)
if salary is not None:
user_profile.set('salary', salary)
if hometown is not None:
region = Query(Region).equal_to('adcode', hometown).first()
user_profile.set('hometown', region)

try:
u.save()
user_profile.save()
except LeanCloudError as e:
logging.error(e)
return False
return True
+

修改后:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
class Account(object):
__slots__ = ('user_id', 'user', 'user_profile', 'account_money')

def __init__(self, user_id):
self.user_id = user_id
self.user = Query(_User).include('avatar.url', 'password').get(user_id)
self.user_profile = UserProflieModel.get_or_create_user_profile(self.user)
self.account_money = None

def __setattr__(self, key, value):
if key in self.__slots__:
object.__setattr__(self, key, value)
elif key in ('height', 'weight', 'sexuality', 'emotion', 'haunt', 'been', 'company',
'position', 'graduated', 'industry', 'salary', 'hometown') and value is not None:
if key == 'hometown':
value = Query(Region).equal_to('adcode', value).first()
self.user_profile.set(key, value)
elif key in ('avatar', 'nickname', 'birth', 'mind') and value is not None:
if key == 'avatar':
value = Query(_File).get(value)
elif key == 'birth':
value = datetime.fromtimestamp(value/1000)
elif key == 'mind':
if len(value) > 300:
raise LeanCloudError(20504, errmsg.ERRMSG[20504])
self.user.set(key, value)

def save(self):
self.user.save()
self.user_profile.save()
# self.account_money.save()

class UserModel(object):

def __init__(self, user_id=None):
self.user_id = user_id
self.query = Query(_User)

...

def update_profile(self, avatar=None, nickname=None, birth=None,
mind=None, height=None, weight=None, sexuality=None, emotion=None,
haunt=None, been=None, company=None, position=None, graduated=None,
industry=None, salary=None, hometown=None):
account = Account(self.user_id)
account.avatar = avatar
account.nickname = nickname
account.birth = birth
account.mind = mind
account.height = height
account.weight = weight
account.sexuality = sexuality
account.emotion = emotion
account.haunt = haunt
account.been = been
account.company = company
account.position = position
account.graduated = graduated
account.industry = industry
account.salary = salary
account.hometown = hometown

try:
account.save()
except LeanCloudError as e:
logging.error(e)
return False
return True
+

感觉自己萌萌哒 (。◕∀◕。)

+
+ + + + + + + +
+ + + + + + +
+ +
+ + + +
+ + + + + + + + +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git "a/2016/\345\246\202\344\275\225\345\260\206\344\270\200\344\270\252\345\267\262\345\255\230\345\234\250\347\232\204\351\241\271\347\233\256push\345\210\260GitHub/index.html" "b/2016/\345\246\202\344\275\225\345\260\206\344\270\200\344\270\252\345\267\262\345\255\230\345\234\250\347\232\204\351\241\271\347\233\256push\345\210\260GitHub/index.html" new file mode 100644 index 0000000000..b996c20898 --- /dev/null +++ "b/2016/\345\246\202\344\275\225\345\260\206\344\270\200\344\270\252\345\267\262\345\255\230\345\234\250\347\232\204\351\241\271\347\233\256push\345\210\260GitHub/index.html" @@ -0,0 +1,510 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 如何将一个已存在的项目push到GitHub | 贾攀的流水账 + + + + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

贾攀的流水账

+ +
+

Panmax's Blog

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+ + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ 如何将一个已存在的项目push到GitHub +

+ + +
+ + + + +
+ + +

第一步:创建一个仓库

这需要登录到GitHub并且穿件一个仓库。你可以选择是否初始化一个README。这不重要,因为你将会覆盖在这个远程仓库里的所有东西。

+

第二步:在项目目录初始化Git

通过你的终端并且确保Git已经安装在你的电脑上,导航到你想要添加的目录后运行下边的命令。

+

初始化git仓库

1
git init
+

增加文件到Git索引

1
git add -A
+

提交增加过的文件

1
git commit -m "Added my project"
+

增加新的远程源(在这里是GitHub)

1
git remote add git@github.com:Panmax/playground.git
+

or

+
1
git remote add https://github.com/Panmax/playground.git
+

Push到GitHub

1
git push -u -f origin master
+

到这里有些事情需要注意,-f标记代表force,这将会自动的覆盖远程目录中所有的东西。我们只是用来覆盖GitHub自动初始化README。如果你跳过了,那么-f标记就不是必须的了。

+

-u标记将远程源设置为默认,这让你以后容易的使用git pushgit pull而不用指定一个源。在这种情况下,我们总是想要的GitHub。

+

总起来

1
2
3
4
5
git init
git add -A
git commit -m 'Added my project'
git remote add origin git@github.com:Panmax/playground.git
git push -u -f origin master
+
+ + + + + + + +
+ + + + + + +
+ +
+ + + +
+ + + + + + + + +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git "a/2016/\346\234\211\350\266\243\347\232\204this-py\346\272\220\347\240\201/index.html" "b/2016/\346\234\211\350\266\243\347\232\204this-py\346\272\220\347\240\201/index.html" new file mode 100644 index 0000000000..ad58570ecd --- /dev/null +++ "b/2016/\346\234\211\350\266\243\347\232\204this-py\346\272\220\347\240\201/index.html" @@ -0,0 +1,509 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 有趣的this.py源码 | 贾攀的流水账 + + + + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

贾攀的流水账

+ +
+

Panmax's Blog

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+ + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ 有趣的this.py源码 +

+ + +
+ + + + +
+ + +

以前知道python中有个彩蛋,在Python shell下,输入

+
1
import this
+

会输出:

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
The Zen of Python, by Tim Peters

Beautiful is better than ugly.
Explicit is better than implicit.
Simple is better than complex.
Complex is better than complicated.
Flat is better than nested.
Sparse is better than dense.
Readability counts.
Special cases aren't special enough to break the rules.
Although practicality beats purity.
Errors should never pass silently.
Unless explicitly silenced.
In the face of ambiguity, refuse the temptation to guess.
There should be one-- and preferably only one --obvious way to do it.
Although that way may not be obvious at first unless you're Dutch.
Now is better than never.
Although never is often better than *right* now.
If the implementation is hard to explain, it's a bad idea.
If the implementation is easy to explain, it may be a good idea.
Namespaces are one honking great idea -- let's do more of those!
+

当时认为this模块就是直接把上边字符串print出来而已。

+

今天心血来潮,看了下this.py的源码

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
s = """Gur Mra bs Clguba, ol Gvz Crgref

Ornhgvshy vf orggre guna htyl.
Rkcyvpvg vf orggre guna vzcyvpvg.
Fvzcyr vf orggre guna pbzcyrk.
Pbzcyrk vf orggre guna pbzcyvpngrq.
Syng vf orggre guna arfgrq.
Fcnefr vf orggre guna qrafr.
Ernqnovyvgl pbhagf.
Fcrpvny pnfrf nera'g fcrpvny rabhtu gb oernx gur ehyrf.
Nygubhtu cenpgvpnyvgl orngf chevgl.
Reebef fubhyq arire cnff fvyragyl.
Hayrff rkcyvpvgyl fvyraprq.
Va gur snpr bs nzovthvgl, ershfr gur grzcgngvba gb thrff.
Gurer fubhyq or bar-- naq cersrenoyl bayl bar --boivbhf jnl gb qb vg.
Nygubhtu gung jnl znl abg or boivbhf ng svefg hayrff lbh'er Qhgpu.
Abj vf orggre guna arire.
Nygubhtu arire vf bsgra orggre guna *evtug* abj.
Vs gur vzcyrzragngvba vf uneq gb rkcynva, vg'f n onq vqrn.
Vs gur vzcyrzragngvba vf rnfl gb rkcynva, vg znl or n tbbq vqrn.
Anzrfcnprf ner bar ubaxvat terng vqrn -- yrg'f qb zber bs gubfr!"""

d = {}
for c in (65, 97):
for i in range(26):
d[chr(i+c)] = chr((i+13) % 26 + c)

print "".join([d.get(c, c) for c in s])
+

当时我就震惊了。。。

+

乍一看,s保存的是个什么鬼😂,还以为是什么小语种,然后进行国际化后再输出呢,再往下看,才明白了原理。

+

先把所有大小写字母经过一定算法转换后,将对照表保存在一个字典中,逐个遍历”加密”后的字符串,从字典中取出对应结果然后进行拼接后再输出。。。

+

顺便也知道了chr(c)的用处。

+ +
+ + + + + + + +
+ + + + + + +
+ +
+ + + +
+ + + + + + + + +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git "a/2016/\347\256\200\346\230\216-Python-\347\274\226\347\250\213\350\247\204\350\214\203/index.html" "b/2016/\347\256\200\346\230\216-Python-\347\274\226\347\250\213\350\247\204\350\214\203/index.html" new file mode 100644 index 0000000000..a9d962ea80 --- /dev/null +++ "b/2016/\347\256\200\346\230\216-Python-\347\274\226\347\250\213\350\247\204\350\214\203/index.html" @@ -0,0 +1,680 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 简明 Python 编程规范 | 贾攀的流水账 + + + + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

贾攀的流水账

+ +
+

Panmax's Blog

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+ + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ 简明 Python 编程规范 +

+ + +
+ + + + +
+ + +

原文地址:http://blog.csdn.net/gzlaiyonghao/article/details/6601123

+

编码

    +
  • 所有的 Python 脚本文件都应在文件头标上如下标识或其兼容格式的标识:
  • +
+
1
# -*- coding:utf-8 -*-
+
    +
  • 设置编辑器,默认保存为 utf-8 格式。
  • +
+

注释

    +
  • 业界普遍认同 Python 的注释分为两种的概念,一种是由 # 开头的“真正的”注释,另一种是 docstrings。前者表明为何选择当前实现以及这种实现的原理和难点,后者表明如何使用这个包、模块、类、函数(方法),甚至包括使用示例和单元测试。
  • +
  • 坚持适当注释原则。对不存在技术难点的代码坚持不注释,对存在技术难点的代码必须注释。但与注释不同,推荐对每一个包、模块、类、函数(方法)写 docstrings,除非代码一目了然,非常简单。
  • +
+

格式

缩进

    +
  • Python 依赖缩进来确定代码块的层次,行首空白符主要有两种:tab 和空格,但严禁两者混用。
  • +
  • 公司内部使用 4 个空格的 tab 进行缩进。
  • +
+
+

(此处修改为我自己的规范,原文是使用2个空格)

+
+

空格

    +
  • 空格在 Python 代码中是有意义的,因为 Python 的语法依赖于缩进,在行首的空格称为前导空格。在这一节不讨论前导空格相关的内容,只讨论非前导空格。非前导空格在 Python 代码中没有意义,但适当地加入非前导空格可以增进代码的可读性。
  • +
  • 在二元算术、逻辑运算符前后加空格,如:
  • +
+
1
a = b + c
+
    +
  • “:”用在行尾时前后皆不加空格,如分枝、循环、函数和类定义语言;用在非行尾时两端加空格,如 dict 对象的定义:
  • +
+
1
d = {'key' : 'value'}
+
    +
  • 括号(含圆括号、方括号和花括号)前后不加空格,如:
  • +
+
1
do_something(arg1, arg2)
+

而不是

+
1
do_something( arg1, arg2 )
+
    +
  • 逗号后面加一个空格,前面不加空格。
  • +
+

空行

    +
  • 适当的空行有利于增加代码的可读性,加空行可以参考如下几个准则:
      +
    • 在类、函数的定义间加空行;
    • +
    • 在 import 不同种类的模块间加空行;
    • +
    • 在函数中的逻辑段落间加空行,即把相关的代码紧凑写在一起,作为一个逻辑段落,段落间以空行分隔
    • +
    +
  • +
+

断行

    +
  • 尽管现在的宽屏显示器已经可以单屏显示超过 256 列字符,但本规范仍然坚持行的最大长度不得超过 78 个字符的标准。折叠长行的方法有以下几种方法:

    +
      +
    • 为长变量名换一个短名,如:

      +
      1
      this._is.a.very.long.variable_name = this._is.another.long.variable_name
      +
    • +
    +
  • +
+
- 应改为:
+
+
1
2
3
variable_name1 = this._is.a.very.long.variable_name  
variable_name2 = this._is.another.variable_name
variable_name1 = variable_name2s
+ + +- 在括号(包括圆括号、方括号和花括号)内换行,如: + +
1
2
3
4
class Edit(Widget):  
def __init__(self, parent, width,
font = FONT, color = BLACK, pos = POS, style = 0): # 注意:多一层缩进
pass
+ + +或 + +
1
2
3
4
5
6
very_very_very_long_variable_name = Edit(parent,  
width,
font,
color,
pos) # 注意:多一层缩进
do_sth_with(very_very_very_long_variable_name)
+ + +- 如果行长到连第一个括号内的参数都放不下,则每个元素都单独占一行: + +
1
2
3
4
5
6
7
very_very_very_long_variable_name = ui.widgets.Edit(  
panrent,
width,
font,
color,
pos) # 注意:多一层缩进
do_sth_with(very_very_very_long_variable_name)
+ + +- 在长行加入续行符强行断行,断行的位置应在操作符前,且换行后多一个缩进,以使维护人员看代码的时候看到代码行首即可判定这里存在换行,如: + +
1
2
3
4
5
if color == WHITE or color == BLACK \  
or color == BLUE: # 注意 or 操作符在新行的行首而不是旧行的行尾,上一行的续行符不可省略
do_something(color);
else:
do_something(DEFAULT_COLOR);
+

命名

    +
  • 一致的命名可以给开发人员减少许多麻烦,而恰如其分的命名则可以大幅提高代码的可读性,降低维护成本。
  • +
+

常量

    +
  • 常量名所有字母大写,由下划线连接各个单词,如:
  • +
+
1
2
WHITE = 0xffffffff  
THIS_IS_A_CONSTANT = 1
+

变量

    +
  • 变量名全部小写,由下划线连接各个单词,如:
  • +
+
1
2
color = WHITE  
this_is_a_variable = 1
+
    +
  • 不论是类成员变量还是全局变量,均不使用 m 或 g 前缀。私有类成员使用单一下划线前缀标识,多定义公开成员,少定义私有成员。
  • +
  • 变量名不应带有类型信息,因为 Python 是动态类型语言。如 iValue、names_list、dict_obj 等都是不好的命名。
  • +
+

函数

    +
  • 函数名的命名规则与变量名相同。
  • +
+

    +
  • 类名单词首字母大写,不使用下划线连接单词,也不加入 C、T 等前缀。如:
  • +
+
1
2
class ThisIsAClass(object):  
passs
+

模块

    +
  • 模块名全部小写,对于包内使用的模块,可以加一个下划线前缀,如:
  • +
+
1
2
3

module.py
_internal_module.py
+

    +
  • 包的命名规范与模块相同。
  • +
+

缩写

    +
  • 命名应当尽量使用全拼写的单词,缩写的情况有如下两种:

    +
      +
    • 常用的缩写,如 XML、ID等,在命名时也应只大写首字母,如:

      +
      1
      class XmlParser(object):pass
      +
    • +
    +
  • +
+
- 命名中含有长单词,对某个单词进行缩写。这时应使用约定成俗的缩写方式,如去除元音、包含辅音的首字符等方式,例如:
+    - function 缩写为 fn
+    - text 缩写为 txt
+    - object 缩写为 obj
+    - count 缩写为 cnt
+    - number 缩写为 num,等。
+

特定命名方式

    +
  • 主要是指 __xxx__ 形式的系统保留字命名法。项目中也可以使用这种命名,它的意义在于这种形式的变量是只读的,这种形式的类成员函数尽量不要重载。如:
  • +
+
1
2
3
4
5
6
class Base(object):  
def __init__(self, id, parent = None):
self.__id__ = id
self.__parent__ = parent
def __message__(self, msgid):
# …略
+

其中 __id____parent____message__ 都采用了系统保留字命名法。

+

语句

import

    +
  • import 语句有以下几个原则需要遵守:

    +
      +
    • import 的次序,先 import Python 内置模块,再 import 第三方模块,最后 import 自己开发的项目中的其它模块;这几种模块中用空行分隔开来。
    • +
    • 一条 import 语句 import 一个模块。
    • +
    • 当从模块中 import 多个对象且超过一行时,使用如下断行法(此语法 py2.5 以上版本才支持):

      +
      1
      2
      from module import (obj1, obj2, obj3, obj4,  
      obj5, obj6)
      +
    • +
    +
  • +
+
- 不要使用 from module import *,除非是 import 常量定义模块或其它你确保不会出现命名空间冲突的模块。
+

赋值

    +
  • 对于赋值语句,主要是不要做无谓的对齐,如:
  • +
+
1
2
3
a        = 1                  # 这是一个行注释  
variable = 2 # 另一个行注释
fn = callback_function # 还是行注释
+

没有必要做这种对齐,原因有两点:一是这种对齐会打乱编程时的注意力,大脑要同时处理两件事(编程和对齐);二是以后阅读和维护都很困难,因为人眼的横向视野很窄,把三个字段看成一行很困难,而且维护时要增加一个更长的变量名也会破坏对齐。直接这样写为佳:

+
1
2
3
a = 1 # 这是一个行注释  
variable = 2 # 另一个行注释
fn = callback_function # 还是行注释
+

分枝和循环

    +
  • 对于分枝和循环,有如下几点需要注意的:

    +
      +
    • 不要写成一行,如:

      +
      1
      if not flg: pass
      +
    • +
    +
  • +
+
和
+
+
1
for i in xrange(10): print i
+ + +都不是好代码,应写成 + +
1
2
3
4
if not flg:  
pass
for i in xrange(10):
print i
+ + +注:本文档中出现写成一行的例子是因为排版的原因,不得作为编码中不断行的依据。 +
    +
  • 条件表达式的编写应该足够 pythonic,如以下形式的条件表达式是拙劣的:
  • +
+
1
2
3
4
5
if len(alist) != 0: do_something()  
if alist != []: do_something()
if s != "": do_something()
if var != None: do_something()
if var != False: do_something()
+

上面的语句应该写成:

+
1
2
3

if seq: do_somethin() # 注意,这里命名也更改了
if var: do_something()
+
    +
  • 用得着的时候多使用循环语句的 else 分句,以简化代码。
  • +
+

已有代码

    +
  • 对于项目中已有的代码,可能因为历史遗留原因不符合本规范,应当看作可以容忍的特例,允许存在;但不应在新的代码中延续旧的风格。
  • +
  • 对于第三方模块,可能不符合本规范,也应看作可以容忍的特例,允许存在;但不应在新的代码中使用第三方模块的风格。
  • +
  • tab 与空格混用的缩进是’’’不可容忍’’’的,在运行项目时应使用 -t 或 -tt 选项排查这种可能性存在。出现混用的情况时,如果是公司开发的基础类库代码,应当通知类库维护人员修改;第三方模块则可以通过提交 patch 等方式敦促开发者修正问题。
  • +
+

已有风格

    +
  • 开发人员往往在加入项目之前已经形成自有的编码风格,加入项目后应以本规范为准编写代码。特别是匈牙利命名法,因为带有类型信息,并不适合 Python 编程,不应在 Python 项目中应用。
  • +
+ +
+ + + + + + + +
+ + + + + + +
+ +
+ + + +
+ + + + + + + + +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git "a/2016/\347\277\273\345\207\272CSDN\345\215\232\345\256\242\346\234\211\346\204\237/1.jpg" "b/2016/\347\277\273\345\207\272CSDN\345\215\232\345\256\242\346\234\211\346\204\237/1.jpg" new file mode 100644 index 0000000000..316de81cfe Binary files /dev/null and "b/2016/\347\277\273\345\207\272CSDN\345\215\232\345\256\242\346\234\211\346\204\237/1.jpg" differ diff --git "a/2016/\347\277\273\345\207\272CSDN\345\215\232\345\256\242\346\234\211\346\204\237/index.html" "b/2016/\347\277\273\345\207\272CSDN\345\215\232\345\256\242\346\234\211\346\204\237/index.html" new file mode 100644 index 0000000000..c0309666c3 --- /dev/null +++ "b/2016/\347\277\273\345\207\272CSDN\345\215\232\345\256\242\346\234\211\346\204\237/index.html" @@ -0,0 +1,505 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 翻出CSDN博客有感 | 贾攀的流水账 + + + + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

贾攀的流水账

+ +
+

Panmax's Blog

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+ + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ 翻出CSDN博客有感 +

+ + +
+ + + + +
+ + +

今天有同事说自己的CSDN博客被盗了,我才隐约想起我以前好像也在CSDN写过东西。找了好久才找到当年的博客,都是那时候学C++时候的笔记,如果我到现在还在坚持用C++的话,也许也已经能和「轮子哥」谈笑风生了吧。

+

+

C++让我懂了很多直接学习动态语言和其他高级语言(比如Java,我并没有黑Java)所接触不到的和更底层的东西,比如指针、内存动态分配、构造函数和析构函数的作用等等。
那时候还亲自动手写过各种数据结构和算法的C++实现,也走过很多很多的弯路。

+

翻到这个东西还能证明一点,我已经符合至少三年开发经验的要求了😂

+

还有,不要报有在达X培训3个月就能成大牛的想法,我都鼓捣快四年了也才刚刚入门。。。

+ +
+ + + + + + + +
+ + + + + + +
+ +
+ + + +
+ + + + + + + + +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git "a/2016/\350\247\243\345\206\263Linux\344\270\213VIM\344\270\255\346\226\207\344\271\261\347\240\201/index.html" "b/2016/\350\247\243\345\206\263Linux\344\270\213VIM\344\270\255\346\226\207\344\271\261\347\240\201/index.html" new file mode 100644 index 0000000000..117770edf6 --- /dev/null +++ "b/2016/\350\247\243\345\206\263Linux\344\270\213VIM\344\270\255\346\226\207\344\271\261\347\240\201/index.html" @@ -0,0 +1,503 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 解决Linux下VIM中文乱码 | 贾攀的流水账 + + + + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

贾攀的流水账

+ +
+

Panmax's Blog

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+ + +
+
+
+ + +
+ + + + + + + + + + + + +
+ +
+ + + +
+ + + + + + + + +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git "a/2016/\350\247\243\345\206\263jinja2\344\270\216Vue-js\347\232\204\346\250\241\346\235\277\345\206\262\347\252\201/index.html" "b/2016/\350\247\243\345\206\263jinja2\344\270\216Vue-js\347\232\204\346\250\241\346\235\277\345\206\262\347\252\201/index.html" new file mode 100644 index 0000000000..2aed1b9843 --- /dev/null +++ "b/2016/\350\247\243\345\206\263jinja2\344\270\216Vue-js\347\232\204\346\250\241\346\235\277\345\206\262\347\252\201/index.html" @@ -0,0 +1,506 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 解决Jinja2与Vue.js的模板冲突 | 贾攀的流水账 + + + + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

贾攀的流水账

+ +
+

Panmax's Blog

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+ + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ 解决Jinja2与Vue.js的模板冲突 +

+ + +
+ + + + +
+ + +

主要思路是通过修改Jinja2的配置,让他只渲染之间的数据,注意空格,而Vue.js处理不加空格的模板。

+

操作:

+
1
2
app.jinja_env.variable_start_string = '{{ '
app.jinja_env.variable_end_string = ' }}'
+

就酱~

+

我这个项目中还使用了flask-bootstrap作为模板,不幸的是,flask-bootstrap使用的大括号都没加空格,导致页面渲染时出现问题。所以我将flask-bootstrap源码进行了修改,安装时,只要用我的数据源安装即可git+https://github.com/Panmax/flask-bootstrap.git

+ +
+ + + + + + + +
+ + + + + + +
+ +
+ + + +
+ + + + + + + + +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git "a/2016/\351\232\217\346\234\272\344\273\216\344\270\200\344\270\252\346\225\260\347\273\204\344\270\255\345\217\226\345\207\272n\344\270\252\345\205\203\347\264\240\345\271\266\346\211\223\344\271\261\351\241\272\345\272\217\346\236\204\346\210\220\346\226\260\347\232\204\346\225\260\347\273\204/index.html" "b/2016/\351\232\217\346\234\272\344\273\216\344\270\200\344\270\252\346\225\260\347\273\204\344\270\255\345\217\226\345\207\272n\344\270\252\345\205\203\347\264\240\345\271\266\346\211\223\344\271\261\351\241\272\345\272\217\346\236\204\346\210\220\346\226\260\347\232\204\346\225\260\347\273\204/index.html" new file mode 100644 index 0000000000..093f32072b --- /dev/null +++ "b/2016/\351\232\217\346\234\272\344\273\216\344\270\200\344\270\252\346\225\260\347\273\204\344\270\255\345\217\226\345\207\272n\344\270\252\345\205\203\347\264\240\345\271\266\346\211\223\344\271\261\351\241\272\345\272\217\346\236\204\346\210\220\346\226\260\347\232\204\346\225\260\347\273\204/index.html" @@ -0,0 +1,508 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 随机从一个数组中取出n个元素打乱顺序构成新的数组 | 贾攀的流水账 + + + + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

贾攀的流水账

+ +
+

Panmax's Blog

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+ + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ 随机从一个数组中取出n个元素打乱顺序构成新的数组 +

+ + +
+ + + + +
+ + +

今天要求实现一个功能是从被推荐的用户中,随机取出n个用户,并打乱顺序返回。

+

看了看random模块刚好有这种功能的实现,所以就直接拿来用了。

+

所有被推荐用户的列表为:recommend_users,已经确定的是,这个列表的长度一定大于n。

+

我们需要将结果保存在recommend_user_list中。

+

首先,从这个列表中随机取出n个元素:

+
1
2
import random
recommend_user_list = random.sample(recommend_users, n)
+

这个方法是从recommend_users列表中按顺序随机取出n个元素,因为我们需要打乱顺序,所以还需要调用另一个方法。

+
1
2
# 将这个数组打乱顺序
random.shuffle(recommend_user_list)
+

搞定!~

+ +
+ + + + + + + +
+ + + + + + +
+ +
+ + + +
+ + + + + + + + +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git "a/2017/CAS-\347\246\201\347\224\250SSL-\347\232\204\346\226\271\345\274\217/index.html" "b/2017/CAS-\347\246\201\347\224\250SSL-\347\232\204\346\226\271\345\274\217/index.html" new file mode 100644 index 0000000000..2f8978b4f1 --- /dev/null +++ "b/2017/CAS-\347\246\201\347\224\250SSL-\347\232\204\346\226\271\345\274\217/index.html" @@ -0,0 +1,490 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + CAS 禁用SSL 的方式 | 贾攀的流水账 + + + + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

贾攀的流水账

+ +
+

Panmax's Blog

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+ + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ CAS 禁用SSL 的方式 +

+ + +
+ + + + +
+ + +

由于要内部使用,所以不需要配置 https 链接。

+

本文是基于 CAS 5.0.X 下进行的修改,修改方式如下:

+

我使用 overlay 的方式进行的部署,只需在 etc/cas/config/cas.properties 中配置如下三项即可

+
1
2
3
server.ssl.enabled=false
cas.tgc.secure=false
cas.warningCookie.secure=false
+

但是现在还有一个问题时,几遍将 server.port 改为其他端口,http 的端口号也还是 8080,也就是说这里修改的 server.port 是修改的 https 的方式。

+ +
+ + + + + + + +
+ + + + + + +
+ +
+ + + +
+ + + + + + + + +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2017/Local-Storage-vs-Session-Storage/index.html b/2017/Local-Storage-vs-Session-Storage/index.html new file mode 100644 index 0000000000..19fefb91a9 --- /dev/null +++ b/2017/Local-Storage-vs-Session-Storage/index.html @@ -0,0 +1,488 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Local Storage vs Session Storage | 贾攀的流水账 + + + + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

贾攀的流水账

+ +
+

Panmax's Blog

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+ + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ Local Storage vs Session Storage +

+ + +
+ + + + +
+ + +

localStoragesessionStorage 都继承自 Storage。除了 sessionStorage 是为了做非持久化的目的,两者没有区别。

+

意思是,数据会一直存储在 localStorage 中直到被明确删除。所做的修改将被保存并且对于当前和未来所有的访问都是有效的。

+

对于 sessionStorage, 修改在每个窗口(或者 Chrome 和 Firefox 的 tab 中)下有效。所做的修改将被保存并且在当前页下有效,以及未来可以在当前窗口下访问。当窗口被关闭,存储就会被删除。

+ +
+ + + + + + + +
+ + + + + + +
+ +
+ + + +
+ + + + + + + + +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git "a/2017/SQLAlchemy-\345\210\206\350\241\250\345\256\236\350\267\265/index.html" "b/2017/SQLAlchemy-\345\210\206\350\241\250\345\256\236\350\267\265/index.html" new file mode 100644 index 0000000000..96d984eb67 --- /dev/null +++ "b/2017/SQLAlchemy-\345\210\206\350\241\250\345\256\236\350\267\265/index.html" @@ -0,0 +1,504 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + SQLAlchemy 分表实践 | 贾攀的流水账 + + + + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

贾攀的流水账

+ +
+

Panmax's Blog

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+ + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ SQLAlchemy 分表实践 +

+ + +
+ + + + +
+ + +

去年年底利用工作之余开发了一个进销存相关的 SAAS 项目,ORM 用的 SQLAlchemy,并且进行了一些分表操作,这里来做个简单的记录(也只能是简单记录了,我是小半年前进行的分表调研)。

+

我没有直接使用 继承自 db.Model 的 ORM 类来操作数据库,而是在其之上又封装了一层,将更具体的一些数据库操作进行了封装。

+

举个例子,我有一个继承自 db.ModelItem 类,同时还有一个自己的 Item 类,然后在自己的 Item 类中引用继承自 db.Model 的类,为了防止名称冲突,在引用时我会将继承自 db.Model 的类叫做 SqlaItem

+

分表是如何实现的呢,我通过 SQLAlchemyautomap_base() 将数据库中所有表进行了映射,然后自己实现了分表函数,通过分表函数得到分表的名称,然后动态的拿到那个表所对应的 ORM。

+

直接看代码:

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
# -*- coding: utf-8 -*-

from __future__ import unicode_literals, absolute_import

from sqlalchemy import create_engine
from sqlalchemy.ext.automap import automap_base

from fuxi.config import SQLALCHEMY_DATABASE_URI

engine = create_engine(SQLALCHEMY_DATABASE_URI)


def ab_cls():
ab_cls = automap_base()
ab_cls.prepare(engine, reflect=True)
return ab_cls


def _get_ab_cls():
if not getattr(_get_ab_cls, '_ab_cls', None):
_ab_cls = ab_cls()
_get_ab_cls._ab_cls = _ab_cls
return _get_ab_cls._ab_cls

ab_cls = _get_ab_cls()
+

所有分了表的类,都要通过 ab_cls 来获取表映射出来的对象。

+

还是以 Item 为例,看一下我的相关代码

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@classmethod
def _get_db_branch_name_by_user(cls, user_id):
branch = str(user_id)[-1]
return branch

@classmethod
def _get_item_dao(cls, branch_name):
tablename = 'item_%s' % branch_name
dao = getattr(ab_cls.classes, tablename)
return dao

@classmethod
def get_dao(cls, user_id):
branch = cls._get_db_branch_name_by_user(user_id)
return cls._get_item_dao(branch)
+

我通过这几个方法实现了获取分表映射的功能,在具体使用时,可以直接用 get_dao(user_id) 获取表映射(我是通过用户ID的规则进行的分表)。

+

随便看一个操作:

+
1
2
3
4
5
6
7
8
9
10
@classmethod
def gets_by_name(cls, user_id, name):
SqlaItem = cls.get_dao(user_id)

cond = (SqlaItem.user_id == user_id)
cond &= (SqlaItem.status != ItemStatus.DELETE.value)
if name:
cond &= (SqlaItem.name == name)
query = db.session.query(SqlaItem).filter(cond)
return [cls.init_from_sqla(x) for x in query]
+

我先通过 get_dao(user_id) 获取到这个用户数据所在的表的映射,然后就可进行各种 CURD 操作了。

+

也就是说,在我的项目中其实有两种获取 SqlaXxxx 的方法,如果没有分表,那么直接用继承自 db.Model 的类即可,如果是分了表的,就用动态映射出来的,所以后者实际上是不需要写继承自 db.Model 的类的,但是为了在初始化时生成所有表结构,我还是写了这些类,只不过这些类所对应的表都是分表中的第一张表,以 Item 为例

+
1
2
3
4
5
6
7
class Item(db.Model):
"""
商品
分表策略: 用户ID最后 1 位
"""
__tablename__ = 'item_1'
...
+

这样的话,我在执行 create_db 时所有需要分表的第一个表都会被建好,这个时候,我只需要再写个简单的脚本,就可以帮我把剩余的表建出来了,因为我所有分表结尾都是以下划线1或者01组成的,意思是,如果表需要分成 10 个,那么对应的第一个表的名称就是 xxx_1,如果需要分成 100 个,那么对应的第一个表的名称就是 xxx_01,所以我根据这个规则写了生成剩余表的脚本,如下:

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
# -*- coding: utf-8 -*-

from __future__ import unicode_literals, absolute_import

import os
from urlparse import urlparse

import MySQLdb

from envcfg.json.fuxi import SQLALCHEMY_DATABASE_URI

p = urlparse(SQLALCHEMY_DATABASE_URI)

DATABASE_HOST = p.hostname
DATABASE_USER = p.username
DATABASE_PASSWD = p.password if p.password else ""


def _get_table_name(table):
table_name_start = table.find('`') + 1
table_name_end = table.find('`', table_name_start)
return table[table_name_start:table_name_end]


def create_table(table):
"""
首先根据表名确定当前表是否需要分表
如果表名是已1结尾,那么就创建0-9结尾的表
如果表名是已01结尾,那么就创建00-99结尾的表
如果以上情况都不是,直接创建
"""
db = MySQLdb.connect(DATABASE_HOST, DATABASE_USER, DATABASE_PASSWD, "fuxi")
cursor = db.cursor()

assert table.startswith('CREATE TABLE')
table_name = _get_table_name(table)
if table_name.endswith('01'):
replace_table = table.replace(table_name, '%s')
for i in range(100):
sharding_table_name = '%s_%02d' % (table_name[:-3], i)
try:
cursor.execute(replace_table % sharding_table_name)
print '%s create success' % sharding_table_name
except Exception as e:
if e.args[0] != 1050:
raise e
print '%s exsit' % sharding_table_name
elif table_name.endswith('1'):
replace_table = table.replace(table_name, '%s')
for i in range(10):
sharding_table_name = '%s_%s' % (table_name[:-2], i)
try:
cursor.execute(replace_table % sharding_table_name)
print '%s create success' % sharding_table_name
except Exception as e:
if e.args[0] != 1050:
raise e
print '%s exsit' % sharding_table_name
else:
try:
cursor.execute(table)
print '%s create success' % table_name
except Exception as e:
if e.args[0] != 1050:
raise e
print '%s exsit' % table_name
db.close()


def gets_all_tables():
with open(os.path.dirname(os.path.realpath(__file__)) + '/fuxi.sql', 'r') as f:
content = f.read()
tables = content.split('\n\n')
for table in tables:
create_table(table)

if __name__ == '__main__':
gets_all_tables()
+

这种方式有个弊端,在项目启动时就需要将所有表结构读入到内存中,直接的表现是启动比较慢,占用内存比较多。

+

我不觉得这是个最佳方案,所以如果有更好的方案或者有任何疑问请通过邮件(jiapan.china#gmail.com)的方式告诉我,谢谢。

+ +
+ + + + + + + +
+ + + + + + +
+ +
+ + + +
+ + + + + + + + +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2017/Soft-Skills-reading-note/index.html b/2017/Soft-Skills-reading-note/index.html new file mode 100644 index 0000000000..2dda63ff8b --- /dev/null +++ b/2017/Soft-Skills-reading-note/index.html @@ -0,0 +1,535 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 《软技能:代码之外的生存指南》 读书笔记 | 贾攀的流水账 + + + + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

贾攀的流水账

+ +
+

Panmax's Blog

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+ + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ 《软技能:代码之外的生存指南》 读书笔记 +

+ + +
+ + + + +
+ + +

之前读书大多都囫囵吞枣,雁过无痕,所以这次打算留下点什么,其实我准备写的内容也谈不上什么读书笔记,就是把原文感觉不错的句子记下来,有时再加上一些自己的想法。

+
+

对于大多数软件开发人员来说,生产力都是一场巨大的斗争,也是阻碍你成为成功人事的最大障碍(没有之一)。

+
+

我自己就是一个有着严重拖延症的人,对时间的管理能力很差,作者提到会在后文给出一个解决方法,我们拭目吧,希望可以对我有所帮助。这句话又可以联想到其他一些事情,比如可以提高生产力的生产工具,有了趁手的生产工具(包括软件在内)绝对是可以事半功倍的,还有就是既然我们是程序员,能自动化的地方就不要手动去搞,重复性工作交给机器,我们来做创作性的工作就好。

+
+

只有你开始把自己当作一个企业去思考时,你才能开始做出良好的商业决策。如果你已经习惯领取一份固定的薪酬,这会很容易导致你产生另一个心态 – 你只是在为某家公司打工。

+
+

这里的重点是「把自己当做企业去思考」,把公司作为自己的客户,将自己的地位转为主动,既然你把公司作为了客户,那么你就一定会有其他的潜在客户,所以你就需要学会营销自己。把自己当做一个企业去思考,就需要为自己做一些规划。

+
+

你需要做到:

+
    +
  • 专注你正在提供怎样的服务,以及如何营销这项服务;
  • +
  • 想方设法提升你的服务;思考你可以专注为哪一特定类型的客户或者行业提供特定的服务;
  • +
  • 集中精力成为以为专家,专门为某一特定类型的客户提供专业的整体服务(作为一个软件开发人员,你只有真正专注于一类客户,才能找到非常好的工作)。
  • +
+
+

我也有必要专注特定类型的「客户」。

+
+

要实现任何目标,都必须先知道目标是什么。

+

大目标并不需要这么具体,但是必须足够清晰,能够让你知道自己是在向它前进还是离它越来越远。

+

较小的目标可以让你航行在自己的轨道上,激励你保持航向朝着更大的目标前进。

+
+

这几句话摘抄自 3 个段落。在接触编程 5 年后的我看来,不同领域的编程思想千差万别,可能站在初级程序员的角度就是写一些增删改查操作,但是深入到一定层次后都是针对某个领域来进行编程,这时需要的不光是你的代码能力,还有你对这个领域的理解程度。比如你在金融行业,你就需要知道很多的金融行业的业务和处理流程,大数据行业,也要了解大数据的业务和解决方案。我说这两个行业的原因是因为我上家公司算是一家金融公司,为什么说算是一家金融公司,因为我觉得它的金融属性并不完全,对外提供的产品都是对其他公司的产品进行包装,这样对于程序员来说,很多底层的业务实际上是接触不到的(我单从程序员的角度说一说就够了,毕竟我觉得那是一家不错的公司)。现在我所在的这家公司以大数据业务为主,所以我给自己的目标是成为大数据领域的专家。再写点题外话,很早之前我是有另一个小目标的,就是成为 Python Web 的专家,但是后来越写越发现 Web 这东西就所能接触到的技术层面不会太深,后来也是比较幸运在几乎没有大数据知识的背景下来到现在这家公司,我觉得这算是一种缘分,给了我更高的追求空间。BigData 专家,我来了!

+
+

在软件开发领域,我们大多时候都是与人而非计算机打交道。甚至我们所写的代码首先是供人使用,其次才是让计算机可以理解的。

+
+

这句话里边包含两个涵义,作者写到要与人打交道而非计算机,是要我们提升自己人际交际的能力,毕竟想要成为一个 Leader 这种能力是必不可少的。我最早选择程序员这个职业,天真的认为我只需要和计算机对话就够了,几乎不需要做我不擅长的与人交流这件事,但是后来发现我错了,拿最简单的例子来说,你和产品对需求时,如果你连自己的想法也说不出来,我觉得你在实现功能的时候,也很难按照产品的原意来进行。原文的另一个涵义是,你写的代码是给人读的,所以写代码的时候请遵守一些编码规范、命名规范,让别人读你的代码时谈不上赏心悦目,但不至于心里骂娘。这也是我更喜欢写 Python 而不是 Java 的原因。并且我所认识的大多数 Java 程序员是不在乎代码风格这件事的(我说的是事实,真心不是黑)。

+
+

一但你贬低他人,削弱他们的成就感,在某种程度上就如同切断了他们的氧气补给,获得的回馈将完全是抓狂和绝望的。

+
+

哎呀,鼓励别人这件事我一直学不来怎么办~

+
+

我们常常容易犯的一项错误就是,轻率的否决同事的想法,以便于可以提出自己的想法。然而随着你做出这样的错误判断,你往往会发现他们对你的想法充耳不闻,仅仅因为你让他们感觉自己是无足轻重的。

+
+

我非常厌恶在我还没说完话就否决我的想法然后自己开始高谈阔论的人,那些人不值得去尊敬。大多数时候他们只是想炫耀一下自己的见闻。当然我自己偶尔也会犯这种错误,今后我会努力改正这个缺点,即便对方的想法有再大的不足,我也尽量等对方说完后再来纠正。

+
+

一项又一项的研究表名,奖励积极行为要比惩罚消极行为有效得多。如果你身处管理岗位,这是一条值得遵守的重要原则。如果你想激励他人做出最好的表现,或者希望达到改变的目的,你必须学会管住自己的舌头,只说些鼓励的画。

+
+

我经常在别人犯了错误之后抱怨,完成很漂亮的时候会发出赞赏,但是很少鼓励。以后要管住嘴,减少抱怨。有一句话我特别喜欢,原话我忘记了,大致意思是:不要抱怨别人笨,毕竟他们之前没有你这么优越的条件。

+
+

你可能会害怕专攻软件开发的某个区域,担心自己陷入很窄的专业领域,从而与其他的工作机会绝缘。虽然专业化确实会把你关在一些工作机会的大门之外,但与此同时它将打开的机会大门要比你用其他方式打开的多得多。

+
+

术业有专攻,虽然编程是一大家,但是每个专业领域编写代码时的思考方式是有很大区别的。下文中作者用律师来举例,当我们聘用律师时,如果不傻的话,都会根据我们遇到的官司找这个方向的律师,很少有人聘用通才律师。

+
+

待续。。。

+ +
+ + + + + + + +
+ + + + + + +
+ +
+ + + +
+ + + + + + + + +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2017/blog-redirect-https/1.png b/2017/blog-redirect-https/1.png new file mode 100644 index 0000000000..3858dda12a Binary files /dev/null and b/2017/blog-redirect-https/1.png differ diff --git a/2017/blog-redirect-https/index.html b/2017/blog-redirect-https/index.html new file mode 100644 index 0000000000..5c46303503 --- /dev/null +++ b/2017/blog-redirect-https/index.html @@ -0,0 +1,494 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 让博客重定向到 https 的方法 | 贾攀的流水账 + + + + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

贾攀的流水账

+ +
+

Panmax's Blog

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+ + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ 让博客重定向到 https 的方法 +

+ + +
+ + + + +
+ + +

前段时间将博客在七牛上部署了一份,并且为新的域名 jpanj.com 申请了 SSL 证书,但是发现一个问题,使用 http 请求还是可以访问的,想通过 https 的方式访问,需要手动将地址修改为 https,我想有没有什么办法能在用 http 访问时重定向到 https。

+

所以我开了个工单请教七牛的工作人员,得到的结果是他们也无法做强制跳转。

+

+

之前让 http 请求重定向到 https 的方法是通过 Nginx 的 rewrite 完成的,但是我现在的博客是一个纯静态站点,而且并没有托管在自己的服务器上,所以无法这样操作。

+

今天得到了一个解决方法,是通过修改主题源码来实现的,就我现在用的这个主题来说,layout 目录下所有模板都会继承 _layout.swig,所以我只要在 <head> 标签中加入以下代码即可:

+
1
2
3
4
5
<script type="text/javascript">
var host = "jpanj.com";
if ((host == window.location.host) && (window.location.protocol != "https:"))
window.location.protocol = "https";
</script>
+

我只需要判断 jpanj.com 就可以了,之前的 panmax.love 不做修改。

+ +
+ + + + + + + +
+ + + + + + +
+ +
+ + + +
+ + + + + + + + +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2017/centos-fix-python-mysql/index.html b/2017/centos-fix-python-mysql/index.html new file mode 100644 index 0000000000..61f57d5e31 --- /dev/null +++ b/2017/centos-fix-python-mysql/index.html @@ -0,0 +1,490 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 解决 CentOS 安装 MySQL-python 报错 | 贾攀的流水账 + + + + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

贾攀的流水账

+ +
+

Panmax's Blog

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+ + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ 解决 CentOS 安装 MySQL-python 报错 +

+ + +
+ + + + +
+ + +

今天在服务器上用 pip 安装 MySQL-python 时报错,虽然之前处理过很多次,但都没有记录,这次记录一下。

+

报错如下:

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
Downloading/unpacking MySQL-python
Downloading MySQL-python-1.2.5.zip (108kB): 108kB downloaded
Running setup.py egg_info for package MySQL-python
sh: mysql_config: command not found
Traceback (most recent call last):
File "<string>", line 16, in <module>
File "/home/jiapan/.virtualenvs/appstore-crawler/build/MySQL-python/setup.py", line 17, in <module>
metadata, options = get_config()
File "setup_posix.py", line 43, in get_config
libs = mysql_config("libs_r")
File "setup_posix.py", line 25, in mysql_config
raise EnvironmentError("%s not found" % (mysql_config.path,))
EnvironmentError: mysql_config not found
Complete output from command python setup.py egg_info:
sh: mysql_config: command not found

Traceback (most recent call last):

File "<string>", line 16, in <module>

File "/home/jiapan/.virtualenvs/appstore-crawler/build/MySQL-python/setup.py", line 17, in <module>

metadata, options = get_config()

File "setup_posix.py", line 43, in get_config

libs = mysql_config("libs_r")

File "setup_posix.py", line 25, in mysql_config

raise EnvironmentError("%s not found" % (mysql_config.path,))

EnvironmentError: mysql_config not found
+

解决方法:

+

sudo yum install python-devel mysql-devel

+ +
+ + + + + + + +
+ + + + + + +
+ +
+ + + +
+ + + + + + + + +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2017/config-server-encrypt-and-decrypt/1.png b/2017/config-server-encrypt-and-decrypt/1.png new file mode 100644 index 0000000000..ecbe0d807f Binary files /dev/null and b/2017/config-server-encrypt-and-decrypt/1.png differ diff --git a/2017/config-server-encrypt-and-decrypt/2.png b/2017/config-server-encrypt-and-decrypt/2.png new file mode 100644 index 0000000000..ce375fb451 Binary files /dev/null and b/2017/config-server-encrypt-and-decrypt/2.png differ diff --git a/2017/config-server-encrypt-and-decrypt/3.png b/2017/config-server-encrypt-and-decrypt/3.png new file mode 100644 index 0000000000..54532b36a0 Binary files /dev/null and b/2017/config-server-encrypt-and-decrypt/3.png differ diff --git a/2017/config-server-encrypt-and-decrypt/4.png b/2017/config-server-encrypt-and-decrypt/4.png new file mode 100644 index 0000000000..e66829fb75 Binary files /dev/null and b/2017/config-server-encrypt-and-decrypt/4.png differ diff --git a/2017/config-server-encrypt-and-decrypt/5.png b/2017/config-server-encrypt-and-decrypt/5.png new file mode 100644 index 0000000000..7a70e31d02 Binary files /dev/null and b/2017/config-server-encrypt-and-decrypt/5.png differ diff --git a/2017/config-server-encrypt-and-decrypt/6.png b/2017/config-server-encrypt-and-decrypt/6.png new file mode 100644 index 0000000000..64e7cf49e0 Binary files /dev/null and b/2017/config-server-encrypt-and-decrypt/6.png differ diff --git a/2017/config-server-encrypt-and-decrypt/7.png b/2017/config-server-encrypt-and-decrypt/7.png new file mode 100644 index 0000000000..3d48a34b27 Binary files /dev/null and b/2017/config-server-encrypt-and-decrypt/7.png differ diff --git a/2017/config-server-encrypt-and-decrypt/index.html b/2017/config-server-encrypt-and-decrypt/index.html new file mode 100644 index 0000000000..4a4e1cfba9 --- /dev/null +++ b/2017/config-server-encrypt-and-decrypt/index.html @@ -0,0 +1,520 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 配置中心的加密与解密功能 | 贾攀的流水账 + + + + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

贾攀的流水账

+ +
+

Panmax's Blog

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+ + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ 配置中心的加密与解密功能 +

+ + +
+ + + + +
+ + +

有时候我们会放一些敏感信息到配置中心里,比如线上数据库密码等,我们直接将敏感信息以明文的方式存储于微服务应用的配置文件中是非常危险的,Spring Cloud Config 提供了对属性进行加密解密的功能,以保护配置文件中的信息安全。

+

在 Spring Cloud Config 中通过在属性值前使用 {cipher} 前缀来标注该内容是一个加密值,当微服务客户端加载配置时,配置中心就会自动为带有 {cipher} 前缀的值进行解密。这里有个需要注意的地方,如果配置文件使用的是 yml 格式的话,一定要用引号将内容包裹起来,properites 的配置文件不需要。如:

+
1
2
3
spring:
datasource:
password: '{cipher}c400dd5d44f112518bbf870894e4b8a60fbc64680073aa535d363c28f038bb77'
+

Spring Cloud Config 同时支持对称加密和非对称加密,下边我们只介绍对称加密的使用方式,一般来说只要密钥不被泄露,对称加密的方式就足够了。

+

首先我们访问配置中心的 /encrypt/status 路径,可以看到返回结果提示我们还没有设置密钥,需要我们在配置文件中进行设置。

+

+

在配置中心项目的配置文件 application.yml 中加入以下配置即可(密钥根据根据需要自行修改):

+
1
2
encrypt:
key: my-encrypt-key
+

然后重新编译后运行,再次访问 /encrypt/status 看到如下错误:

+

+

这是因为在 JRE 中,自带的 JCE 默认是有长度限制的版本,我们需要从 Oracle 官网下载不限长度的版本:http://www.oracle.com/technetwork/java/javase/downloads/jce8-download-2133166.html ,下载解压后可以看到下边三个文件:

+

+

我们需要将 local_policy.jarUS_export_policy.jar 两个文件复制到
$JAVA_HOME/jre/lib/security
目录下,在复制前,最好将之前的两个文件进行备份,我将这两个文件放到了 lc0 机器的相应目录中,现在目录中的文件如下:

+

+

重新运行配置中心,访问 /encrypt/status 可以看到密钥已经生效了,并且配置中心已经支持对配置进行加密了。

+

+

此时,我们配置中心的加密解密功能就已经可以使用了,可以访问 /encrypt/decrypt 来使用加密和解密功能。这两个端点都是 POST 请求,我们来用 curl 测试下:

+

+

我们用 my-password 为明文生成了
49db5b628a4b722ef776262d67c0fa9676de7767e54ecfb2be70df5157677d20
这个密文,同时又测试了解密功能。

+

下边我们在实际情景中运用一下加密解密功能

在 GitLab 中修改 app-a 的 dev 配置文件,加上如下配置:

+
1
password: '{cipher}49db5b628a4b722ef776262d67c0fa9676de7767e54ecfb2be70df5157677d20'
+

然后在 app-a 中新加一个 Controller:

+
1
2
3
4
5
6
7
@Value("${password}")
private String password;

@RequestMapping(value = "/password", method = RequestMethod.GET)
public String password() {
return password;
}
+

重新编译后运行,访问它的 /password 路径,可以看到结果:

+

+

我们通过配置 encrypt.key 参数来指定密钥的实现方式采用了对称加密。这种方式实现起来比较简单,只需要配置一个参数即可。另外,我们也可以使用环境变量 ENCRYPT_KEY 来进行配置,让密钥信息外置。

+ +
+ + + + + + + +
+ + + + + + +
+ +
+ + + +
+ + + + + + + + +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2017/cookies-max-age-vs-expires/index.html b/2017/cookies-max-age-vs-expires/index.html new file mode 100644 index 0000000000..19f98161b9 --- /dev/null +++ b/2017/cookies-max-age-vs-expires/index.html @@ -0,0 +1,508 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Http Cookies 中 Max-age 和 Expires 有什么区别? | 贾攀的流水账 + + + + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

贾攀的流水账

+ +
+

Panmax's Blog

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+ + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ Http Cookies 中 Max-age 和 Expires 有什么区别? +

+ + +
+ + + + +
+ + +

快速回答

+
    +
  • Expires 为 Cookie 的删除设置一个过期的日期
  • +
  • Max-age 设置一个 Cookie 将要过期的秒数
  • +
  • IE 浏览器(ie6、ie7 和 ie8) 不支持 max-age,所有的浏览器都支持 expires
  • +
+

深入一些来说明

+

expires 参数是当年网景公司推出 Cookies 原有的一部分。在 HTTP1.1 中,expires 被弃用并且被更加易用的 max-age 所替代。你只需说明这个 Cookie 能够存活多久就可以了,而不用像之前那样指定一个日期。设置二者中的一个,Cookie 会在它过期前一直保存,如果你一个都没有设置,这个 Cookie 将会一直存在直到你关闭浏览器,这种称之为 Session Cookie

+

举个栗子

+

expires 的方式设置 foo=bar 在5分钟后过期

+
1
2
3
var d = new Date();
d.setTime(d.getTime() + 5*60*1000); // in milliseconds
document.cookie = 'foo=bar;path=/;expires='+d.toGMTString()+';';
+

max-age 来做同样的事情

+
1
document.cookie = 'foo=bar;path=/;max-age='+5*60+';';
+

不幸的是,IE 浏览器 不支持 max-age,如果你想跨浏览器存放 Cookie,应该坚持用 expires

+

下边我们来进行几个假设的问答

+

问:如果我在 Cookie 中同时设置了 expiresmax-age 会发生什么?

+

答:所有支持 max-age 的浏览器会忽略 expires 的值,只有 IE 另外,IE 会忽略 max-age 只支持 expires

+

问:如果我只设了 max-age 会怎样?

+

答:除了 IE 之外的所有浏览器会正确的使用它。在 IE 浏览器中,这个 Cookie 将会作为一个 Session Cookie(当你关闭浏览器时它会被删除)。

+

问:如果我只设了 expires

+

答:所有浏览器会正确使用它来保存 Cookie,只需要记得像上边示例那样设置它的 GMT 时间就行了。

+

问:这篇文章的寓意是什么?

+

答:如果你关心你的 Cookies 功能在大多数 Web 用户下正常工作,不要用正确的方式(max-age)存储你的 Cookies,应该用 expires 的方式让他们工作。

+ +
+ + + + + + + +
+ + + + + + +
+ +
+ + + +
+ + + + + + + + +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2017/different-notnull-notempty-notblank/index.html b/2017/different-notnull-notempty-notblank/index.html new file mode 100644 index 0000000000..a7c773f159 --- /dev/null +++ b/2017/different-notnull-notempty-notblank/index.html @@ -0,0 +1,490 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + NotNull、NotEmpty、NotBlank 的区别 | 贾攀的流水账 + + + + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

贾攀的流水账

+ +
+

Panmax's Blog

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+ + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ NotNull、NotEmpty、NotBlank 的区别 +

+ + +
+ + + + +
+ + +

@NotNull

The CharSequence, Collection, Map or Array object is not null, but can be empty.

+

@NotEmpty

The CharSequence, Collection, Map or Array object is not null and size > 0.

+

@NotBlank

The string is not null and the trimmed length is greater than zero.

+

Here are a few examples:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
String name = null;
@NotNull: false
@NotEmpty: false
@NotBlank: false
String name = "";
@NotNull: true
@NotEmpty: false
@NotBlank: false
String name = " ";
@NotNull: true
@NotEmpty: true
@NotBlank: false
String name = "Great answer!";
@NotNull: true
@NotEmpty: true
@NotBlank: true
+ +
+ + + + + + + +
+ + + + + + +
+ +
+ + + +
+ + + + + + + + +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2017/docker-install-rabbitmq/1.png b/2017/docker-install-rabbitmq/1.png new file mode 100644 index 0000000000..0d2d093e43 Binary files /dev/null and b/2017/docker-install-rabbitmq/1.png differ diff --git a/2017/docker-install-rabbitmq/2.png b/2017/docker-install-rabbitmq/2.png new file mode 100644 index 0000000000..c9212830eb Binary files /dev/null and b/2017/docker-install-rabbitmq/2.png differ diff --git a/2017/docker-install-rabbitmq/index.html b/2017/docker-install-rabbitmq/index.html new file mode 100644 index 0000000000..d85ac13035 --- /dev/null +++ b/2017/docker-install-rabbitmq/index.html @@ -0,0 +1,515 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 使用 Docker 部署 RabbitMQ | 贾攀的流水账 + + + + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

贾攀的流水账

+ +
+

Panmax's Blog

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+ + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ 使用 Docker 部署 RabbitMQ +

+ + +
+ + + + +
+ + +

为了更加熟悉我们现在所使用的微服务架构,了解每一个组件的特性,我将部署在 lc0 上的一系列微服务组件(网关、注册中心、配置中心、熔断监控等)尝试重新在 lc7 机器上再部署一遍。

+

在部署配置中心时,需要依赖一个 MQ 组件,项目中用的是 RabbitMQ,所以我需要在 lc7 上安装它。

+

RabbitMQ 是用 Erlang 编写的,直接部署的话需要先部署 Erlang 环境,比较麻烦。在 docker 环境下部署就比较简单了,直接使用 RabbitMQ 官方提供的镜像即可。

+

直接安装的方式可以参考 http://blog.didispace.com/spring-boot-rabbitmq/ 这篇文章,下边我主要来说下如何使用 Docker 部署 RabbitMQ。

+

运行 docker pull rabbitmq:management 从官方下载镜像到本地,这里使用的是带 Web 管理插件的镜像。

+

启动容器:

+
1
2
3
docker run -d --name rabbitmq --publish 5671:5671 \
--publish 5672:5672 --publish 4369:4369 --publish 25672:25672 --publish 15671:15671 --publish 15672:15672 \
rabbitmq:management
+

容器启动之后就可以访问 Web 管理界面了 http://IP:15672

+

+

默认创建了一个 guest 用户,密码也是 guest。

+

+

通过这种方式来部署 RabbitMQ 非常方便,今后可以在部署 测试环境 时用起来,因为我们还没有大规模使用 Docker,所以暂时不建议在 生产环境 来使用。

+

AMQP 协议中的几个重要概念

    +
  • Queue 是 RabbitMQ 的内部对象,用于存储消息。RabbitMQ 中的消息只能存储在 Queue 中,消费者从 Queue 中获取消息并消费。
  • +
  • Exchange 生产者将消息发送到 Exchange,由 Exchange 根据一定的规则将消息路由到一个或多个 Queue 中(或者丢弃)。
  • +
  • Binding RabbitMQ 中通过 Binding 将 Exchange 与 Queue 关联起来。
  • +
  • Binding key 在绑定(Binding) Exchange 与 Queue 的同时,一般会指定一个 binding key。
  • +
  • Routing key 生产者在将消息发送给 Exchange 的时候,一般会指定一个 routing key,来指定这个消息的路由规则。 Exchange 会根据 routing key 和 Exchange Type 以及 Binding key 的匹配情况来决定把消息路由到哪个 Queue。
  • +
  • Exchange Types RabbitMQ 常用的 Exchange Type 有 fanout、 direct、 topic、 headers 这四种。
      +
    • fanout 这种类型的 Exchange 路由规则非常简单,它会把所有发送到该 Exchange 的消息路由到所有与它绑定的 Queue 中,这时 Routing key 不起作用。
    • +
    • direct 这种类型的 Exchange 路由规则也很简单,它会把消息路由到那些 binding key 与 routing key完全匹配的 Queue 中。
    • +
    • topic 这种类型的 Exchange 的路由规则支持 binding key 和 routing key 的模糊匹配,会把消息路由到满足条件的 Queue。 binding key 中可以存在两种特殊字符 与 #,用于做模糊匹配,其中 用于匹配一个单词,# 用于匹配多个单词(可以是零个),单词以 . 为分隔符。
    • +
    • headers 这种类型的 Exchange 不依赖于 routing key 与 binding key 的匹配规则来路由消息,而是根据发送的消息内容中的 headers 属性进行匹配。
    • +
    +
  • +
+ +
+ + + + + + + +
+ + + + + + +
+ +
+ + + +
+ + + + + + + + +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2017/eureka-config-outside/1.png b/2017/eureka-config-outside/1.png new file mode 100644 index 0000000000..2235e63f18 Binary files /dev/null and b/2017/eureka-config-outside/1.png differ diff --git a/2017/eureka-config-outside/2.png b/2017/eureka-config-outside/2.png new file mode 100644 index 0000000000..4f2d470b7f Binary files /dev/null and b/2017/eureka-config-outside/2.png differ diff --git a/2017/eureka-config-outside/index.html b/2017/eureka-config-outside/index.html new file mode 100644 index 0000000000..4669c3a5a3 --- /dev/null +++ b/2017/eureka-config-outside/index.html @@ -0,0 +1,505 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Eureka Server 外置配置文件 | 贾攀的流水账 + + + + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

贾攀的流水账

+ +
+

Panmax's Blog

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+ + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ Eureka Server 外置配置文件 +

+ + +
+ + + + +
+ + +

在我们现在的架构中,一切都是由 Eureka 开始的,因为业务中的配置中心地址是使用 service-id 的形式,从注册中心来自动发现配置中心地址。

+

这就出现了一个矛盾,如果我们想将 Eureka Server 的配置外置的话就不太可行了,因为 Eureka 启动前,配置中心还没有注册进来,所以它也无法发现配置中心。

+

现在我们项目中的做法是:Eureka Server 的所有配置文件都是写在自己的 application.yml 中。

+

今天我想到一个思路,并验证了其可行性:

+

可以先将 Config Server 启动起来,因为 Config Server 需要注册到 Eureka Server 上,但是注册失败并不会导致服务的终止,只是在发心跳包时会有一些错误信息。也就是说,即便不注册上去,配置中心也是可以通过 IP+端口号 的形式来访问的。

+

Config Server 的 application.yml 如下:

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
management:
security:
enabled: false

server:
port: 7020

spring:
application:
name: config-server
cloud:
config:
server:
git:
username: xxxxx
password: yyyyy
uri: http://gitlab-server/config-repo/{application}.git
force-pull: true
label: master

eureka:
client:
service-url:
defaultZone: http://localhost:7011/eureka/
instance:
prefer-ip-address: true
+

然后我们在 Git 中新建 eureka 仓库,并创建 eureka-dev.yml 文件,其内容如下:

+
1
2
3
4
5
6
7
8
9
10
11
server:
port: 7011

eureka:
instance:
hostname: localhost
client:
service-url:
defaultZone: http://localhost:7011/eureka/
register-with-eureka: false
fetch-registry: false
+

然后在 Eureka Server 项目中新建 bootstrap.yml 文件,内容如下:

+
1
2
3
4
5
6
7
8
9
10
spring:
application:
name: eureka
jackson:
default-property-inclusion: non_null
cloud:
config:
label: master
profile: dev
uri: http://localhost:7020/
+

这里我们使用 IP+端口号 的形式来访问配置中心,然后将之前的 application.yml 配置文件删除。

+

现在我们来分别启动这两个项目,注意启动顺序:先启动 Config Server,再启动 Eureka Server,启动完 Config Server 后会看到一些错误,暂时不用理会,启动 Eureka Server 时我们可以在控制台中看到可以正常拉取配置,如图:

+

+

待 Eureka Server 启动完后,再回来看 Config Server 的控制台,已经不报错了,到 http://localhost:7011/ 看到,配置中心也成功注册上来,这也说明我们的 Eureka Server 已经成功读到了 配置中心 提供的配置文件。

+

+

以上验证了这种思路是可行的,回头可以将线上环境也修改为这种方式。

+ +
+ + + + + + + +
+ + + + + + +
+ +
+ + + +
+ + + + + + + + +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2017/exa-and-zsh-syntax-highlighting/1.png b/2017/exa-and-zsh-syntax-highlighting/1.png new file mode 100644 index 0000000000..b67fd32cbc Binary files /dev/null and b/2017/exa-and-zsh-syntax-highlighting/1.png differ diff --git a/2017/exa-and-zsh-syntax-highlighting/2.png b/2017/exa-and-zsh-syntax-highlighting/2.png new file mode 100644 index 0000000000..9becf5af46 Binary files /dev/null and b/2017/exa-and-zsh-syntax-highlighting/2.png differ diff --git a/2017/exa-and-zsh-syntax-highlighting/3.png b/2017/exa-and-zsh-syntax-highlighting/3.png new file mode 100644 index 0000000000..ba7c3acb61 Binary files /dev/null and b/2017/exa-and-zsh-syntax-highlighting/3.png differ diff --git a/2017/exa-and-zsh-syntax-highlighting/4.png b/2017/exa-and-zsh-syntax-highlighting/4.png new file mode 100644 index 0000000000..beceebc41a Binary files /dev/null and b/2017/exa-and-zsh-syntax-highlighting/4.png differ diff --git a/2017/exa-and-zsh-syntax-highlighting/5.png b/2017/exa-and-zsh-syntax-highlighting/5.png new file mode 100644 index 0000000000..15f33cc78d Binary files /dev/null and b/2017/exa-and-zsh-syntax-highlighting/5.png differ diff --git a/2017/exa-and-zsh-syntax-highlighting/index.html b/2017/exa-and-zsh-syntax-highlighting/index.html new file mode 100644 index 0000000000..a2e2410862 --- /dev/null +++ b/2017/exa-and-zsh-syntax-highlighting/index.html @@ -0,0 +1,519 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + exa 和 zsh-syntax-highlighting | 贾攀的流水账 + + + + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

贾攀的流水账

+ +
+

Panmax's Blog

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+ + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ exa 和 zsh-syntax-highlighting +

+ + +
+ + + + +
+ + +

今天再来记录两个命令行神器。

+

第一个是 exa: https://the.exa.website/

+

官方的介绍为

+
+

exa is a modern replacement for ls.

+
+

顾名思义 exa 是一个用来替代 ls 的工具,官方介绍了很多关于 exa 的特性,对于我来说,使用它的原因是可以支持不同文件类型可以用不同颜色来展示这个特性。至于官方还提到,exals 要更快一些,这我倒是没有什么感觉。

+

在 Mac 下直接用 Homebrew 安装就行了: brew install exa,为了方便使用,我直接修改 alias 为 ls,这样之后再使用 ls 命令时,系统就自动用 exa 来代替了,毫无学习成本。

+

+

来看下效果:

+

+

我这个目录下不同类型的文件不多,没有展示出特别好的效果

+

第二个神器是 zsh-syntax-highlighting,看名字就知道,它是一个在 zsh 下使用的工具,官方介绍为:

+
+

Fish shell-like like syntax highlighting for Zsh.

+
+

zsh-syntax-highlighting 是用来在命令行中提供语法高亮的工具 (很抱歉我没有用过 Fish)。

+

效果图:

+

+

安装方法:

+

brew install zsh-syntax-highlighting

+

然后将

+

source /usr/local/share/zsh-syntax-highlighting/zsh-syntax-highlighting.zsh

+

加入到 .zshrc 文件中即可。

+

最后再来说下我现在用的 iTerm2 配色,之前一直用的都是自己配的,会遇到文字和背景色不太搭的情况,比如 Date 和 Modified 那两列:

+

+

所以最近换用了: https://github.com/dracula/iterm 这个配色方案。看起来挺舒服的,所以就不再自己折腾了。

+

+ +
+ + + + + + + +
+ + + + + + +
+ +
+ + + +
+ + + + + + + + +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git "a/2017/gradle-\347\274\226\350\257\221\346\227\266\345\274\272\345\210\266\345\210\267\346\226\260\344\276\235\350\265\226/index.html" "b/2017/gradle-\347\274\226\350\257\221\346\227\266\345\274\272\345\210\266\345\210\267\346\226\260\344\276\235\350\265\226/index.html" new file mode 100644 index 0000000000..e298cc4f2a --- /dev/null +++ "b/2017/gradle-\347\274\226\350\257\221\346\227\266\345\274\272\345\210\266\345\210\267\346\226\260\344\276\235\350\265\226/index.html" @@ -0,0 +1,492 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + gradle 编译时强制刷新依赖 | 贾攀的流水账 + + + + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

贾攀的流水账

+ +
+

Panmax's Blog

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+ + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ gradle 编译时强制刷新依赖 +

+ + +
+ + + + +
+ + +

最近团队封装了个 springbootstarter,用起来很爽,后来优化代码的时候,看到下边的代码中已经指定了 profile

+
1
2
3
4
5
6
7
cloud:
config:
discovery:
enabled: true
service-id: config-server
label: master
profile: ${spring.profiles.active:dev}
+

所以理所当然的认为不需要指定 spring 的 active 了,就把 active 给删掉了(如下):

+
1
2
3
spring:
profiles:
active: dev
+

发布到 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 的时候无条件的重新拉取最新的依赖,但是这个时候为什么没有生效?

+ +
+ + + + + + + +
+ + + + + + +
+ +
+ + + +
+ + + + + + + + +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2017/graph-database-thinkerpop/1.png b/2017/graph-database-thinkerpop/1.png new file mode 100644 index 0000000000..ada4a52160 Binary files /dev/null and b/2017/graph-database-thinkerpop/1.png differ diff --git a/2017/graph-database-thinkerpop/2.png b/2017/graph-database-thinkerpop/2.png new file mode 100644 index 0000000000..2b620ecaf8 Binary files /dev/null and b/2017/graph-database-thinkerpop/2.png differ diff --git a/2017/graph-database-thinkerpop/index.html b/2017/graph-database-thinkerpop/index.html new file mode 100644 index 0000000000..bff45a1f6d --- /dev/null +++ b/2017/graph-database-thinkerpop/index.html @@ -0,0 +1,510 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 图数据库入门:ThinkerPop 介绍 | 贾攀的流水账 + + + + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

贾攀的流水账

+ +
+

Panmax's Blog

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+ + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ 图数据库入门:ThinkerPop 介绍 +

+ + +
+ + + + +
+ + +

Apache TinkerPop 是一个开源的图计算框架。在这其中,TinkerPop 代表了很多的功能和技术,并且在它广阔的生态系统下还另外扩展了第三方贡献图库和系统的世界。TinkerPop 的生态系统对于新手来说可能是复杂的,尤其是第一次浏览参考文档的时候。

+

所以,你要从哪里开始使用 TinkerPop 呢?你如何快速入门并且获得成果?

+

Gremlin,TinkerPop 世界里最知名的公民,让它来帮助你完成入门,之后,你也可以使用 TinkerPop 构建图应用程序了。

+

+

认识 Gremlin

+

Gremlin 可以帮助你浏览一个图中的点和边。他本质上是你用来查询图数据库的语言,就和 SQL 是用来查询关系型数据库的语言一样。为了告诉 Gremlin 他应该如何「遍历」图(也就是你想做的查询)你需要一种方法来用他能明白的语言下达命令,这个语言当然被叫做「Gremlin」。对于这个任务,你需要一个 TinkerPop 的最重要的工具:Gremlin 控制台

+
+

你现在可能还不知道点和边是什么,这会在后文中进行介绍,不过请允许我带你先认识一下 Gremlin 控制台,让你能够了解这个可以帮助你学习体验的工具。

+
+

我们来下载控制台然后解压并启动它:

+
1
2
3
4
5
6
7
8
9
10
11
$ unzip apache-tinkerpop-gremlin-console-3.3.0-bin.zip
$ cd apache-tinkerpop-gremlin-console-3.3.0
$ bin/gremlin.sh

\,,,/
(o o)
-----oOOo-(3)-oOOo-----
plugin activated: tinkerpop.server
plugin activated: tinkerpop.utilities
plugin activated: tinkerpop.tinkergraph
gremlin>
+

Gremlin 控制台是个 REPL 环境,它提供了很 nice 的方式来学习 Gremlin,因为你可以在输入代码后立刻得到反馈。这消除了需要「创建项目」才能尝试的复杂方式。控制台不仅仅是用来「入门」的,你将发现你会使用它来进行和 TinkerPop 相关的各种活动,比如加载数据、管理图、编写复杂的遍历等等。

+

为了让 Gremlin 遍历一个图,你需要一个 Graph 实例,它保存着图的结构和数据。TinkerPop 是不同图数据库和图处理器之上的图抽象层,所以控制台中有很多可以实例化的实例供你选择。开始时最好的 Grahp 实例当然是 TinkerGraph。TinkerGraph 是一个快速、运行于内存的图数据库,有少量配置项,使其成为初学者不错的选择。

+
+

TinkerGraph 不仅仅是提供给初学者的玩具。它在以下几个场景也是非常有用的:分析从大图中取出的子图时,使用不会有太大变化的静态图时,编写单元测试和其他能适应内存的图用例时。

+
+

待续

+ +
+ + + + + + + +
+ + + + + + +
+ +
+ + + +
+ + + + + + + + +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2017/idea-slow-getter-and-getter-setter-public/index.html b/2017/idea-slow-getter-and-getter-setter-public/index.html new file mode 100644 index 0000000000..6695dcf2df --- /dev/null +++ b/2017/idea-slow-getter-and-getter-setter-public/index.html @@ -0,0 +1,491 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 解决 IDEA 启动非常慢和生成 getter setter 不是 public 的问题 | 贾攀的流水账 + + + + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

贾攀的流水账

+ +
+

Panmax's Blog

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+ + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ 解决 IDEA 启动非常慢和生成 getter setter 不是 public 的问题 +

+ + +
+ + + + +
+ + +

今天在用 IDEA 运行 Spring Boot 项目的时候,每次重启都会卡住,过好一会才能恢复,同时 IDEA 底部显示 Finished, saving caches,经过 Google 找到了解决办法,但是不明白为什么这样能解决。

+

方法很简单,修改 hosts 文件,在里边 127.0.0.1::1 后边加上 <hostname>.local,比如我电脑的 hostname 是 panmax,所以我的 host 文件修改完后为

1
2
3
127.0.0.1       localhost   panmax.local
...
::1 localhost panmax.local

+

重启 IDEA,发现已经不会卡顿了。

+

再有一个是我使用 IDEA 生成的 gettersetterprotected 的,我用同事电脑测了一下,他的生成的确是 public 的,经过如下设置改回了正常:

+
1
2
3
4
5
File | Settings | Editor | Code Style | Java
|
Code Generation
|
Default Visibility
+

改为 Public 即可。

+ +
+ + + + + + + +
+ + + + + + +
+ +
+ + + +
+ + + + + + + + +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2017/linux-note/index.html b/2017/linux-note/index.html new file mode 100644 index 0000000000..36f7b75c51 --- /dev/null +++ b/2017/linux-note/index.html @@ -0,0 +1,520 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Linux 笔记 | 贾攀的流水账 + + + + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

贾攀的流水账

+ +
+

Panmax's Blog

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+ + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ Linux 笔记 +

+ + +
+ + + + +
+ + +

添加用户

在添加用户时,最好用 adduser,虽然 adduseruseradd 这两个命令在其他发行版的 Linux 系统下一样,但是在 Ubuntu 下是有区别的:adduser 会自动创建用户的 home 目录,并且创建用户同名的组,而 useradd 不会。

+

如果不小心将用户 home 目录删除了,可以使用下边的方法来重建:

+
1
2
3
sudo mkdir /home/user   # 这里的 /home/user 里的 user 最好改成跟你原来用户名一样 
sudo chown -R user:user /home/user # 这里的 user:user 要改成你之前的“用户名:用户组”的格式
sudo chmod -R 755 /home/user # 这里权限给 755
+

755 是同组的还有别的组的用户可以查看并且可以执行的。如果不想同组的和别的组的用户查看,可以把权限设置为 700。

+

赋予 sudo 权限

新建用户后可能还需要给用户添加 sudo 权限,有两种方法:

+
    +
  1. sudo usermod -aG sudo username
  2. +
  3. 通过修改 /etc/sudoers
  4. +
+

ssh 免密登录

将自己电脑上的公钥内容插入到主机用户 home 目录下的 .ssh/authorized_keys 中,通常新建的用户没有这个目录文件,需要手动创建一下。

+

如果本地没有生成过公钥和私钥,或者想生成新的,可使用 ssh-keygen

+

运行上面的命令后,系统会出现一系列提示,可以一路回车。特别说明,其中有一个问题是,要不要对私钥设置口令(passphrase),如果担心私钥的安全,可以设置一个。运行结束以后,会在 ~/.ssh/ 目录下新生成两个文件:id_rsa.pubid_rsa,前者是公钥,后者是私钥。

+

ubuntu 安装 zsh

查看默认安装了哪些 shell

1
2
3
4
5
6
7
8
jiapan@ubuntu:~$ cat /etc/shells
# /etc/shells: valid login shells
/bin/sh
/bin/dash
/bin/bash
/bin/rbash
/usr/bin/tmux
/usr/bin/screen
+

当前正在运行的是哪个 shell

1
echo $SHELL/bin/bash
+

安装 zsh、git 和 wget

1
2
3
4
5
sudo apt-get install zsh git wget

wget --no-check-certificate https://github.com/robbyrussell/oh-my-zsh/raw/master/tools/install.sh -O - | sh

chsh -s /bin/zsh # 替换 bash 为 zsh
+

Ubuntu 下安装官方 JDK

1
2
3
sudo add-apt-repository ppa:webupd8team/java  # 添加仓库源
sudo apt-get update # 更新软件包列表
sudo apt-get install oracle-java8-installer
+

安装过程中需要接受协议,选择 Yes

+

查看 Java 版本: java -version (我每次都输成 --version)

+

查看修改时区

    +
  1. 查看当前时区
  2. +
+

date -R

+
    +
  1. 修改时区
  2. +
+

tzselect

+
    +
  1. 赋值相应时区文件,替换系统时区文件
  2. +
+

cp /usr/share/zoneinfo/Asia/Shanghai /etc/localtime

+

telent 退出

1
2
3
Control + ]

quit
+

调整 ssh 登录时的提示信息

修改 /etc/update-motd.d/ 下的几个文件就行了。

+

scp 拷贝整个目录

1
scp -r ~/local_dir user@host.com:/var/www/html/target_dir
+

查看 CUP 信息

1
2
3
4
5
6
7
8
9
10
11
# 总核数 = 物理CPU个数 X 每颗物理CPU的核数 
# 总逻辑CPU数 = 物理CPU个数 X 每颗物理CPU的核数 X 超线程数

# 查看物理CPU个数
cat /proc/cpuinfo| grep "physical id"| sort| uniq| wc -l

# 查看每个物理CPU中core的个数(即核数)
cat /proc/cpuinfo| grep "cpu cores"| uniq

# 查看逻辑CPU的个数
cat /proc/cpuinfo| grep "processor"| wc -l
+ +
+ + + + + + + +
+ + + + + + +
+ +
+ + + +
+ + + + + + + + +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2017/my-idea-settings/1.png b/2017/my-idea-settings/1.png new file mode 100644 index 0000000000..fc25555e23 Binary files /dev/null and b/2017/my-idea-settings/1.png differ diff --git a/2017/my-idea-settings/10.png b/2017/my-idea-settings/10.png new file mode 100644 index 0000000000..d07f532f5b Binary files /dev/null and b/2017/my-idea-settings/10.png differ diff --git a/2017/my-idea-settings/2.png b/2017/my-idea-settings/2.png new file mode 100644 index 0000000000..e76210a579 Binary files /dev/null and b/2017/my-idea-settings/2.png differ diff --git a/2017/my-idea-settings/3.png b/2017/my-idea-settings/3.png new file mode 100644 index 0000000000..847c26ce27 Binary files /dev/null and b/2017/my-idea-settings/3.png differ diff --git a/2017/my-idea-settings/4.png b/2017/my-idea-settings/4.png new file mode 100644 index 0000000000..b3b5e08805 Binary files /dev/null and b/2017/my-idea-settings/4.png differ diff --git a/2017/my-idea-settings/5.png b/2017/my-idea-settings/5.png new file mode 100644 index 0000000000..4ba3f5670b Binary files /dev/null and b/2017/my-idea-settings/5.png differ diff --git a/2017/my-idea-settings/6.png b/2017/my-idea-settings/6.png new file mode 100644 index 0000000000..133d87141b Binary files /dev/null and b/2017/my-idea-settings/6.png differ diff --git a/2017/my-idea-settings/7.png b/2017/my-idea-settings/7.png new file mode 100644 index 0000000000..e53ad05103 Binary files /dev/null and b/2017/my-idea-settings/7.png differ diff --git a/2017/my-idea-settings/8.png b/2017/my-idea-settings/8.png new file mode 100644 index 0000000000..076e01f5f0 Binary files /dev/null and b/2017/my-idea-settings/8.png differ diff --git a/2017/my-idea-settings/9.png b/2017/my-idea-settings/9.png new file mode 100644 index 0000000000..6900a836bf Binary files /dev/null and b/2017/my-idea-settings/9.png differ diff --git a/2017/my-idea-settings/index.html b/2017/my-idea-settings/index.html new file mode 100644 index 0000000000..2ef8f5dde8 --- /dev/null +++ b/2017/my-idea-settings/index.html @@ -0,0 +1,524 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 我的 IDEA 配置指南 | 贾攀的流水账 + + + + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

贾攀的流水账

+ +
+

Panmax's Blog

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+ + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ 我的 IDEA 配置指南 +

+ + +
+ + + + +
+ + +
+

本指南理论上适用于 IntelliJ 家的所有产品。

+
+

首先来介绍下我自己定义的一些快捷键

左右分屏 (Extend Selection): Alt + w

写代码时,有时候需要同时打开多份文件,在 IDEA 中有两种分屏方式,一种是上下分,一种是左右分,我觉得上下的方式基本上看不了几行代码,所以我都是使用左右分。默认的快捷键需要用到方向键,但在 HHKB 中使用方向键还需要其他按键组合

+

+

代码补全 (Basic): Control + ,

默认的代码补全快捷键为 Control + Space,但是这个组合被我的 iTerm2 的弹出栏占用了,所以我改为了:Control + ,

+

合并 Git 的修改 (Merge Changes): Alt + m

Fetch 代码 (Fetch): Alt + f

git pull (Pull): Shift + Conmmand + p

因为 push 默认快捷键为 Shift + Command + k,但是 pull 没有默认快捷键,所以我用了这样的组合。

+

然后就是我的一些设置

关闭代码拖拽功能

代码拖拽是我非常不喜欢的功能,经常不小心误操作,如下图,去掉勾即可。

+

+

代码提示不区分大小写

默认的代码补全提示是会区分大小写的,比如我们在 Java 文件中输入 stringBuffer IDEA 是不会帮我们提示的,我们需要输入 StringBuffer 才行。

+

如图所示,将选项改为 None 即可。

+

+

显示内存使用情况

对于我这种 8G 内存的 Mac 用户来说,打开这个功能很有必要性,而且点击内存信息展示的那个条可以进行部分的内存回收

+

+

优化 Java 注释

使用 Command + / 快捷键可以对代码进行注释,IDEA 对 Java 代码的单行注释是把斜杠放在本行最开头,这种注释方式非常丑,所以我修改为将斜杠放在代码之前,并且加一个空格。

+

+

+

小技巧

点击右下角戴帽子的小人,可以选择不同的检查等级,在编辑大文件的时候,可以暂时将等级改为 None,提高流畅性

+
    +
  • Inspections 为最高等级检查,可以检查单词拼写,语法错误,变量使用,方法之间调用等
  • +
  • Syntax 可以检查单词拼写,简单语法错误
  • +
  • None 不设置检查
  • +
+

点击右下角戴帽子的小人,可以看到有一种叫做 Pover Save Mode(省电模式),开启这个模式后 IDEA 会关掉代码检查和提示等功能,可以将这种模式作为一种「阅读模式」来使用。

在展示代码的包时,默认会将一些空的包进行折叠,如果更习惯属性结构的话,可以更具下图来修改的方式进行调整:

+

调整后

+

+

IDEA 默认情况下会把只有一行的代码进行折叠,我不喜欢这样,所以会关掉这个特性:

+ +
+ + + + + + + +
+ + + + + + +
+ +
+ + + +
+ + + + + + + + +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2017/mycli-fzf-thefuck/1.png b/2017/mycli-fzf-thefuck/1.png new file mode 100644 index 0000000000..2879ac6056 Binary files /dev/null and b/2017/mycli-fzf-thefuck/1.png differ diff --git a/2017/mycli-fzf-thefuck/2.png b/2017/mycli-fzf-thefuck/2.png new file mode 100644 index 0000000000..1a5e62545b Binary files /dev/null and b/2017/mycli-fzf-thefuck/2.png differ diff --git a/2017/mycli-fzf-thefuck/3.png b/2017/mycli-fzf-thefuck/3.png new file mode 100644 index 0000000000..e3d0e59c5f Binary files /dev/null and b/2017/mycli-fzf-thefuck/3.png differ diff --git a/2017/mycli-fzf-thefuck/index.html b/2017/mycli-fzf-thefuck/index.html new file mode 100644 index 0000000000..ef6f87f897 --- /dev/null +++ b/2017/mycli-fzf-thefuck/index.html @@ -0,0 +1,498 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + mycli fzf thefuck | 贾攀的流水账 + + + + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

贾攀的流水账

+ +
+

Panmax's Blog

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+ + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ mycli fzf thefuck +

+ + +
+ + + + +
+ + +

今天装了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 还有几种用法我感觉没多少用:

+
1
2
3
4
5
6
7
cd **<TAB>
vim **<TAB>
ssh **<TAB>
telnet **<TAB>
unset **<TAB>
export **<TAB>
unalias **<TAB>
+ +
+ + + + + + + +
+ + + + + + +
+ +
+ + + +
+ + + + + + + + +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2017/neo4j-warms-up/1.png b/2017/neo4j-warms-up/1.png new file mode 100644 index 0000000000..277e34e9af Binary files /dev/null and b/2017/neo4j-warms-up/1.png differ diff --git a/2017/neo4j-warms-up/index.html b/2017/neo4j-warms-up/index.html new file mode 100644 index 0000000000..4313c87558 --- /dev/null +++ b/2017/neo4j-warms-up/index.html @@ -0,0 +1,512 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Neo4j 冷启动预热缓存 | 贾攀的流水账 + + + + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

贾攀的流水账

+ +
+

Panmax's Blog

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+ + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ Neo4j 冷启动预热缓存 +

+ + +
+ + + + +
+ + +

你可能发现有些查询在第二次运行时非常的快,这是因为在冷启动时服务节点中没有任何缓存,需要到硬盘中查找所有的记录。每当部分或全部记录被缓存,你将发现有了很大的性能提升。

+

一种被广泛使用的技术是「缓存预热」,借助这个技术,我们运行一个查询语句来触发图中所有的点和关系。假设内存可以容纳这些数据,整个图会被缓存起来。否则将会缓存尽可能多的数据。尝试一下它是如何给你带来帮助的吧!

+

Cypher(Server,Shell)

+
1
2
3
MATCH (n)
OPTIONAL MATCH (n)-[r]->()
RETURN count(n.prop) + count(r.prop);
+

上边的例子用到了 count(n.prop) + count(r.prop) ,来强制让优化器在点或关系中搜索名为 prop 的属性。用 count(*) 替代它将不够充分,因为这样不会加载所有的点和关系属性。

+

内嵌方式(Java):

+
1
2
3
4
5
6
7
8
9
10
11
12
13
@GET @Path("/warmup")
public String warmUp(@Context GraphDatabaseService db) {
try ( Transaction tx = db.beginTx()) {
for ( Node n : GlobalGraphOperations.at(db).getAllNodes()) {
n.getPropertyKeys();
for ( Relationship relationship : n.getRelationships()) {
relationship.getPropertyKeys();
relationship.getStartNode();
}
}
}
return "Warmed up and ready to go!";
}
+

在 3.0 之后的版本并且使用了 APOC 插件的话,可以运行如下存储过程来完成缓存预热

+

CALL apoc.warmup.run()

+
+

property record loading for warmup, apoc.warmup.run(true)

+
+

CALL apoc.warmup.run() 默认不读取属性记录,更加建议使用 call apoc.warmup.run(true),这个是 3.2.0 以上版本插件的新功能。

+

+

这样做除了纯粹的提升性能外还可以提供更多方面的帮助,如果你使用的是 Neo4j集群的话,还可以帮助缓解由于查询滞后而导致的上游问题。例如,如果节点繁忙并且负载均衡超时时间很短,图中没有任何数据在内存中,很可能会显示该集群最初不可用。如果缓存处于预热状态,那么冷启动应该就不会有短暂超时的问题了。

+

APOC 安装

    +
  1. 将最新的插件 jar 包下载后放进 neo4j 的 plugins 目录中
  2. +
  3. 修改配置文件加入 dbms.security.procedures.unrestricted=apoc.*
  4. +
  5. 重启 neo4j
  6. +
+ +
+ + + + + + + +
+ + + + + + +
+ +
+ + + +
+ + + + + + + + +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2017/optimization-config-server/1.png b/2017/optimization-config-server/1.png new file mode 100644 index 0000000000..aca0540432 Binary files /dev/null and b/2017/optimization-config-server/1.png differ diff --git a/2017/optimization-config-server/2.png b/2017/optimization-config-server/2.png new file mode 100644 index 0000000000..843cf20e99 Binary files /dev/null and b/2017/optimization-config-server/2.png differ diff --git a/2017/optimization-config-server/3.png b/2017/optimization-config-server/3.png new file mode 100644 index 0000000000..7e74a6ca5a Binary files /dev/null and b/2017/optimization-config-server/3.png differ diff --git a/2017/optimization-config-server/index.html b/2017/optimization-config-server/index.html new file mode 100644 index 0000000000..f3770def95 --- /dev/null +++ b/2017/optimization-config-server/index.html @@ -0,0 +1,515 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 对配置中心进行优化 | 贾攀的流水账 + + + + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

贾攀的流水账

+ +
+

Panmax's Blog

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+ + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ 对配置中心进行优化 +

+ + +
+ + + + +
+ + +

现在我们的配置中心使用 Spring Cloud BusSpring Cloud Config 的整合,并以 RabbitMQ 作为消息代理,实现了应用配置的动态更新。

+

架构如下图所示:

+

+

但是,现在的架构有个很大的缺陷,就是每次在修改配置文件后,需要手动地触发下应用的 /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 那台机器刚好出了问题,这个时候其他的实例也就得不到更新了。

+

其实这个时候依赖哪个节点都不合适,谁也不知道哪个节点在什么时候会挂掉,可能有人会想到,我可以给所有节点都发一遍请求,我来分析一下这样做的缺点:

+
    +
  • 第一点是节点很多的时候,你需要配很多 Webhook
  • +
  • 第二点是当每个实例收到消息后,都会通过 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,加入

+
1
2
3
4
5
6
7
8
9
10
11
management:
security:
enabled: false

spring:
...
rabbitmq:
host: 172.24.8.100
port: 5672
username: admin
password: admin
+

然后在 app-a 的 GitLab 仓库中修改我们刚才设置的 Webhook,将地址改为:http://172.24.8.100:7020/bus/refresh?destination=app-a

+

注意端口号已经变了,对应的是配置中心的端口,destination 的值是要刷新的服务名称,这样的话配置其他服务 Webhook 的时候,只需要修改这个名称就可以了。

+

+

通过上面的改动,我们的服务实例就不需要再承担触发配置更新的职责。同时,对于Git的触发等配置都只需要针对 Config Server 即可,从而简化了集群上的一些维护工作。

+

保存 Webhook 后再次修改 GitLab 上 app-a 的配置文件,提交修改后刷新页面看到结果已经变为最新的配置了。

+ +
+ + + + + + + +
+ + + + + + +
+ +
+ + + +
+ + + + + + + + +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2017/redis-open-port-and-set-password/index.html b/2017/redis-open-port-and-set-password/index.html new file mode 100644 index 0000000000..66db6f386b --- /dev/null +++ b/2017/redis-open-port-and-set-password/index.html @@ -0,0 +1,498 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Redis 开放端口与设置密码 | 贾攀的流水账 + + + + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

贾攀的流水账

+ +
+

Panmax's Blog

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+ + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ Redis 开放端口与设置密码 +

+ + +
+ + + + +
+ + +

最近自己的 VPS 上部署了几个 Docker 服务,其中一个打算用 Reids 做个计数的功能,因为我的偏好是数据库类的程序不用 Docker 来部署,所以在本地安装了 Redis 服务,但是这样如果不做任何配置的话 Docker 容器中的服务是访问不到宿主机的 Redis 服务的。

+

从网上搜了下解决方法,都挺复杂的,需要配置网桥之类的,所以我就走了个捷径,直接将 Redis 的端口进行开放,然后设置一个密码:

+

修改 /etc/redis/redis.conf:

+

将里边的 bind 127.0.0.1 改为 bind 0.0.0.0,这样的话 Redis 就可以监听外部请求了。

+

接下来为 Redis 配置一个认证密码:

+

找到 #requirepass foobared 将注释去掉,同时将 foobared 改为自己想设置的密码。

+

修改完后,保存退出,然后重启 Redis 服务:sudo /etc/init.d/redis-server restart

+

这样就完成了,在我本地尝试登录服务器的 Redis:

+

redis-cli -h ipaddress 发现登录成功,发送个命令试试看: keys *,这是会得到:

+

(error) NOAUTH Authentication required.

+

这样的结果,告诉我们没有权限,因为我们设置了访问密码。

+

正确的登录姿势是:redis-cli -h ipaddress -a password

+

同时,Python 程序中连接 Redis 的时候也要记得加上 password 参数。

+ +
+ + + + + + + +
+ + + + + + +
+ +
+ + + +
+ + + + + + + + +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2017/spark-operator-elasticsearch-demo/index.html b/2017/spark-operator-elasticsearch-demo/index.html new file mode 100644 index 0000000000..095a5da78a --- /dev/null +++ b/2017/spark-operator-elasticsearch-demo/index.html @@ -0,0 +1,519 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Spark 操作 Elasticsearch 示例 | 贾攀的流水账 + + + + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

贾攀的流水账

+ +
+

Panmax's Blog

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+ + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ Spark 操作 Elasticsearch 示例 +

+ + +
+ + + + +
+ + +

上周五调研了下如何用 Spark 读写 Elasticsearch(下文简称 es),中间被官方提供的 jar 包卡了很久,所以本来想周末记录一下,结果一发懒就没做,就蹭到周一晚上来写一下了,最近调研的东西很多,有很多要记得东西,一点一点来吧。

+

不废话,直接 Show you the code:

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
import org.apache.spark.{SparkConf, SparkContext}
import org.elasticsearch.spark._


object ElasticSparkHelloWorld {
def main(args: Array[String]) {


val conf = new SparkConf().setAppName(ElasticSparkHelloWorld.getClass.getName)
conf.setMaster("local")
conf.set("es.nodes", "localhost")
conf.set("es.port", "9200")
conf.set("es.index.auto.create", "true")
conf.set("es.nodes.wan.only", "true")
conf.set("es.query", "?q=*")
conf.set("es.resource", "spark/docs")

val sc = new SparkContext(conf)
val numbers = Map("one" -> 1, "two" -> 2, "three" -> 3, "four" -> 4)
val airports = Map("OTP" -> "Otopeni", "SFO" -> "San Fran")

sc.makeRDD(Seq(numbers, airports)).saveToEs("spark/docs")

println(sc.esRDD().count())

}
}
+

其实上边这些代码从网上一搜一大堆,重点是下边 sbt 部分的配置:

+
1
2
3
4
5
6
7
8
9
10
name := "spark-es-demo"

version := "1.0"

scalaVersion := "2.11.11"

//scalacOptions += "-Ylog-classpath"

libraryDependencies += "org.apache.spark" %% "spark-core" % "1.6.2"
libraryDependencies += "org.elasticsearch" % "elasticsearch-spark-13_2.11" % "5.5.2"
+

需要注意 scala 版本,spark 版本还有 es 版本一定要对应,否则无法运行

+

比如

+
    +
  • scalaVersion 版本是 2.11.11
  • +
  • spark 版本是 1.6.2
  • +
  • es 版本是 5.5.2
  • +
+

依赖需要写成下边这样:

+
1
2
libraryDependencies += "org.apache.spark" %% "spark-core" % "1.6.2"  // 这里指定 spark-core 的版本
libraryDependencies += "org.elasticsearch" % "elasticsearch-spark-13_2.11" % "5.5.2"
+

解释一下 "elasticsearch-spark-13_2.11" % "5.5.2" 这部分

+

-13 是给 Spark1.3-1.6 提供的
-20 是给 Spark2.0 提供的

+

_2.11scalaVersion 的前边两位

+

5.5.2elasticsearch 的版本号

+

官方文档中提到

+
+

The Spark connector framework is the most sensitive to version incompatibilities.

+
+
+

Spark 连接器框架是对版本号非常敏感并且不兼容的。

+
+

另外一个坑是,elasticsearch-spark-13_2.11 这个 jar 包所依赖的包无法在 maven 官方源中找到,需要添加另一个源:conjars: http://conjars.org/repo

+

~/.sbt 下新建 repositories 文件,我的 repositories 内容如下:

+
1
2
3
4
5
[repositories]
local
aliyun: http://maven.aliyun.com/nexus/content/groups/public/
conjars: http://conjars.org/repo
central: http://repo1.maven.org/maven2/
+

将阿里源放在上边,可以让官方依赖下载更快。

+

完整代码见:https://github.com/Panmax/spark-es-demo

+ +
+ + + + + + + +
+ + + + + + +
+ +
+ + + +
+ + + + + + + + +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2017/spring-boot-docker/1.png b/2017/spring-boot-docker/1.png new file mode 100644 index 0000000000..e898a272ce Binary files /dev/null and b/2017/spring-boot-docker/1.png differ diff --git a/2017/spring-boot-docker/2.png b/2017/spring-boot-docker/2.png new file mode 100644 index 0000000000..a4194c77ca Binary files /dev/null and b/2017/spring-boot-docker/2.png differ diff --git a/2017/spring-boot-docker/3.png b/2017/spring-boot-docker/3.png new file mode 100644 index 0000000000..1996c3768a Binary files /dev/null and b/2017/spring-boot-docker/3.png differ diff --git a/2017/spring-boot-docker/4.png b/2017/spring-boot-docker/4.png new file mode 100644 index 0000000000..b7dd62b5ff Binary files /dev/null and b/2017/spring-boot-docker/4.png differ diff --git a/2017/spring-boot-docker/5.png b/2017/spring-boot-docker/5.png new file mode 100644 index 0000000000..c216a07d9d Binary files /dev/null and b/2017/spring-boot-docker/5.png differ diff --git a/2017/spring-boot-docker/6.png b/2017/spring-boot-docker/6.png new file mode 100644 index 0000000000..88c3eab947 Binary files /dev/null and b/2017/spring-boot-docker/6.png differ diff --git a/2017/spring-boot-docker/7.png b/2017/spring-boot-docker/7.png new file mode 100644 index 0000000000..f02a5a61cc Binary files /dev/null and b/2017/spring-boot-docker/7.png differ diff --git a/2017/spring-boot-docker/index.html b/2017/spring-boot-docker/index.html new file mode 100644 index 0000000000..02870c4331 --- /dev/null +++ b/2017/spring-boot-docker/index.html @@ -0,0 +1,543 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Spring Boot 与 Docker 结合 | 贾攀的流水账 + + + + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

贾攀的流水账

+ +
+

Panmax's Blog

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+ + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ Spring Boot 与 Docker 结合 +

+ + +
+ + + + +
+ + +
+

Docker 是一个具有社交倾向的 Linux 容器管理工具包,允许用户发布容器镜像,其他用户可以使用这些镜像。Docker 镜像是运行容器化进程的基础,本文将介绍如何编译一个简单的 Spring Boot 应用的镜像。

+
+

Docker 的安装和基本使用不在本文中介绍,之后可以单独拿出来写一写。

+

本文将使用 Gradle 作为编译工具,基础项目工程直接使用 IDEA 的 Spring Initializr 生成,如下图:

+

+

直接下一步,注意这里的 Type 修改为 Gradle Project,然再下一步

+

+

然后只需要勾选 Web 即可

+

+

我们来简单调整一下 build.gradle,新增:

1
2
3
4
jar {
baseName = 'my-spring-boot-docker'
version = '0.1.0'
}
+

它的作用是让编译出来的 jar 包文件名为:my-spring-boot-docker-0.1.0.jar

+

此时 build.gradle 如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
buildscript {
ext {
springBootVersion = '1.5.4.RELEASE'
}
repositories {
mavenCentral()
}
dependencies {
classpath("org.springframework.boot:spring-boot-gradle-plugin:${springBootVersion}")
}
}

apply plugin: 'java'
apply plugin: 'eclipse'
apply plugin: 'org.springframework.boot'


jar {
baseName = 'my-spring-boot-docker'
version = '0.1.0'
}


version = '0.0.1-SNAPSHOT'
sourceCompatibility = 1.8

repositories {
mavenCentral()
}


dependencies {
compile('org.springframework.boot:spring-boot-starter-web')
testCompile('org.springframework.boot:spring-boot-starter-test')
}
+

然后我们写一个最简单的 Controller,为了方便直接写在 main 方法的类中:

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
package com.jpanj;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@SpringBootApplication
@RestController
public class SpringBootDockerApplication {

@RequestMapping("/")
public String home() {
return "Hello Docker World";
}

public static void main(String[] args) {
SpringApplication.run(SpringBootDockerApplication.class, args);
}
}
+

现在我们在不使用 Docker 容器 的情况下运行这个应用:

1
2
./gradlew build
java -jar build/libs/gs-spring-boot-docker-0.1.0.jar
+

然后访问 localhost:8080 可以看到 Hello Docker World 的返回结果。

+

接下来让我们把它容器化吧

Docker 有一个简单的 Dockerfile 文件格式用来指定生成镜像的层次,所以我们在 Spring Boot 项目中创建一个 Dockerfile,将这个文件放在项目根目录下即可,现在这个项目结构是这个样的:

+

+

Dockerfile 内容如下:

1
2
3
4
5
FROM frolvlad/alpine-oraclejdk8:slim
VOLUME /tmp
ADD target/my-spring-boot-docker-0.1.0.jar app.jar
ENV JAVA_OPTS=""
ENTRYPOINT [ "sh", "-c", "java $JAVA_OPTS -Djava.security.egd=file:/dev/./urandom -jar /app.jar" ]
+

这个 Dockerfile 非常简单,不过这就是你运行一个 Spring Boot 应用的全部了,只需要 JavaJAR 文件就够了。这个项目 JAR 文件被作为 app.jar 加入到容器中,然后通过 ENTRYPOINT 来执行它。

+

Docker容器 运行时应该尽量保持容器存储层不发生写操作,在这里我们添加一个 VOLUME 指向 /tmp 是因为 Spring Boot 应用在默认情况下会为 Tomcat 创建工作目录。这里的 /tmp 目录就会在运行时自动挂载为匿名卷,任何向 /tmp 中写入的信息都不会记录进容器存储层,从而保证了容器存储层的无状态化。

+

当然,也可以在运行时可以覆盖这个挂载设置

+

docker run -d -v mytmp:/tmp xxxx

+

在这行命令中,就使用了 mytmp 这个命名卷挂载到了 /tmp 这个位置,替代了 Dockerfile 中定义的匿名卷的挂载配置。

+

不过此步骤在这个简单的应用中是可选的,但是在其他会写入文件系统的 Spring Boot 应用中是必须的。

+

接下来就是把这个项目编译成一个可以到处运行的 Docker 镜像了,在 build.gradle 中我们添加一些新的插件:

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
buildscript {
...
dependencies {
...
classpath('se.transmode.gradle:gradle-docker:1.2')
}
}


...
apply plugin: 'docker'

task buildDocker(type: Docker, dependsOn: build) {
applicationName = jar.baseName
dockerfile = file('Dockerfile')
doFirst {
copy {
from jar
into "${stageDir}/target"
}
}
}
+

上边的配置做了这 3 件事:

    +
  • 镜像的名字被设置为 jar 配置的 baseName 属性
  • +
  • 确定 Dockerfile 的位置
  • +
  • jar 文件从编译目录复制到 docker 编译目录的 target 目录下,这就是我们在 Dockerfile 中看到的 ADD target/my-spring-boot-docker-0.1.0.jar app.jar 为什么会生效的原因。
  • +
+

此时完整的 build.gradle 如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
buildscript {
ext {
springBootVersion = '1.5.4.RELEASE'
}
repositories {
mavenCentral()
}
dependencies {
classpath("org.springframework.boot:spring-boot-gradle-plugin:${springBootVersion}")
classpath('se.transmode.gradle:gradle-docker:1.2')
}
}

apply plugin: 'java'
apply plugin: 'eclipse'
apply plugin: 'org.springframework.boot'
apply plugin: 'docker'


jar {
baseName = 'my-spring-boot-docker'
version = '0.1.0'
}


task buildDocker(type: Docker, dependsOn: build) {
applicationName = jar.baseName
dockerfile = file('Dockerfile')
doFirst {
copy {
from jar
into "${stageDir}/target"
}
}
}


version = '0.0.1-SNAPSHOT'
sourceCompatibility = 1.8

repositories {
mavenCentral()
}


dependencies {
compile('org.springframework.boot:spring-boot-starter-web')
testCompile('org.springframework.boot:spring-boot-starter-test')
}
+

现在你可以使用下边的命令编译这个 docker 镜像,然后将它推送到远端仓库中分享给其他用户使用(本教程不介绍推送远端仓库的方法)。

+

./gradlew build buildDocker

+

执行上边命令后,可以在本地的 docker 镜像仓库中看到,已经有了我们自己编译出来的 my-spring-boot-docker 镜像:

+

+

然后你就可以像这样来运行它了:

docker run -p 8080:8080 -t my-spring-boot-docker:0.0.1-SNAPSHOT

+
+

这里说明一下 -p 的用途, -p 8080:8080 的意思是将本地的 8080 端口映射到容器的 8080 端口,因为容器相对于主机来说是完全隔离的,所以必须要有此设置,不然外部是无法访问到 8080 端口的。

+
+

现在再次访问 localhost:8080 就可以看到 Hello Docker World 啦。

+

当容器运行时你可以通过 docker ps 的命令看到正在运行的容器列表:

+

+

而且可以通过 docker stop 加上这个容器的 ID 来停掉它:

+

+

如果你想删掉这个容器,可以使用 docker rm + 容器 ID

+

到此你已经为 Spring Boot 应用创建了一个 docker 容器,默认运行在容器内的 8080 端口上,我们在命令行中使用 -p 参数将它映射到主机的相同端口上。

+ +
+ + + + + + + +
+ + + + + + +
+ +
+ + + +
+ + + + + + + + +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2017/spring-boot-redis-mi-zhi-bug/index.html b/2017/spring-boot-redis-mi-zhi-bug/index.html new file mode 100644 index 0000000000..449521c159 --- /dev/null +++ b/2017/spring-boot-redis-mi-zhi-bug/index.html @@ -0,0 +1,495 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Spring Boot Redis 蜜汁 Bug | 贾攀的流水账 + + + + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

贾攀的流水账

+ +
+

Panmax's Blog

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+ + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ Spring Boot Redis 蜜汁 Bug +

+ + +
+ + + + +
+ + +

今天遇到一个问题,搞了将近小半天也没有解决,最后我给出来的结论是 Spring Boot Redis Starter 里用的 Jedis 的 Bug 导致。

+

我来描述一下问题过程,今天我为了测试 Spring Boot 和 Redis 相结合,写了一个 Demo 程序,先来连接我本地的 Redis 服务,测试了一下没有问题,然后我想试试连接远程的 Redis 服务,我之前在自己的一台服务器上搭了 Redis 服务,在我的一个正在运行的 Python 项目中用到了这个库,说明服务是没有问题的。并且我已经将这个服务端口监听到了 0.0.0.0,所以外部是可以访问的,唯一的区别是我加了密码验证。我在 application.yml 中配置 hostpassword 后,进行测试,发现报错:

+
1
2
3
4
5
6
7
8
9
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

redis.clients.jedis.exceptions.JedisConnectionException: Unexpected end of stream.
at redis.clients.util.RedisInputStream.ensureFill(RedisInputStream.java:199) ~[jedis-2.9.0.jar:na]
at redis.clients.util.RedisInputStream.readByte(RedisInputStream.java:40) ~[jedis-2.9.0.jar:na]
at redis.clients.jedis.Protocol.process(Protocol.java:151) ~[jedis-2.9.0.jar:na]
at redis.clients.jedis.Protocol.read(Protocol.java:215) ~[jedis-2.9.0.jar:na]
at
...
+

然后我根据网上的给出的方案,尝试修改 spring.redis.pool 相关的各种参数,都没有解决。网上有的说是 Redis 的连接数太高导致的,我看了等下 Redis 的 info 都正常。

+

我在本地用命令 redis-cli -h hostname -a password 也是可以连上的,先不输密码登录成功后用 AUTH + password 的方式也可以访问。

+

因为我自己服务器上的 Redis 在使用中,不方便修改密码,所以我在公司 lc7 的服务器上搭了个 Redis,修改配置监听 0.0.0.0 不过没有设置密码,修改待测试程序的配置文件,发现可以读到,然后又把 lc7 的 Redis 服务加上了密码,再次测试还是没有问题。

+

后来我猜是不是我自己服务器上的密码设置的太长了,我又把 lc7 的密码改为和我自己服务器相同的密码,测试后还是没问题。

+

然后我又对比了下两个机器上安装的 Redis 版本,发现我的服务器上的版本为 2.x,而 lc7 的为 3.x 版本,于是我又冒着风险将我自己服务器的 Redis 进行了升级,升级完先检查了下用到这个 Redis 的其他应用能不能正常工作,检查没有问题后,修改我要测试程序的配置文件来连接这个 Redis,结果还是报那个错误。

+

最后,我冒着自己服务器上所部署的应用暂时不可用的风险,去掉了 Redis 的密码,这时候测试发现没问题了。

+

综上,就是这个 Bug 非常迷的论述。暂时没有找到解决方法。

+ +
+ + + + + + + +
+ + + + + + +
+ +
+ + + +
+ + + + + + + + +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2017/spring-chang-yong-zhu-jie/index.html b/2017/spring-chang-yong-zhu-jie/index.html new file mode 100644 index 0000000000..9059517e53 --- /dev/null +++ b/2017/spring-chang-yong-zhu-jie/index.html @@ -0,0 +1,534 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + SpringMVC 常用注解 | 贾攀的流水账 + + + + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

贾攀的流水账

+ +
+

Panmax's Blog

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+ + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ SpringMVC 常用注解 +

+ + +
+ + + + +
+ + +

@Controller

    +
  • 用于标注控制层组件
  • +
  • @Controller 用于标记在一个类上,使用它标记的类就是一个 SpringMVC Controller 对象,分发处理器将会扫描使用了该注解的类方法,并检测该方法是否使用了 @RequestMapping 注解
  • +
  • 可以把 Request 请求 header 部分的值绑定到方法参数上
  • +
+

@RestController

    +
  • 相当于 @Controller@ResponseBody 的组合效果
  • +
+

@Component

    +
  • 泛指组件,当组件不好归类的时候,我们可以使用这个注解进行标注
  • +
+

@Respository

    +
  • 用于注解 dao 层,在 daoImpl 类上面注解
  • +
+

@Service

    +
  • 用于注解业务组件
  • +
+
+

@ResponseBody

    +
  • 异步请求
  • +
  • 该注解用于将 Controller 的方法返回的对象通过适当的 HttpMessageConverter 转换为指定格式后,写入到 Response 对象的 body 数据区
  • +
  • 返回的数据不是 html 标签的页面,而是其他某种格式的数据时(如json、xml)使用
  • +
+

@RequestMapping

    +
  • 一个用来处理请求地址映射的注解,可用于类或方法上。用于类上,表示类的所有响应请求的方法都是以该地址作为父路径
  • +
+

@Autowired

    +
  • 它可以对类成员变量、方法及构造函数进行标注,完成自动装配的工作。通过 @Autowired 的使用来消除 setget 方法
  • +
+

@PathVariable

    +
  • 用于将请求 URL 中的模板变量映射到功能处理方法的参数上,即取出 URL 模板中的变量作为参数
  • +
+

@RequestParam

    +
  • 主要用于在 Spring MVC 后台控制层获取参数,类似的一种做法是: request.getParamter("name")
  • +
+

@RequestHeader

    +
  • 可以把 request 请求 header 部分的值绑定到方法参数上
  • +
+
+

@SessionAttribute

    +
  • 用来映射 HttpSession 中 attribute 对象的值,将值放到 session 作用域中,写在 class 上面
  • +
+

@Valid

    +
  • 实体校验数据,可结合 hibernate validator 一起使用
  • +
+

@CookieValue

    +
  • 用来获取 Cookies 的值
  • +
+

@ModelAttribute

+
+ + + + + + + +
+ + + + + + +
+ +
+ + + +
+ + + + + + + + +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2017/spring-profiles-active-chuan-tou-xing/index.html b/2017/spring-profiles-active-chuan-tou-xing/index.html new file mode 100644 index 0000000000..12bb0af22c --- /dev/null +++ b/2017/spring-profiles-active-chuan-tou-xing/index.html @@ -0,0 +1,498 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + --spring.profiles.active 穿透性 | 贾攀的流水账 + + + + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

贾攀的流水账

+ +
+

Panmax's Blog

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+ + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ --spring.profiles.active 穿透性 +

+ + +
+ + + + +
+ + +

今天在帮助 datamaster 接入配置中心时发现一个问题,就是如果在应用的 application.yml 像这样:

+
1
2
3
4
5
spring:
application:
name: datamaster-scheduler
profiles:
active: lc10
+

指定环境的话,在启动时会自动寻找 datamaster-scheduler-lc10.yml 这个配置文件,不论使用 bootrun 启动或者打成 jar 包都没有问题。

+

但是如果不在 application.yml 中指定 spring.profiles.active,而是打成 jar 包,使用 --spring.profiles.active=lc10 参数启动的话,就注册不上 Eureka,所以也就没办法正常拉取配置文件了。

+

出现这个问题可以理解,因为我们在封装微服务接入的 starter 时,只定义了两个 active:

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
---
spring:
profiles: dev
rabbitmq:
host: 172.24.8.100
port: 5672
username: admin
password: admin
zipkin:
base-url: http://172.24.8.100:7030

eureka:
client:
service-url:
defaultZone: http://172.24.8.100:7011/eureka/,http://172.24.8.100:7012/eureka/,http://172.24.8.100:7013/eureka/

---
spring:
profiles: beta
rabbitmq:
host: 172.24.8.100
port: 5672
username: admin
password: admin
zipkin:
base-url: http://172.24.8.100:7030
eureka:
client:
service-url:
defaultZone: http://172.24.8.100:7011/eureka/,http://172.24.8.100:7012/eureka/,http://172.24.8.100:7013/eureka/
+

所以使用这两种之外的环境是找不到配置中心的地址的,但是之前那种方式所得到的行为就不太理解了。

+

这个问题我得到的结论是这样的:在应用系统的 application.yml 中定义的 active 是不具有穿透性的,所以我们的 微服务 starter 是不会得到这里定义的 active 的,而且 微服务starter 中的 bootstrap.yml 中定义了:

+
1
2
3
spring:
profiles:
active: dev
+

所以在没有指定时会使用 dev,没有任何问题。

+

使用 --spring.profiles.active=lc10 指定的 active 具有穿透性,会让这个应用系统依赖的其他组件也使用指定的 active,所以这个时候 微服务 starter 也会切换到 lc10 的 active,因为我们没有在 starter 中配 lc10 对应的 active,也就就相当于没有指定 eureka 的注册地址,所以接下来的所有流程就都跑不通了。

+

为了解决这个问题,我在 微服务 starter 的 bootstrap.yml 中加上了默认的 eureka 注册地址和其他需要用到的配置,这样即便是指定的环境不存在的话也没有任何问题。

+

之后在为客户部署时,只需要在 微服务 starter 中增加一个客户所对应的环境,然后启动应用时指定客户的 active 就可以了。

+ +
+ + + + + + + +
+ + + + + + +
+ +
+ + + +
+ + + + + + + + +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2017/the-best-way-to-deploy-jar/1.png b/2017/the-best-way-to-deploy-jar/1.png new file mode 100644 index 0000000000..4e0d104adc Binary files /dev/null and b/2017/the-best-way-to-deploy-jar/1.png differ diff --git a/2017/the-best-way-to-deploy-jar/2.png b/2017/the-best-way-to-deploy-jar/2.png new file mode 100644 index 0000000000..92713a19fa Binary files /dev/null and b/2017/the-best-way-to-deploy-jar/2.png differ diff --git a/2017/the-best-way-to-deploy-jar/3.png b/2017/the-best-way-to-deploy-jar/3.png new file mode 100644 index 0000000000..597759880b Binary files /dev/null and b/2017/the-best-way-to-deploy-jar/3.png differ diff --git a/2017/the-best-way-to-deploy-jar/4.png b/2017/the-best-way-to-deploy-jar/4.png new file mode 100644 index 0000000000..c873effa89 Binary files /dev/null and b/2017/the-best-way-to-deploy-jar/4.png differ diff --git a/2017/the-best-way-to-deploy-jar/index.html b/2017/the-best-way-to-deploy-jar/index.html new file mode 100644 index 0000000000..6b6c785316 --- /dev/null +++ b/2017/the-best-way-to-deploy-jar/index.html @@ -0,0 +1,505 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 部署 jar 包到生产环境的科学方法 | 贾攀的流水账 + + + + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

贾攀的流水账

+ +
+

Panmax's Blog

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+ + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ 部署 jar 包到生产环境的科学方法 +

+ + +
+ + + + +
+ + +

我们现在的线上环境都是简单的使用 nohup java -jar xxx.jar & 的命令来将项目启起来的(感谢 Spring Boot),不过这种方式有诸多不便,比如我们想停掉或者重启某个项目,都需要通过 ps 命令先找到程序对应的 pid,然后再执行 kill 命令,然后再手动启动一遍这个程序。

+

下边我介绍一种(自我认为)比较科学的方式:

+

以我们已有的 demo 项目 app-c 为例,在 build.gradle 中加入:

+
1
2
3
springBoot {
executable = true
}
+

这样可以编译出来可执行的 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

+

这样就可以使用 startstoprestart 来管理我们的应用了

+
1
2
3
4
5
6

/etc/init.d/app-c start|stop|restart



service app-c start|stop|restart
+

并且 status 可以查看运行状态:

+


用这样的方式来管理线上应用,比之前的方式方便了很多:不需要再进到存放 jar 包的目录来用冗长的代码启动,只需知道应用名称,就可以直接启动、重启、停止我们想管理的应用。

+ +
+ + + + + + + +
+ + + + + + +
+ +
+ + + +
+ + + + + + + + +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2017/titan-edge-label-simple-and-one2one/1.png b/2017/titan-edge-label-simple-and-one2one/1.png new file mode 100644 index 0000000000..620bc5244b Binary files /dev/null and b/2017/titan-edge-label-simple-and-one2one/1.png differ diff --git a/2017/titan-edge-label-simple-and-one2one/2.png b/2017/titan-edge-label-simple-and-one2one/2.png new file mode 100644 index 0000000000..408ee4b5a8 Binary files /dev/null and b/2017/titan-edge-label-simple-and-one2one/2.png differ diff --git a/2017/titan-edge-label-simple-and-one2one/index.html b/2017/titan-edge-label-simple-and-one2one/index.html new file mode 100644 index 0000000000..19c530b8ef --- /dev/null +++ b/2017/titan-edge-label-simple-and-one2one/index.html @@ -0,0 +1,500 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Titan 边标签 SIMPLE 和 ONE2ONE 的区别 | 贾攀的流水账 + + + + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

贾攀的流水账

+ +
+

Panmax's Blog

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+ + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ Titan 边标签 SIMPLE 和 ONE2ONE 的区别 +

+ + +
+ + + + +
+ + +

昨天在读 Titan 文档关于边的多样性时看到两个设置,分别是 SIMPLEONE2ONE,这两个设置的介绍有点绕,我琢磨了很久,最终通过程序弄明白了这两种模式的区别。

+

+

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.

+

先给结论,一张图来解释:

+

+

程序验证

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
gremlin> graph = TitanFactory.open("conf/titan.properties")
==>standardtitangraph[cassandra:[172.24.8.84]]
gremlin> mgmt = graph.openManagement()
==>com.thinkaurelius.titan.graphdb.database.management.ManagementSystem@d7109be
gremlin> mgmt.makeEdgeLabel('simple').multiplicity(SIMPLE).make()
==>simple
gremlin> mgmt.makeEdgeLabel('one2one').multiplicity(ONE2ONE).make()
==>one2one
gremlin> mgmt.commit()

gremlin> a = graph.addVertex("name", "a")
==>v[4152]
gremlin> b = graph.addVertex("name", "b")
==>v[8248]
gremlin> c = graph.addVertex("name", "c")
==>v[4128]
gremlin> d = graph.addVertex("name", "d")
==>v[4328]
+

先分别创建 multiplicitySIMPLEONE2ONEEdge Label,然后创建 a b c d 四个点。

+

首先来验证 SIMPLE:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
gremlin> a.addEdge("simple", b)
==>e[1zb-37c-t1-6d4][4152-simple->8248]
gremlin> a.addEdge("simple", b)
An edge with the given label already exists between the pair of vertices and the label [simple] is simple
Display stack trace? [yN] n
gremlin> b.addEdge("simple", a)
==>e[2dj-6d4-t1-37c][8248-simple->4152]
gremlin> a.addEdge("simple", c)
==>e[2rr-37c-t1-36o][4152-simple->4128]
gremlin> a.addEdge("simple", d)
==>e[35z-37c-t1-3c8][4152-simple->4328]
gremlin> a.addEdge("simple", c)
An edge with the given label already exists between the pair of vertices and the label [simple] is simple
Display stack trace? [yN] n
gremlin> c.addEdge("simple", b)
==>e[16s-36o-t1-6d4][4128-simple->8248]
+

得到的结论是,只要两点之间不存在相同方向的 SIMPLE 边就可以。

+

然后验证 ONE2ONE

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
gremlin> a.addEdge("one2one", b)
==>e[3k7-37c-1lh-6d4][4152-one2one->8248]
gremlin> a.addEdge("one2one", c)
An edge with the given label already exists on the out-vertex and the label [one2one] is out-unique
Display stack trace? [yN] n
gremlin> a.addEdge("one2one", d)
An edge with the given label already exists on the out-vertex and the label [one2one] is out-unique
Display stack trace? [yN] n
gremlin> d.addEdge("one2one", a)
==>e[17h-3c8-1lh-37c][4328-one2one->4152]
gremlin> d.addEdge("one2one", b)
An edge with the given label already exists on the out-vertex and the label [one2one] is out-unique
Display stack trace? [yN] n
gremlin> b.addEdge("one2one", c)
==>e[3yf-6d4-1lh-36o][8248-one2one->4128]
+

结论是,一个点上的 ONE2ONE 边只能有一次 in 和一次 out

+ +
+ + + + + + + +
+ + + + + + +
+ +
+ + + +
+ + + + + + + + +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git "a/2017/virtualenvwrapper-\345\256\211\350\243\205-\344\270\216-iPython/index.html" "b/2017/virtualenvwrapper-\345\256\211\350\243\205-\344\270\216-iPython/index.html" new file mode 100644 index 0000000000..2b79787d24 --- /dev/null +++ "b/2017/virtualenvwrapper-\345\256\211\350\243\205-\344\270\216-iPython/index.html" @@ -0,0 +1,514 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + virtualenvwrapper 安装 与 iPython for Python2 | 贾攀的流水账 + + + + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

贾攀的流水账

+ +
+

Panmax's Blog

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+ + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ virtualenvwrapper 安装 与 iPython for Python2 +

+ + +
+ + + + +
+ + +

这篇文章完全是要写两件事:

+
    +
  1. 安装 virtualenvwrapper 后如何配置
  2. +
  3. Python 2 上安装 iPython
  4. +
+

如果分成两篇文章来写的话,每篇文章就会非常短,不值当的,所以直接合成一篇来写。

+
+

配置 virtualenvwrapper

安装 virtualenvwrapper 的过程就不再讲解了,直接 pip install 就可以完成,主要是安装完成后的配置,因为每次我装完都需要问一下谷歌然后才能继续,所以不如记到自己的 Blog 下,即便下次再忘了也能快速找到解决方法。

+

安装完 virtualenvwrapper 后,要根据自己使用的 shell 来配置不同的文件,比如 bash 需要配置 .bashrczsh 配置 .zshrc

+

配置如下:

+
1
2
export WORKON_HOME=$HOME/.virtualenvs
source /usr/local/bin/virtualenvwrapper.sh
+

第一行是给定一个虚拟环境保存的目录,第二行是执行 virtualenvwrapper 的脚本使 workon, mkvirtualenv 等命令生效。

+

大多数时候都卡在第二行那个命令上,因为不同发行版的机器 virtualenvwrapper.sh 所在位置不同,所以需要通过:

+

find / -name virtualenvwrapper.sh

+

找到 virtualenvwrapper.sh 所在的位置后,根据自己机器上的实际位置来写那一行脚本。

+

修改完成后保存退出,重新启动一个命令窗口,检查有没有配置成功。

+
+

在 Python 2 上安装 iPython

最新版的 iPython 已经不支持 Py2 了,所以直接用 pip 安装 iPython 时,会提示安装失败,所以要手动指定安装版本。

+

最后一个支持 Py2 的版本是 5.4.0,所以用 pip install ipython==5.4.0 就行了。

+
+

UPDATE AT 2017-07-19

+
+

今天在一台服务器上将 pip 改为了阿里源后发现安装 ipython==5.4.0 时会报错:

+
1
2
3
4
Running setup.py egg_info for package ipython
error in ipython setup command: Invalid environment marker: sys_platform == "win32" and python_version < "3.6"
Complete output from command python setup.py egg_info:
error in ipython setup command: Invalid environment marker: sys_platform == "win32" and python_version < "3.6"
+

这个问题使用 pip install pip --upgrade 将 pip 更新为最新版本就可以解决了。

+

顺便再记一下 pip 源的地址,虽然知道修改方法,但每次还要去网上搜一下源地址

+

vi ~/.pip/pip.conf

+
1
2
3
[global]
trusted-host = mirrors.aliyun.com
index-url = http://mirrors.aliyun.com/pypi/simple
+ +
+ + + + + + + +
+ + + + + + +
+ +
+ + + +
+ + + + + + + + +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git "a/2017/\343\200\212\345\244\247\350\257\235\346\225\260\346\215\256\347\273\223\346\236\204\343\200\213\351\230\205\350\257\273\347\254\224\350\256\260/1.png" "b/2017/\343\200\212\345\244\247\350\257\235\346\225\260\346\215\256\347\273\223\346\236\204\343\200\213\351\230\205\350\257\273\347\254\224\350\256\260/1.png" new file mode 100644 index 0000000000..2f79d6053d Binary files /dev/null and "b/2017/\343\200\212\345\244\247\350\257\235\346\225\260\346\215\256\347\273\223\346\236\204\343\200\213\351\230\205\350\257\273\347\254\224\350\256\260/1.png" differ diff --git "a/2017/\343\200\212\345\244\247\350\257\235\346\225\260\346\215\256\347\273\223\346\236\204\343\200\213\351\230\205\350\257\273\347\254\224\350\256\260/2.png" "b/2017/\343\200\212\345\244\247\350\257\235\346\225\260\346\215\256\347\273\223\346\236\204\343\200\213\351\230\205\350\257\273\347\254\224\350\256\260/2.png" new file mode 100644 index 0000000000..ab271293ec Binary files /dev/null and "b/2017/\343\200\212\345\244\247\350\257\235\346\225\260\346\215\256\347\273\223\346\236\204\343\200\213\351\230\205\350\257\273\347\254\224\350\256\260/2.png" differ diff --git "a/2017/\343\200\212\345\244\247\350\257\235\346\225\260\346\215\256\347\273\223\346\236\204\343\200\213\351\230\205\350\257\273\347\254\224\350\256\260/3.png" "b/2017/\343\200\212\345\244\247\350\257\235\346\225\260\346\215\256\347\273\223\346\236\204\343\200\213\351\230\205\350\257\273\347\254\224\350\256\260/3.png" new file mode 100644 index 0000000000..1e80174a5f Binary files /dev/null and "b/2017/\343\200\212\345\244\247\350\257\235\346\225\260\346\215\256\347\273\223\346\236\204\343\200\213\351\230\205\350\257\273\347\254\224\350\256\260/3.png" differ diff --git "a/2017/\343\200\212\345\244\247\350\257\235\346\225\260\346\215\256\347\273\223\346\236\204\343\200\213\351\230\205\350\257\273\347\254\224\350\256\260/4.png" "b/2017/\343\200\212\345\244\247\350\257\235\346\225\260\346\215\256\347\273\223\346\236\204\343\200\213\351\230\205\350\257\273\347\254\224\350\256\260/4.png" new file mode 100644 index 0000000000..244c37b8a4 Binary files /dev/null and "b/2017/\343\200\212\345\244\247\350\257\235\346\225\260\346\215\256\347\273\223\346\236\204\343\200\213\351\230\205\350\257\273\347\254\224\350\256\260/4.png" differ diff --git "a/2017/\343\200\212\345\244\247\350\257\235\346\225\260\346\215\256\347\273\223\346\236\204\343\200\213\351\230\205\350\257\273\347\254\224\350\256\260/index.html" "b/2017/\343\200\212\345\244\247\350\257\235\346\225\260\346\215\256\347\273\223\346\236\204\343\200\213\351\230\205\350\257\273\347\254\224\350\256\260/index.html" new file mode 100644 index 0000000000..627985b606 --- /dev/null +++ "b/2017/\343\200\212\345\244\247\350\257\235\346\225\260\346\215\256\347\273\223\346\236\204\343\200\213\351\230\205\350\257\273\347\254\224\350\256\260/index.html" @@ -0,0 +1,633 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 《大话数据结构》阅读笔记 | 贾攀的流水账 + + + + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

贾攀的流水账

+ +
+

Panmax's Blog

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+ + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ 《大话数据结构》阅读笔记 +

+ + +
+ + + + +
+ + +
+

update at 2017-04-12

+
+

逻辑结构:指数据对象中数据元素之间的相互关系

+
1. 集合结构
+2. 线性结构
+3. 树形结构
+4. 图形结构
+

物理结构:指数据的逻辑结构在计算机中的存储形式

+
1. 顺序存储结构
+2. 链式存储结构
+

逻辑结构是面相问题的,物理结构是面相计算机的,基本目标就是将数据及其逻辑关系存储到计算机的内存中。

+
+
+

update at 2017-04-13

+
+

算法具有五个基本特性:输入、输出、有穷性、确定性和可行性

+

好的算法应该具有正确性、可读性、健壮性、高效率和低存储量的特征

+

判断一个算法的效率时,函数中的常数和其他次要项常常可以忽略,而更应该关注主项(最高阶项)的阶数

+
+
+

update at 2017-04-17

+
+

推到大 O 阶:

+
    +
  1. 用常数 1 取代运行时间中的所有加法常数。
  2. +
  3. 在修改后的运行次数函数中,只保留最高阶项。
  4. +
  5. 如果最高阶项存在且不是 1,则去除与这个项相乘的常数。
  6. +
+

得到的结果就是大 O 阶。

+
+
+

update at 2017-04-18

+
+

线性表顺序存储结构需要三个属性:

+
    +
  1. 存储空间的起始位置
  2. +
  3. 线性表的最大存储容量
  4. +
  5. 线性表的当前长度
  6. +
+

线性表顺序存储结构的优缺点:

+

优点:

+
    +
  • 无需为表示表中元素之间的逻辑关系而增加额外的存储空间
  • +
  • 可以快速地存取表中任一位置的元素
  • +
+

缺点:

+
    +
  • 插入和删除操作需要移动大量元素
  • +
  • 当线性表长度变化较大时,难以确定存储空间的容量
  • +
  • 造成存储空间的碎片
  • +
+
+
+

update at 2017-04-19

+
+

链式结构

+

为了表示每个数据元素 ai 与其直接后继数据元素 ai+1 之间的逻辑关系,对数据元素 ai 来说,除了存储其本身的信息之外,还需存储一个指示其直接后继的信息(即直接后继的存储位置)

+

把存储数据元素信息的域称为数据域,把存储直接后继位置的域成为指针域。指针域中存储的信息称做指针或链。这两部分信息组成数据元素 ai 的存储映像,称为结点(Node)

+

链表中第一个节点的存储位置叫做头指针

+

头指针

+
    +
  • 头指针是指链表指向第一个节点的指针,若链表有头结点,则是指向投结点的指针
  • +
  • 头指针具有标识作用,所以常用头指针冠以链表的名字
  • +
  • 无论链表是否为空,头指针均不为空。头指针是链表的必要元素
  • +
+

头结点

+
    +
  • 头结点是为了操作的统一和方便而设立的,放在第一元素的结点之前,其数据域一般无意义(也可以存放链表的长度)
  • +
  • 有了头结点,对在第一元素结点前插入结点和删除第一节点,其操作与其他节点的操作就统一了
  • +
  • 头结点不一定是链表必须要素
  • +
+
+
+

update at 2017-04-20

+
+

3.11 顺序结构与单链表结构优缺点

存储分配方式

+
    +
  • 顺序存储结构用一段连续的存储单元依次存储线性表的数据元素
  • +
  • 单链表采用链式存储结构,用一组任意的存储单元存放线性表的元素
  • +
+

时间性能

+

查找

+
    +
  • 顺序存储结构O(1)
  • +
  • 单链表O(n)
  • +
+

插入和删除

+
    +
  • 顺序存储结构需要平均移动表长一半的元素,时间为O(n)
  • +
  • 单链表在找出某位置的指针后,插入和删除时间为O(1)
  • +
+

空间性能

+
    +
  • 顺序存储结构需预分配存储空间,分大了浪费,分小了容易发生上溢
  • +
  • 单链表不需要分配存储空间,只要有就可以分配,元素个数也不受限制
  • +
+

结论

+
    +
  • 若线性表需要频繁查找,很少进行插入和删除操作时,宜采用顺序存储结构。若需要频繁插入和删除时,宜采用单链表结构
  • +
  • 当线性表中的元素个数变化较大或者根本不知道有多大时,最好用单链表结构,这样可以不需要考虑存储空间的大小问题。而如果事先知道线性表的大致长度,用顺序存储结构效率会高很多
  • +
+

3.14 双向链表

线性表的双向链表存储结构

+
1
2
3
4
5
6
typedef struct DulNode
{
ElemType data;
struct DuLNode *prior;
struct DuLNode *next;
} DulNode, *DuLinkList;
+

双向链表插入元素

+

+
1
2
3
4
s -> prior = p
s -> next = p -> next
p -> next -> prior = s
p -> next = s
+

双向链表删除元素

+

+
1
2
3
p -> prior -> next = p -> next;
p -> next -> prior = p -> prior
free(p);
+

3.15 回顾总结

线性表的两种结构

+

+

4.2 栈的定义

+

栈(stack)是限定仅在表尾进行插入和删除操作的线性表。

+
+

把允许插入和删除的一端称为栈顶(top),另一端称为栈底(bottom)

+

不含任何数据元素的栈称为空栈,栈又称为后进先出(Last In First Out)的线性表,简称 FIFO 结构。

+

栈的插入操作叫作进栈,也称压栈、入栈。

+

栈的删除操作叫做出栈。

+

4.9 栈的应用 – 四则运算表达式求值

中缀表达式转后缀表达式:

+

如:9+(3-1)3+10/2 -> 9 3 1 - 3 + 10 2 / +

+

规则:从左到右遍历中缀表达式的每个数字和符号,若是数字就输出,即成为后缀表达式的一部分;若是符号,则判断其与栈顶符号的优先级,是有括号或优先级低于栈顶符号(乘除优先加减)则栈顶元素依次出栈并输出,并将当前符号进栈,一直到最终输出后缀表达式为止。

+

4.10 队列的定义

+

队列(queue)是只允许在一段进行插入操作,而在另一端进行删除操作的线性表。

+
+

队列是一种先进先出(First In First Out)的线性表,简称 FIFO。允许插入的一端称为队尾,允许删除的一端称为队头。

+

4.12.2 循环队列定义

循环队列满时没我们有两种办法来判断:

+
    +
  • 办法一是设置一个标志变量 flag,当 front == rear,且 flag == 0 时为队列空,当 front == rear,且 flag = 1 时为队列满。
  • +
  • 办法二是当前队列空时,条件就是 front = rear,当队列满时,我们修改其条件,保留一个元素空间。也就是说队列满时,数组中还有一个空闲单元。如图所示,我们就认为此队列已经满了。
  • +
+

+

第二种方法,队列满的条件是 (rear+1) % QueueSize == front

+

计算队列长度公式:(rear-front+Queue) % QueueSize

+

4.14 总结回顾

对于栈来说,如果是两个相同数据类型的栈,则可以用数组的两端作栈底的方法来让两个栈共享数据,这就可以最大化地利用数组的空间。

+

对于队列来说,为了避免数组插入和删除时需要移动数据,于是就引入了循环队列,使得队头和队尾可以在数组中循环变化。解决了移动数据的时间损耗,使得本来插入和删除时间是 O(n) 的时间复杂度变成了 O(1)。

+

他们也都可以通过链式存数结构来实现。

+

5.2 串的定义

串(string)是由零个或多个字符组成的有限序列,又名叫字符串。

+ +
+ + + + + + + +
+ + + + + + +
+ +
+ + + +
+ + + + + + + + +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git "a/2017/\344\274\230\345\214\226blog\351\200\237\345\272\246/1.png" "b/2017/\344\274\230\345\214\226blog\351\200\237\345\272\246/1.png" new file mode 100644 index 0000000000..74591a07db Binary files /dev/null and "b/2017/\344\274\230\345\214\226blog\351\200\237\345\272\246/1.png" differ diff --git "a/2017/\344\274\230\345\214\226blog\351\200\237\345\272\246/2.png" "b/2017/\344\274\230\345\214\226blog\351\200\237\345\272\246/2.png" new file mode 100644 index 0000000000..440787082e Binary files /dev/null and "b/2017/\344\274\230\345\214\226blog\351\200\237\345\272\246/2.png" differ diff --git "a/2017/\344\274\230\345\214\226blog\351\200\237\345\272\246/index.html" "b/2017/\344\274\230\345\214\226blog\351\200\237\345\272\246/index.html" new file mode 100644 index 0000000000..afe28b77c1 --- /dev/null +++ "b/2017/\344\274\230\345\214\226blog\351\200\237\345\272\246/index.html" @@ -0,0 +1,506 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 优化 blog 速度 | 贾攀的流水账 + + + + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

贾攀的流水账

+ +
+

Panmax's Blog

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+ + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ 优化 blog 速度 +

+ + +
+ + + + +
+ + +

之前已经做过对 Blog 静态资源的优化,但是都没有进行记录,今天又做了一个比较实用的优化,赶紧记一下。

+

我打开 Chrome 的调试器进入站点时,看到 Network 里有一条访问 fonts.gstatic.com,这个请求应该是访问的 谷歌 CDN,但是国内的访问速度是非常不友好的,由于我平时都是开着 Surge 所以没什么感觉。

+

这个资源是在由主题渲染成静态站点时插入进来的,因为我用的是自己修改的 Next 模板,所以在 themes/next 目录下查找带 fonts.gstatic.com 关键字的文件竟然没有找到,很是费解。后来我又看那条请求,发现它的 refererfonts.googleapis.com,所以我又尝试用这个关键字进行查找,最后在 themes/next/layout/_partials/head/external-fonts.swig 找到了它。

+
1
2
3
4
5
{% if font_families !== '' %}
{% set font_families += '&subset=latin,latin-ext' %}
{% set font_host = font_config.host | default('//fonts.googleapis.com') %}
<link href="{{ font_host }}/css?family={{ font_families }}" rel="stylesheet" type="text/css">
{% endif %}
+

然后我在网上找了找 fonts.googleapis.com 国内镜像,有两个用的比较多的:360网站卫士 和 中科大。但是一些地方写到 360网站卫士 提供的源不支持 HTTPS,虽然我的博客现在并不是 HTTPS 的,但保不齐以后我要改呢。所以我选择使用中科大镜像。只需用 fonts.lug.ustc.edu.cn 替代之即可。然后再 hexo g 重新生成下站点就可以了。

+

下边是几个常用的替代镜像:

+
    +
  1. ajax.googleapis.com => ajax.lug.ustc.edu.cn
  2. +
  3. fonts.googleapis.com => fonts.lug.ustc.edu.cn
  4. +
  5. themes.googleusercontent.com => google-themes.lug.ustc.edu.cn
  6. +
+
+

updateAt: 2017-06-20

然鹅,在我用了一段时间后,发现中科大的源速度我也不满意,最终将那个 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

+ +
+ + + + + + + +
+ + + + + + +
+ +
+ + + +
+ + + + + + + + +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git "a/2017/\344\275\277\347\224\250-KM-\345\244\204\347\220\206-HHKB-\346\226\271\345\220\221\351\224\256/1.png" "b/2017/\344\275\277\347\224\250-KM-\345\244\204\347\220\206-HHKB-\346\226\271\345\220\221\351\224\256/1.png" new file mode 100644 index 0000000000..ad74b4821e Binary files /dev/null and "b/2017/\344\275\277\347\224\250-KM-\345\244\204\347\220\206-HHKB-\346\226\271\345\220\221\351\224\256/1.png" differ diff --git "a/2017/\344\275\277\347\224\250-KM-\345\244\204\347\220\206-HHKB-\346\226\271\345\220\221\351\224\256/index.html" "b/2017/\344\275\277\347\224\250-KM-\345\244\204\347\220\206-HHKB-\346\226\271\345\220\221\351\224\256/index.html" new file mode 100644 index 0000000000..e2db867242 --- /dev/null +++ "b/2017/\344\275\277\347\224\250-KM-\345\244\204\347\220\206-HHKB-\346\226\271\345\220\221\351\224\256/index.html" @@ -0,0 +1,498 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 使用 KM 处理 HHKB 方向键 | 贾攀的流水账 + + + + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

贾攀的流水账

+ +
+

Panmax's Blog

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+ + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ 使用 KM 处理 HHKB 方向键 +

+ + +
+ + + + +
+ + +

对于上了 HHKB 这条贼船的人来说,刚开始使用起来最大的别扭可能就是没有方向键的问题了。

+

最早的我使用 Karabiner 来解决,里边有一些内置的组合可以替代方向键,我用 control + hjkl(同vi) 替代四个方向键,因为 HHKB 的 control 在 caps lock 的位置,所以使用起来还是很舒服的,But 当系统升级到 macOS Sierra 后,Karabiner 就不能工作了,作者也在官网中写了:

+
+

Karabiner does not work on macOS Sierra at the moment.

+
+

同时也给出了替代方案,使用 Karabiner-Elements,但是新版的 Karabiner 并不支持这样的组合,所以我就又走上了寻找解决方向键之路。

+

后来找到了 Keyboard Maestro(简称 KM) 这个神器,这个软功能非常多,不过我只用了里边的设置组合键的功能,我自定义了 5 个组合,用来解决 HHKB 中的不方便的方向键问题。

+

分别是 control + hjkl 来操作方向和 control + delete 来反向删除(也就是删除光标后边的内容),但是用起来有些问题:不能连击(比如按住 control + h 光标不可以一直前移,需要手动敲击多次),然鹅就在我将就用了小一年后,今天尝试将触发方式 is pressed 改成了 is down,成功解决了不能连击的问题,所以 HHKB 方向键 的问题现在可以说是完美解决了。

+

不过我现在并没有将 Karabiner-Elements 删掉,因为里边有一个比较实用的功能:可以在插入外置键盘时禁用内置键盘的功能,防止意外点击。因为我之前是把 HHKB 垫到 Mac 上用的,经常不小心按住了某个键,现在我把 Mac 放在了支架上,已经不会再出现这个问题了,不过我还是留下了它。

+

最后上一张设置截图(我将 KM 中自带的其他组合全都关闭了,只留下 5 个我自己写的组合):

+

+ +
+ + + + + + + +
+ + + + + + +
+ +
+ + + +
+ + + + + + + + +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git "a/2017/\344\275\277\347\224\250-spring-boot-starter-security-\351\233\206\346\210\220-CAS-\351\201\207\345\210\260\347\232\204\351\227\256\351\242\230/index.html" "b/2017/\344\275\277\347\224\250-spring-boot-starter-security-\351\233\206\346\210\220-CAS-\351\201\207\345\210\260\347\232\204\351\227\256\351\242\230/index.html" new file mode 100644 index 0000000000..8d592e2b43 --- /dev/null +++ "b/2017/\344\275\277\347\224\250-spring-boot-starter-security-\351\233\206\346\210\220-CAS-\351\201\207\345\210\260\347\232\204\351\227\256\351\242\230/index.html" @@ -0,0 +1,488 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 使用 spring-boot-starter-security 集成 CAS 遇到的问题 | 贾攀的流水账 + + + + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

贾攀的流水账

+ +
+

Panmax's Blog

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+ + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ 使用 spring-boot-starter-security 集成 CAS 遇到的问题 +

+ + +
+ + + + +
+ + +

根据这篇教程: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(); 就行了。

+ +
+ + + + + + + +
+ + + + + + +
+ +
+ + + +
+ + + + + + + + +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git "a/2017/\345\205\263\344\272\216\346\203\205\346\231\257\347\232\204\344\270\200\347\202\271\347\202\271\346\200\235\350\200\203/index.html" "b/2017/\345\205\263\344\272\216\346\203\205\346\231\257\347\232\204\344\270\200\347\202\271\347\202\271\346\200\235\350\200\203/index.html" new file mode 100644 index 0000000000..688d1025a6 --- /dev/null +++ "b/2017/\345\205\263\344\272\216\346\203\205\346\231\257\347\232\204\344\270\200\347\202\271\347\202\271\346\200\235\350\200\203/index.html" @@ -0,0 +1,489 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 关于情景的一点点思考 | 贾攀的流水账 + + + + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

贾攀的流水账

+ +
+

Panmax's Blog

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+ + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ 关于情景的一点点思考 +

+ + +
+ + + + +
+ + +

这段时间的工作内容,让我更加体会到「情景」的重要性。

+

想把事情做好,就要有一个已经存在的情景设定,空穴来潮地去做一定做不完美,或者做着做着会失去动力。
这段时间学习了很多图数据库知识,接触了一下Titan,深入学习了 Neo4j 。用 Hadoop/Spark 写了一些大数据处理工具。

+

因为有上亿级的数据需要处理,所以不得不使用分布式计算引擎;为了将上亿级的数据导入到图数据库,不得不使用 Neo4j 的初始化导入工具;为了将一些增量关系通过计算后追加到图中,不得不学习通过编码的导入方式;为了满足产品需求,不得不学习各种复杂的查询语句,不断 debug 语句的正确性;因为有上亿数据的存在,不得不学习如果优化语句性能,在合适的地方添加索引。以上这些都是在工作内容这个场景下进行的,因为有了这个场景,推动着我不断的探索和进步,如果没有这个场景,我就算会去自己学习,也不会学的这么深入。

+

还可以从另一个方面说一下场景的重要性:如果你想开发一个软件/工具来让大家用,也需要一个场景,要么当下你用得到,要么你的家人或者朋友用得到,否则我觉得空想是做不来的。

+ +
+ + + + + + + +
+ + + + + + +
+ +
+ + + +
+ + + + + + + + +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git "a/2017/\345\215\225\347\202\271\347\231\273\345\275\225\346\265\201\347\250\213\346\242\263\347\220\206/1.png" "b/2017/\345\215\225\347\202\271\347\231\273\345\275\225\346\265\201\347\250\213\346\242\263\347\220\206/1.png" new file mode 100644 index 0000000000..47dbd0ff5d Binary files /dev/null and "b/2017/\345\215\225\347\202\271\347\231\273\345\275\225\346\265\201\347\250\213\346\242\263\347\220\206/1.png" differ diff --git "a/2017/\345\215\225\347\202\271\347\231\273\345\275\225\346\265\201\347\250\213\346\242\263\347\220\206/2.png" "b/2017/\345\215\225\347\202\271\347\231\273\345\275\225\346\265\201\347\250\213\346\242\263\347\220\206/2.png" new file mode 100644 index 0000000000..65e42f877d Binary files /dev/null and "b/2017/\345\215\225\347\202\271\347\231\273\345\275\225\346\265\201\347\250\213\346\242\263\347\220\206/2.png" differ diff --git "a/2017/\345\215\225\347\202\271\347\231\273\345\275\225\346\265\201\347\250\213\346\242\263\347\220\206/3.png" "b/2017/\345\215\225\347\202\271\347\231\273\345\275\225\346\265\201\347\250\213\346\242\263\347\220\206/3.png" new file mode 100644 index 0000000000..fe0e31af9c Binary files /dev/null and "b/2017/\345\215\225\347\202\271\347\231\273\345\275\225\346\265\201\347\250\213\346\242\263\347\220\206/3.png" differ diff --git "a/2017/\345\215\225\347\202\271\347\231\273\345\275\225\346\265\201\347\250\213\346\242\263\347\220\206/4.png" "b/2017/\345\215\225\347\202\271\347\231\273\345\275\225\346\265\201\347\250\213\346\242\263\347\220\206/4.png" new file mode 100644 index 0000000000..13ce35dc8a Binary files /dev/null and "b/2017/\345\215\225\347\202\271\347\231\273\345\275\225\346\265\201\347\250\213\346\242\263\347\220\206/4.png" differ diff --git "a/2017/\345\215\225\347\202\271\347\231\273\345\275\225\346\265\201\347\250\213\346\242\263\347\220\206/5.png" "b/2017/\345\215\225\347\202\271\347\231\273\345\275\225\346\265\201\347\250\213\346\242\263\347\220\206/5.png" new file mode 100644 index 0000000000..ec69c46806 Binary files /dev/null and "b/2017/\345\215\225\347\202\271\347\231\273\345\275\225\346\265\201\347\250\213\346\242\263\347\220\206/5.png" differ diff --git "a/2017/\345\215\225\347\202\271\347\231\273\345\275\225\346\265\201\347\250\213\346\242\263\347\220\206/index.html" "b/2017/\345\215\225\347\202\271\347\231\273\345\275\225\346\265\201\347\250\213\346\242\263\347\220\206/index.html" new file mode 100644 index 0000000000..2c848a77a5 --- /dev/null +++ "b/2017/\345\215\225\347\202\271\347\231\273\345\275\225\346\265\201\347\250\213\346\242\263\347\220\206/index.html" @@ -0,0 +1,529 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 单点登录流程梳理 | 贾攀的流水账 + + + + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

贾攀的流水账

+ +
+

Panmax's Blog

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+ + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ 单点登录流程梳理 +

+ + +
+ + + + +
+ + +

之前研究了一段时间的单点登录系统,在这里做一下流程上的总结吧。

+

先说下我对几个词的认识:我觉得 统一认证、单点登录、集中认证、统一登录 这几个词的想表达的目的都是一样的,都是提供一个登录中心或者叫认证中心的地方,当某个系统需要用户进行登录时,统一跳转到这里来进行处理。

+

进入正文:

+

假定一个场景,现在有系统A(a.com)、系统B(b.com)、和认证中心(sso.com)。我们想实现的效果是,其中一个系统登录一次后,访问其他系统的需要登录页面时无需再次手动提交帐号密码。

+

注意:我这里说的是无需再次输入帐号密码,内部的登录流程还是要执行的,只是不需要用户的参与。

+

下边是基于 CAS 的 SSO 的流程介绍:

+

用户通过浏览器访问系统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 访问认证中心,认证中心一看没登录过,就会展示一个登录框让用户去登录,登录成功以后,认证中心要做几件重要的事情:

+
    +
  1. 建立一个 session
  2. +
  3. 创建一个 ticket (可以认为是个随机字符串)
  4. +
  5. 重定向到系统A,同时把 ticket 放在 url 中: www.a.com/pageA?ticket=T123 与此同时之前建立 session 对应的 cookie 也会发送给浏览器,比如: Set cookie : ssoid=1234, sso.com
  6. +
+

到这里会产生一个疑惑,为什么认证中心要写一个 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,就知道它登录过了,不需要再次到认证中心去了。

+

+

接下来看看,当用户访问系统A时已经通过认证中心登录了,再访问系统B www.b.com/pageB 时是什么样的情况。

+

其实和首次访问 www.a.com/pageA 非常类似,唯一不同就是不需要用户输入用户名密码来登录了,因为浏览器已经有了认证中心的 cookie,直接发送给 www.sso.com 就可以了。这里解释了我上边提到的认证中心写入浏览器 cookie 的用途。

+

+

同样,认证中心会返回 ticket,系统B需要做验证。

+

+

整个流程的本质是一个认证中心的 cookie,加上多个子系统的 cookie 而已。

+

下边来说说单点退出的原理。

+

单点退出的作用是用户在一个地方退出,所有系统都要进行退出。这怎么来实现呢。还记得我前边提到的注册服务吗?没错,就是使用之前登录时给认证中心传的 service 参数,认证中心记录下来都有哪些系统进行过登录,当用户访问认证中心的 /logout 需要退出的时候,认证中心需要把自己的会话和 cookie 干掉,然后给之前注册过那些服务的地址发送退出登录的请求,默认是对根路径发一个 POST 请求,Body 中携带一些字段,比如比如之前登录时用到的ticket,这时候各个子系统根据传过来的这个 ticket 来将对应的用户 session 干掉即可。

+

所以用户在系统A点击退出登录后,系统A取消本地会话然后重定向到认证中心的退出登录地址,剩下的交给认证中心来处理就好了。这里也可以传一个回跳地址参数,当认证中心完成退出后,可以再跳会到设置的地址。

+
+

我觉得用户在一个系统中退出登录时,系统此时结不结束会话其实都可以,因为最终还是要被认证中心调用一次退出。

+
+

说一个我做单点退出时遇到的坑,由于我把 CAS Server 部署在了机房中的一台设备上,然后在我本地启了一个 WEB 服务,这个时候登录填的 service127.0.0.1:8081/xxx 登录完之后的重定向是没有问题的,但是当我访问 CAS Logout 页面后,再访问我的系统,发现并没有退出登录,也没有访问退出登录的记录。原因是我注册服务时的 127.0.0.1 这个url认证中心根本访问不到。

+

后来我在本地起了一个 CAS Server 再次验证后没有问题。

+

还有人问道那个 ticket 如果被其他人截获了,岂不是就可以冒充我来登录了?并不会。

+

首先来说,默认情况下,CAS 要求子系统和它之间的通讯为 https ,再有就是这个 ticket 只有一次有效性,验证一次后即失效,而且有效期还很短,默认我记得只有5秒。最重要的是即便这个 ticket 被其他人获取了也没啥用,验证这个 ticket 时,还需要带上申请这个 ticket 时的 url 信息,而且认证中心鉴定 ticket 为真后也只是返回用户的用户名、认证时间等最基本的信息,由于子系统没有拿到这些信息,所以对于子系统来说,你还是没有登录的。

+

最后敬上官方的 CAS 协议流程图

+

+ +
+ + + + + + + +
+ + + + + + +
+ +
+ + + +
+ + + + + + + + +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git "a/2017/\345\237\272\344\272\216-Eureka-\347\232\204\346\234\215\345\212\241\346\263\250\345\206\214\344\270\216\345\217\221\347\216\260\350\260\203\347\240\224/1.png" "b/2017/\345\237\272\344\272\216-Eureka-\347\232\204\346\234\215\345\212\241\346\263\250\345\206\214\344\270\216\345\217\221\347\216\260\350\260\203\347\240\224/1.png" new file mode 100644 index 0000000000..c50cfc1046 Binary files /dev/null and "b/2017/\345\237\272\344\272\216-Eureka-\347\232\204\346\234\215\345\212\241\346\263\250\345\206\214\344\270\216\345\217\221\347\216\260\350\260\203\347\240\224/1.png" differ diff --git "a/2017/\345\237\272\344\272\216-Eureka-\347\232\204\346\234\215\345\212\241\346\263\250\345\206\214\344\270\216\345\217\221\347\216\260\350\260\203\347\240\224/index.html" "b/2017/\345\237\272\344\272\216-Eureka-\347\232\204\346\234\215\345\212\241\346\263\250\345\206\214\344\270\216\345\217\221\347\216\260\350\260\203\347\240\224/index.html" new file mode 100644 index 0000000000..c17cd3986f --- /dev/null +++ "b/2017/\345\237\272\344\272\216-Eureka-\347\232\204\346\234\215\345\212\241\346\263\250\345\206\214\344\270\216\345\217\221\347\216\260\350\260\203\347\240\224/index.html" @@ -0,0 +1,519 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 基于 Eureka 的服务注册与发现调研 | 贾攀的流水账 + + + + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

贾攀的流水账

+ +
+

Panmax's Blog

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+ + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ 基于 Eureka 的服务注册与发现调研 +

+ + +
+ + + + +
+ + +

Eureka 是 Netflix 开源的一款提供服务注册和发现的产品。

Eureka 由两个组件组成: Eureka 服务器Eureka 客户端。Eureka 服务器 用作服务注册服务器。Eureka 客户端 是一个 Java 客户端,用来简化与服务器的交互、作为轮询负载均衡器,并提供服务的故障切换支持。

+

官方对自己的定义是:

+

Eureka 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.

+
+

通俗点讲什么是服务注册发现?

服务注册与发现就像是在一个聊天室,每个用户来的时候去服务器上注册,这样你的好友们就能看到你,你同时也将获取好友的上线列表。在微服务中,服务就相当于聊天室的用户,而服务注册中心就像聊天室服务器一样。

+

Eureka特性

    +
  1. Eureka Server 具有服务定位/发现的能力,在各个微服务启动时,会通过 Eureka Client 向Eureka Server 进行注册自己的信息(例如网络信息)。
  2. +
  3. 一般情况下,微服务启动后,Eureka Client 会周期性向 Eureka Server 发送心跳检测(默认周期为30秒)以注册/更新自己的信息。
  4. +
  5. 如果 Eureka Server 在一定时间内(默认90秒)没有收到 Eureka Client 的心跳检测,就会注销掉该微服务点。
  6. +
  7. 同时,Eureka Server 本身也是 Eureka Client,多个 Eureka Server 通过复制注册表的方法来完成服务注册表的同步从而达到集群的效果。
  8. +
+

为什么选择 Eureka

1) 它提供了完整的 Service RegistryService Discovery 实现

首先是提供了完整的实现,并且也经受住了 Netflix 自己的生产环境考验,相对使用起来会比较省心。

+

2) 和 Spring Cloud 无缝集成

服务端和客户端都是 Java 编写的,针对微服务场景,并且和 Netflix 的其他开源项目以及 Spring Cloud 都有着非常好的整合,具备良好的生态。

+

3) Open Source

最后一点是开源,由于代码是开源的,所以非常便于我们了解它的实现原理和排查问题。

+

Eureka Server 使用介绍

在 Spring Boot 项目的 pom.xml 中加入 spring-cloud-starter-eureka-server

使用 Spring Cloud 需要在 pom.xml 中加入 Spring Cloud 的父级引用,让 Spring 帮我们管理依赖版本。

+
1
2
3
4
5
6
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-eureka-server</artifactId>
</dependency>
</dependencies>
+

在application.yml中配置

Eureka 是一个高可用的组件,它没有后端缓存,每一个实例注册之后需要向注册中心发送心跳(因此可以在内存中完成),在默认情况下 Erureka Server 也是一个 Eureka Client,必须要指定一个 Server。

+
1
2
3
4
5
6
7
8
9
10
11
server:
port: 8761

eureka:
instance:
hostname: localhost
client:
register-with-eureka: false # 不用将自己注册到Eureka
fetch-registry: false # 不用发现Eureka中的服务
service-url:
default-zone: http://${eureka.instance.hostname}:${server.port}/eureka/
+

添加 Application.java 启动类 添加 @EnableEurekaServer 注解

1
2
3
4
5
6
7
@SpringBootApplication
@EnableEurekaServer
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
+

Eureka Client 使用介绍

服务注册与服务发现都是使用 Eureka Client,所以在 Spring Boot 项目的 pom.xml 中加入

1
2
3
4
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-eureka</artifactId>
</dependency>
+

在 application.yml 中加入 Eureka 的 Server 配置

1
2
3
4
5
6
7
8
9
10
11
server:
port: 8762

spring:
application:
name: service-hi

eureka:
client:
serviceUrl:
defaultZone: http://localhost:8761/eureka/
+

需要指明 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 的自我保护机制:

+
+

Eureka Server 在运行期间,会统计心跳成功率在 15分钟 之内是否低于85%,如果出现低于的情况(在单机调试的时候很容易满足,实际在生产环境上通常是由于网络不稳定导致),Eureka Server 会将当前的实例注册信息保护起来,同时提示这个警告。至于为什么需要有这个自我保护机制,官方的解释是:Service 不是强一致的,所以会有部分情况下没发现新服务导致请求出错,对于Service 发现服务而言,宁可返回某服务 5分钟 之前在哪几个服务器上可用的信息,也不能因为暂时的网络故障而找不到可用的服务器,而不返回任何结果。

+
+

其他实现方式比较

DNS 可以算是最为原始的服务发现系统,但是在服务变更较为频繁,即服务的动态性很强的时候,DNS 记录的传播速度可能会跟不上服务的变更速度,这将导致在一定的时间窗口内无法提供正确的服务位置信息,所以这种方案只适合在比较静态的环境中使用,不适用于微服务。

+

基于 ZooKeeper、Etcd 等分布式键值对存储服务来建立服务发现系统在现在看起来 也不是一种很好的方案,一方面是因为它们只能提供基本的数据存储功能,还需要在外围做大量的开发才能形成完整的服务发现方案。另一方面是因为它们都是强一致性系统,在集群发生分区时会优先保证一致性、放弃可用性,而服务发现方案更注重可用性,为了保证可用性可以选择最终一致性,这两方面原因共同导致了 ZooKeeper、Etcd 这类系统越来越远离服务发现方案的备选清单,像 SmartStack 这种依赖 ZooKeeper 的服务发现方案也逐渐发觉 ZooKeeper 成了它的薄弱环节。与 ZooKeeper、Etcd 或者依赖它们的方案不同,Eureka 是个专门为服务发现从零开始开发的项目,Eureka 以可用性为先,可以在多种故障期间保持服务发现和服务注册功能可用,虽然此时会存在一些数据错误,但是 Eureka 的设计原则是“存在少量的错误数据,总比完全不可用要好”,并且可以在故障恢复之后按最终一致性进行状态合并,清理掉错误数据。

+

Eureka 有个强大的对手 Consul。Consul 是 HashiCorp 公司的商业产品,它有一个开源的基础版本,这个版本在基本的服务发现功能之外,还提供了多数据中心部署能力,包括内存、存储使用情况在内的细粒度服务状态检测能力,和用于服务配置的键值对存储能力(这是一把双刃剑,使用它可以带来便捷,但是也意味着和 Consul 的较强耦合性),这几个能力 Eureka 目前都没有。但是 Consul 对业务的侵入性较大,在与 SpringBoot 项目对接时没有那么方便,而且 Consul 由一家商业软件公司提供,那么必然或多或少的存在商业软件的某些弊端。

+ +
+ + + + + + + +
+ + + + + + +
+ +
+ + + +
+ + + + + + + + +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git "a/2017/\345\237\272\344\272\216-Hystrix-\347\232\204\347\206\224\346\226\255\345\231\250\350\260\203\347\240\224/1.png" "b/2017/\345\237\272\344\272\216-Hystrix-\347\232\204\347\206\224\346\226\255\345\231\250\350\260\203\347\240\224/1.png" new file mode 100644 index 0000000000..5a60a4df27 Binary files /dev/null and "b/2017/\345\237\272\344\272\216-Hystrix-\347\232\204\347\206\224\346\226\255\345\231\250\350\260\203\347\240\224/1.png" differ diff --git "a/2017/\345\237\272\344\272\216-Hystrix-\347\232\204\347\206\224\346\226\255\345\231\250\350\260\203\347\240\224/2.png" "b/2017/\345\237\272\344\272\216-Hystrix-\347\232\204\347\206\224\346\226\255\345\231\250\350\260\203\347\240\224/2.png" new file mode 100644 index 0000000000..13a1d442d3 Binary files /dev/null and "b/2017/\345\237\272\344\272\216-Hystrix-\347\232\204\347\206\224\346\226\255\345\231\250\350\260\203\347\240\224/2.png" differ diff --git "a/2017/\345\237\272\344\272\216-Hystrix-\347\232\204\347\206\224\346\226\255\345\231\250\350\260\203\347\240\224/index.html" "b/2017/\345\237\272\344\272\216-Hystrix-\347\232\204\347\206\224\346\226\255\345\231\250\350\260\203\347\240\224/index.html" new file mode 100644 index 0000000000..ef6250ad8e --- /dev/null +++ "b/2017/\345\237\272\344\272\216-Hystrix-\347\232\204\347\206\224\346\226\255\345\231\250\350\260\203\347\240\224/index.html" @@ -0,0 +1,522 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 基于 Hystrix 的熔断器调研 | 贾攀的流水账 + + + + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

贾攀的流水账

+ +
+

Panmax's Blog

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+ + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ 基于 Hystrix 的熔断器调研 +

+ + +
+ + + + +
+ + +

基于 Hystrix 的熔断器调研

什么是雪崩效应

在微服务架构中通常会有多个服务层调用,基础服务的故障可能会导致级联故障,进而造成整个系统不可用的情况,这种现象被称为服务雪崩效应。服务雪崩效应是一种因 服务提供者 的不可用导致 服务消费者 的不可用,并将不可用逐渐放大的过程。

+

如果下图所示:A 作为服务提供者,B 为 A 的服务消费者,C 和 D 是 B 的服务消费者。A 不可用引起了 B 的不可用,并将不可用像滚雪球一样放大到 C 和 D 时,雪崩效应就形成了。

+

+

熔断器(CircuitBreaker)

熔断器的原理很简单,如同电力过载保护器。它可以实现快速失败,如果它在一段时间内侦测到许多类似的错误,会强迫其以后的多个调用快速失败,不再访问远程服务器,从而防止应用程序不断地尝试执行可能会失败的操作,使得应用程序继续执行而不用等待修正错误,或者浪费 CPU 时间去等到长时间的超时产生。熔断器也可以使应用程序能够诊断错误是否已经修正,如果已经修正,应用程序会再次尝试调用操作。

+

熔断器模式就像是那些容易导致错误的操作的一种代理。这种代理能够记录最近调用发生错误的次数,然后决定是否允许操作继续,或者立即返回错误。熔断器开关相互转换的逻辑如下图:

+

+

Hystrix 是什么

在分布式系统,我们一定会依赖各种服务,那么这些个服务一定会出现失败的情况,Hystrix 就是这样的一个工具,它通过提供了逻辑上延时和错误容忍的解决力来协助我们完成分布式系统的交互。Hystrix 通过分离服务的调用点,阻止错误在各个系统的传播,并且提供了错误回调机制,这一系列的措施提高了系统的整体服务弹性。

+

Hystrix 是干嘛的

Hystrix 被设计用来做了下面几件事:

    +
  1. 保护系统间的调用延时以及错误,特别是通过第三方的工具的网络调用
  2. +
  3. 阻止错误在分布式系统之前的传播
  4. +
  5. 快速失败和迅速恢复
  6. +
  7. 错误回退和优雅的服务降级
  8. +
  9. 提供近乎实时的系统监控,报警和动态操控
  10. +
+

Hystrix特性

1.熔断器机制

+

熔断器很好理解,当 Hystrix Command 请求后端服务失败数量超过一定比例(默认50%),熔断器会切换到开路状态(Open)。这时所有请求会直接失败而不会发送到后端服务。熔断器保持在开路状态一段时间后(默认5秒),自动切换到半开路状态(HALF-OPEN),这时会判断下一次请求的返回情况,如果请求成功,熔断器切回闭路状态(CLOSED),否则重新切换到开路状态(OPEN)。Hystrix 的熔断器就像我们家庭电路中的保险丝,一旦后端服务不可用,熔断器会直接切断请求链,避免发送大量无效请求影响系统吞吐量,并且熔断器有自我检测并恢复的能力。

+

2.Fallback

+

Fallback 相当于是降级操作。对于查询操作, 我们可以实现一个 fallback 方法,当请求后端服务出现异常的时候,可以使用 fallback 方法返回的值。fallback 方法的返回值一般是设置的默认值或者来自缓存。

+

3.资源隔离

+

在Hystrix中, 主要通过线程池来实现资源隔离. 通常在使用的时候我们会根据调用的远程服务划分出多个线程池. 例如调用产品服务的 Command 放入 A 线程池,调用账户服务的 Command 放入 B 线程池. 这样做的主要优点是运行环境被隔离开了。这样就算调用服务的代码存在 bug 或者由于其他原因导致自己所在线程池被耗尽时,不会对系统的其他服务造成影响。但是带来的代价就是维护多个线程池会对系统带来额外的性能开销。如果是对性能有严格要求而且确信自己调用服务的客户端代码不会出问题的话,可以使用 Hystrix 的信号模式(Semaphores)来隔离资源。

+

Hystrix工作方式如下:

    +
  • 阻止一个单独的依赖耗尽系统的所有线程,比如(tomcat)
  • +
  • 使用快速失败代替将这个请求排队
  • +
  • 在任何可能失败的地方提供后退机制来确保用户不会看到错误
  • +
  • 使用隔离技术(比如: 隔板,泳道,环路切断模式)降低一个依赖的失败对整个系统的影响
  • +
  • 优化使得系统可以近乎实时的收集,监控,报警
  • +
  • 优化使得系统可以近乎实时的修改,并且可以近乎实时生效
  • +
  • 保护系统不仅仅在网络层面,也包括客户端层面的依赖执行的失败
  • +
+

Hystrix 的使用

因为 Hystrix 是一个开源的 Java 库,所以可以进行像官方示例那样直接用起来,下面我们介绍如何将 Hystrix 集成到 SpringBoot 项目中。

+

在pox.xml文件中加入:

1
2
3
4
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-hystrix</artifactId>
</dependency>
+

在程序入口类加上:@EnableHystrix

在需要使用熔断器的地方标记注解即可:

1
2
@HystrixCommand(fallbackMethod = "yyy")
public String doSomething()
+

yyy 就是在 熔断器开启时 是我们要调用的函数。

+ +
+ + + + + + + +
+ + + + + + +
+ +
+ + + +
+ + + + + + + + +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git "a/2017/\345\237\272\344\272\216\345\211\215\345\220\216\347\253\257\345\210\206\347\246\273\347\232\204CAS\345\257\271\346\216\245\346\226\271\346\241\210/index.html" "b/2017/\345\237\272\344\272\216\345\211\215\345\220\216\347\253\257\345\210\206\347\246\273\347\232\204CAS\345\257\271\346\216\245\346\226\271\346\241\210/index.html" new file mode 100644 index 0000000000..569f52418a --- /dev/null +++ "b/2017/\345\237\272\344\272\216\345\211\215\345\220\216\347\253\257\345\210\206\347\246\273\347\232\204CAS\345\257\271\346\216\245\346\226\271\346\241\210/index.html" @@ -0,0 +1,531 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 基于前后端分离的CAS对接方案 | 贾攀的流水账 + + + + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

贾攀的流水账

+ +
+

Panmax's Blog

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+ + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ 基于前后端分离的CAS对接方案 +

+ + +
+ + + + +
+ + +

最近开发的项目,需要对接公司已有的 CAS 服务,之前的项目都是用的传统模式(后端渲染或前后端不分离)来开发的,所以后端可以很容易的实现控制跳转,完成校验,写入 Cookies 等逻辑。在传统的开发模式下,使用 session-cookie 可以保证接口安全,在没有登录的情况下访问关键数据会跳转到登录界面或者请求失败。而使用 REStful API 之后,session-cookie 存在以下 3 个问题:

+
    +
  • 客户端除了浏览器,可能还包括手机端 APP,对于手机端而言,管理 cookie 是一件麻烦的事情
  • +
  • RESTful 风格的 API 不建议使用 cookie
  • +
  • Cookie 本身有一个缺陷,不能跨域
  • +
+

解决这个问题的方案是让前端传数据时,在 URL 参数中或者 header 中携带一个 参数,我们成这个参数为 token,后端通过这个 token 来判断用户身份,这样可以免去对 Cookies 的管理。

+

顺着这个思路,我们很容易想到一种方案,就是后端维护一个 Mapkey 值为 tokenvalue 为用户信息,这样只要用户登录时生成这个 key 后,放到 Map 中,然后将 key 返回给前端就行了,但是这样做有个很严重的问题,Map 是在内存中的,如果后端服务为集群时,还需要做 key 同步,非常麻烦,当然也有人会提出可以将后端生成的 token 和对应的用户信息放在 键-值 数据库中,这样就不用考虑同步问题了,当然这样做没有什么问题,但是会额外引入基础组件(我们现在做第一版,为了快速开发,不打算引入太多的组件),而且还要保证键值数据库的高可用性。

+

这里我使用了一个更加优雅的方案:JSON Web Token

+

JSON Web Token(JWT)是一个非常轻巧的规范。这个规范允许我们使用 JWT 在用户和服务器之间传递安全可靠的信息。

+

JWT的组成:

一个JWT实际上就是一个字符串,它由三部分组成,头部、载荷与签名。

+

载荷(Payload):

Payload 是 JWT 存储信息的部分。Payload 也是一个 json 数据,每一个 json 的 key-value 称为一个声明。

+

我们将用户信息描述成一个 json 对象。其中添加了一些其他的信息,帮助今后收到这个 JWT 的服务器理解这个 JWT 。

+
1
2
3
4
5
6
7
8
9
{
"iss": "Panmax JWT",
"iat": 1441593502,
"exp": 1441594722,
"aud": "www.example.com",
"sub": "jrocket@example.com",
"user_id": 1,
"username": "jiapan"
}
+

这里面的前五个字段都是由JWT的标准所定义的

+
    +
  • iss: 该JWT的签发者
  • +
  • sub: 该JWT所面向的用户
  • +
  • aud: 接收该JWT的一方
  • +
  • exp(expires): JWT 的过期时间,是一个 unxi 时间戳
  • +
  • iat(issued at): JWT 的签发时间,是一个 unix 时间戳
  • +
+

上面这个 payload 中,user_idusername 为自定义声明。

+

将上面的 json 对象进行base64编码可以得到下面的字符串。这个字符串我们将它称作JWT的Payload(载荷)。

+

ewogICAgImlzcyI6ICJQYW5tYXggSldUIiwKICAgICJpYXQiOiAxNDQxNTkzNTAyLAogICAgImV4cCI6IDE0NDE1OTQ3MjIsCiAgICAiYXVkIjogInd3dy5leGFtcGxlLmNvbSIsCiAgICAic3ViIjogImpyb2NrZXRAZXhhbXBsZS5jb20iLAogICAgInVzZXJfaWQiOiAxLAogICAgInVzZXJuYW1lIjogImppYXBhbiIKfQ==

+
+

注:Base64是一种编码,也就是说,它是可以被翻译回原来的样子来的。它并不是一种加密过程。

+
+

头部(Header)

WT还需要一个头部,头部用于描述关于该JWT的最基本的信息,例如其类型以及签名所用的算法等。这也可以被表示成一个JSON对象。

+
1
2
3
4
{
"typ": "JWT",
"alg": "HS256"
}
+

在这里,我们说明了这是一个 JWT,并且我们所用的签名算法(后面会提到)是 HS256 算法。

+

对它也要进行Base64编码,之后的字符串就成了 JWT 的 Header(头部)。

+

ewogICJ0eXAiOiAiSldUIiwKICAiYWxnIjogIkhTMjU2Igp9

+

签名(签名)

将上面的两个编码后的字符串都用句号.连接在一起(头部在前),就形成了

+

ewogICJ0eXAiOiAiSldUIiwKICAiYWxnIjogIkhTMjU2Igp9.ewogICAgImlzcyI6ICJQYW5tYXggSldUIiwKICAgICJpYXQiOiAxNDQxNTkzNTAyLAogICAgImV4cCI6IDE0NDE1OTQ3MjIsCiAgICAiYXVkIjogInd3dy5leGFtcGxlLmNvbSIsCiAgICAic3ViIjogImpyb2NrZXRAZXhhbXBsZS5jb20iLAogICAgInVzZXJfaWQiOiAxLAogICAgInVzZXJuYW1lIjogImppYXBhbiIKfQ==

+

最后,我们将上面拼接完的字符串用 HS256 算法进行加密。在加密的时候,我们还需要提供一个密钥(secret)。加密之后,得到一串加密字符串,最后把这串加密字符串也是用 . 拼接在 header.payload 后面,形成完整的 JWT。

+

最终生成的JWT格式如下:

+

xxx.yyy.zzz

+

这里签名的目的是为了保证 payload 数据的完整性。如果 JWT 在传输过程中被第三方劫持,中间人对 header.payload 进行修改,并且使用自己的密钥重新签名。服务端收到中间人修改过的 JWT,使用自己的密钥对 header.payload 进行再次加密,由于中间人和服务端使用的是不同的密钥签名,所以服务端再次加密的结果肯定和中间人加密的结果不一致,由此可以断定该 JWT 被恶意篡改。

+

经过上边的介绍,我们可以看出 JWT 中 Payload 的信息是可以解码会明文的,也就是说信息会泄露,所以 JWT 中不应该存放任何敏感信息,用于登录时我们只需放入用户ID或者用户名就可以了,不要把身份号或者密码等信息放入JWT中,应该让后端拿到用户ID或者用户名后进行查询得到身份证号等隐私信息。

+

好的,以上就是JWT的科普部分,下边介绍下我这边CAS对接实现方案。

+

我单独写了一个服务,命名为 hodor(hold the door),作为一个中间层来验证 CAS ticket 并且根据用户信息生成 JWT Token。

+

我们用 zuul 作为网关服务,当请求过来后,网关判断有没有携带 token,并且判断 token 的有效性,我们需要把网关里验证 jwt 的密钥和 hodor 中生成 jwt 的密钥设置为相同的字符串就可以完成验证工作。

+

当网关验证 token 没有被篡改并且还在有效期内后,从 Payload 中取出我们需要的信息,将这些信息明文放在 header 中继续往后请求各个应用,对于应用来说从网关过来的请求是可信的,直接从头中取出相应的用户名或者ID就行了。这里有一个坑,就是网关将信息放入 header 的时候,只能传 ASCII 编码字符串,我们的 CAS 返回用户信息时会同时返回中文姓名,所以中文在传入 header 时,需要做 urlencode 处理,同时应用内接受时也需要做 urldecode。

+

如果没有携带 token 或 token 无效,网关会返回 HTTP 401 错误,前端收到这个返回码后,会跳转到 CAS 认证地址,让用户来登录。同时 service 是前端配置好的一个地址,当用户登录成功后,会回到前端配置好那个地址,前端拿到 CAS 给的 ticket 后,用这个 ticket 和申请 ticket 时的 service 来请求我写的 hodor 服务,hodor根据 ticket 和service 来完成ticket验证工作,获取用户信息,生成jwt,返回给前端,前端保存这个 token,再之后的请求中携带这个 token 来访问就行了。

+ +
+ + + + + + + +
+ + + + + + +
+ +
+ + + +
+ + + + + + + + +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git "a/2017/\345\257\271 Python \345\210\227\350\241\250\345\270\270\350\247\201\346\223\215\344\275\234\347\232\204\347\220\206\350\247\243/index.html" "b/2017/\345\257\271 Python \345\210\227\350\241\250\345\270\270\350\247\201\346\223\215\344\275\234\347\232\204\347\220\206\350\247\243/index.html" new file mode 100644 index 0000000000..c4691c7dc3 --- /dev/null +++ "b/2017/\345\257\271 Python \345\210\227\350\241\250\345\270\270\350\247\201\346\223\215\344\275\234\347\232\204\347\220\206\350\247\243/index.html" @@ -0,0 +1,496 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 对 Python 列表常见操作的理解 | 贾攀的流水账 + + + + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

贾攀的流水账

+ +
+

Panmax's Blog

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+ + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ 对 Python 列表常见操作的理解 +

+ + +
+ + + + +
+ + +

写这篇文章的原因是前几天做了一道面试题,问题大致是这样的:

+
1
2
3
4
5
6
7
l = [1, 2, 3, 4, 5]

l.insert(1, 6)

l.append(6)

的时间复杂度分表是什么
+

我理所应当的认为,Python 列表的内部实现应该是一个链表,而链表的插入和追加操作应该都是 O(1),但今天看到一篇文章原文地址,发现并不是我认为的那样。

+

Python 在 C 实现中,存储数据的部分是一块连续的内存数组,不过这个数组里存放的也是指针,指向具体的元素,并且会在 结构体 中记录元素的实际个数,结构体如下。

+
1
2
3
4
5
6
7
8
typedef struct {
# 列表元素的长度
int ob_size;
# 真正存放列表元素容器的指针,list[0] 就是 ob_item[0]
PyObject **ob_item;
# 当前列表可容纳的元素大小
Py_ssize_t allocated;
} PyListObject;
+

当追加新元素的时候,可以直接通过 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

+ +
+ + + + + + + +
+ + + + + + +
+ +
+ + + +
+ + + + + + + + +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git "a/2017/\345\260\206\345\215\232\345\256\242\351\203\250\347\275\262\345\210\260\344\270\203\347\211\233/1.png" "b/2017/\345\260\206\345\215\232\345\256\242\351\203\250\347\275\262\345\210\260\344\270\203\347\211\233/1.png" new file mode 100644 index 0000000000..7bbc51e12f Binary files /dev/null and "b/2017/\345\260\206\345\215\232\345\256\242\351\203\250\347\275\262\345\210\260\344\270\203\347\211\233/1.png" differ diff --git "a/2017/\345\260\206\345\215\232\345\256\242\351\203\250\347\275\262\345\210\260\344\270\203\347\211\233/2.png" "b/2017/\345\260\206\345\215\232\345\256\242\351\203\250\347\275\262\345\210\260\344\270\203\347\211\233/2.png" new file mode 100644 index 0000000000..22ed669e7d Binary files /dev/null and "b/2017/\345\260\206\345\215\232\345\256\242\351\203\250\347\275\262\345\210\260\344\270\203\347\211\233/2.png" differ diff --git "a/2017/\345\260\206\345\215\232\345\256\242\351\203\250\347\275\262\345\210\260\344\270\203\347\211\233/3.png" "b/2017/\345\260\206\345\215\232\345\256\242\351\203\250\347\275\262\345\210\260\344\270\203\347\211\233/3.png" new file mode 100644 index 0000000000..f08acf4a49 Binary files /dev/null and "b/2017/\345\260\206\345\215\232\345\256\242\351\203\250\347\275\262\345\210\260\344\270\203\347\211\233/3.png" differ diff --git "a/2017/\345\260\206\345\215\232\345\256\242\351\203\250\347\275\262\345\210\260\344\270\203\347\211\233/4.png" "b/2017/\345\260\206\345\215\232\345\256\242\351\203\250\347\275\262\345\210\260\344\270\203\347\211\233/4.png" new file mode 100644 index 0000000000..b67945df03 Binary files /dev/null and "b/2017/\345\260\206\345\215\232\345\256\242\351\203\250\347\275\262\345\210\260\344\270\203\347\211\233/4.png" differ diff --git "a/2017/\345\260\206\345\215\232\345\256\242\351\203\250\347\275\262\345\210\260\344\270\203\347\211\233/index.html" "b/2017/\345\260\206\345\215\232\345\256\242\351\203\250\347\275\262\345\210\260\344\270\203\347\211\233/index.html" new file mode 100644 index 0000000000..884339983a --- /dev/null +++ "b/2017/\345\260\206\345\215\232\345\256\242\351\203\250\347\275\262\345\210\260\344\270\203\347\211\233/index.html" @@ -0,0 +1,508 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 将博客部署到七牛 | 贾攀的流水账 + + + + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

贾攀的流水账

+ +
+

Panmax's Blog

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+ + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ 将博客部署到七牛 +

+ + +
+ + + + +
+ + +

我觉得我是不折腾就会死的人,现在的博客部署在 Github Pages 上,访问速度一直令我非常不爽,所以准备迁移到七牛。

+

七牛新建 bucket 后,在空间设置,把默认首页设置改成启动就完成了。

+

还有一个就是一定要绑定自己的独立域名,否则站内数据改变后,用七牛提供的临时域名来访问的话,缓存一时半会是不会刷新的,独立域名可以设置缓存刷新时间,前提是那个域名需要在国内进行备案。

+

缓存可以根据不同类型的数据有不同的策略,但我为了省事,直接将所有配置改为了 0,也就是不进行缓存,因为不是什么大流量站点,再加上七牛的 CDN 优化,所以即便不缓存速度也非常快,这样可以保证我每次在发布或者修改内容后能够及时更新。其实完全可以把非 HTML 类型的数据设置一些缓存时间。

+

+

可以点这里试一试部署在七牛上的速度,只是我之前的一个备案过的域名,但我没打算用这个域名来做我的国内博客地址,新地址正在备案中: jpanj.com 贾攀家。

+

接下来就是把博客生成出来的静态站按照目录结构完整上传到七牛就行了,但是这个工作非常麻烦,每次上传时如果有二级目录的话,需要自己填写前缀,而且每次生成后都需要自己登录到七牛上传一下。身为程序员的我,这不是在侮辱我的智商吗?所以我写了一个 Python 脚本,可以帮我自动完成这个工作。

+

我之前是使用的 hexo 的 github 插件来进行发布的,其实也非常简单,只需要执行: hexo d -g 即可完成生成静态站和部署的工作。(g=Generate static files. d=Deploy your website.),现在只需要在执行玩这个操作后,再执行下发布到七牛的脚本,就可以完成双发布了。

+

我是配合 Alfred 来用的,如果没装 Alfred Workflow 的话,直接执行脚本也是可以的,把脚本简单修改下就行了。

+

上效果图:

+

+

+

代码已上传到 Github:https://github.com/Panmax/qiniu-blog-deploy

+
+

updateAt: 2017-06-20

我把缓存策略改为了如下所示

+

+

图片和样式文件进行缓存,html 等文本文件不缓存。因为七牛会自动寻找 index.html,所以在真正访问时,不带 /index.html 后缀的页面也可以打开,所以我把全局配置设为了不缓存,也就是说不在这个配置中的文件也不进行缓存。

+ +
+ + + + + + + +
+ + + + + + +
+ +
+ + + +
+ + + + + + + + +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git "a/2017/\346\216\245\345\205\245sentry\346\227\266\351\201\207\345\210\260\347\232\204\345\235\221/index.html" "b/2017/\346\216\245\345\205\245sentry\346\227\266\351\201\207\345\210\260\347\232\204\345\235\221/index.html" new file mode 100644 index 0000000000..d7a4e71032 --- /dev/null +++ "b/2017/\346\216\245\345\205\245sentry\346\227\266\351\201\207\345\210\260\347\232\204\345\235\221/index.html" @@ -0,0 +1,489 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 接入 sentry 时遇到的坑 | 贾攀的流水账 + + + + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

贾攀的流水账

+ +
+

Panmax's Blog

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+ + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ 接入 sentry 时遇到的坑 +

+ + +
+ + + + +
+ + +

将 进销存SaaS 接入 Sentry,但是接入后发现无法通过 ajax 来 POST 或者 PUT 数据, 会报:

+
1
2
3
<title>400 Bad Request</title>
<h1>Bad Request</h1>
<p>Failed to decode JSON object: No JSON object could be decoded</p>
+

解决办法是在 js 的 ajax 方法中加上:contentType:"application/json; charset=utf-8",

+

注:只需要在提交的数据类型为 JsonPOSTPUT 的方法上添加就行了,不用在提交 Form 表单 的地方添加,否则加上后 Form 表单 类型的 ajax 就无法提交了。

+ +
+ + + + + + + +
+ + + + + + +
+ +
+ + + +
+ + + + + + + + +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git "a/2017/\347\272\252\345\277\265\345\276\210\344\271\205\346\262\241\346\234\211\345\206\231\344\273\243\347\240\201\345\210\260\346\267\261\345\244\234-\345\207\240\344\270\252\346\217\220\351\253\230\346\225\210\347\216\207\347\232\204oh-my-zsh\346\217\222\344\273\266/index.html" "b/2017/\347\272\252\345\277\265\345\276\210\344\271\205\346\262\241\346\234\211\345\206\231\344\273\243\347\240\201\345\210\260\346\267\261\345\244\234-\345\207\240\344\270\252\346\217\220\351\253\230\346\225\210\347\216\207\347\232\204oh-my-zsh\346\217\222\344\273\266/index.html" new file mode 100644 index 0000000000..e475fbe69b --- /dev/null +++ "b/2017/\347\272\252\345\277\265\345\276\210\344\271\205\346\262\241\346\234\211\345\206\231\344\273\243\347\240\201\345\210\260\346\267\261\345\244\234-\345\207\240\344\270\252\346\217\220\351\253\230\346\225\210\347\216\207\347\232\204oh-my-zsh\346\217\222\344\273\266/index.html" @@ -0,0 +1,500 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 纪念很久没有写代码到深夜 && 几个提高效率的oh-my-zsh插件 | 贾攀的流水账 + + + + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

贾攀的流水账

+ +
+

Panmax's Blog

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+ + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ 纪念很久没有写代码到深夜 && 几个提高效率的oh-my-zsh插件 +

+ + +
+ + + + +
+ + +

昨晚写代码时遇到一个坑,导致12点半左右才合上电脑。这个坑是自己挖出来的,大致原因是在使用 sqlalchemy 读取一个数据前,给这个数据进行了操作,导致每次读出来的值都不准。之前没想到是因为前边的代码操作了数据,恰好我在这之前为了验证一些逻辑,手动改了下表数据,所以我一度怀疑是 sqlalchemy 或者 mysql 的缓存导致,或者有事务没有提交导致,然后各种查资料,尝试关闭 mysql 缓存啥的,都没有解决问题,但是后来发现用不同参数调用时,有时又能得到正确的数据,使我不禁开始怀疑人生。

+

眼看到了0点,我静下心来,一行一行检查代码运行路径,最终捉住了这只虫子。。。在我印象中,自从去年7、8月份后,就没有写代码到这么晚了,因为之前每次写代码都会兴奋,导致休息不好,所以就改掉了深夜写代码的习惯。

+

今天白天在打开终端时,我的 oh-my-zsh 例行提示我是否要检查更新,我进行了更新工作后,饶有兴趣的查了查 oh-my-zsh 的常用插件,自己也收入囊肿几个。在此做下记录:

+

先说下如何配置插件,打开 ~/.zshrc 里边有个

+

plugins=(...) 编辑括号中的内容就可以了

+

d

这个插件可以记录我本次窗口进入过的目录历史记录,当在几个目录之间来回穿梭时,可以输入 d 回车,按照提示的数字直接进入之前进入过的目录。

+

sublime

之前在命令行下,为了快速编辑一个文件,我通常使用 vi, 或者做复杂编辑的时候使用 atom,其实我更喜欢用 sublime 一些,但是一直找不到如何让 终端 调起它的方法,今天终于知道到了。常用命令如下

+
1
2
3
4
st          # 直接打开sublime
st file_a # 用sublime打开文件 file
st dir_a # 用sublime打开目录 dir
stt # 在sublime打开当前目录,相当于 st .
+

extract

我觉得这个插件真的解决了我的痛点,之前每次解压文件,都需要先去网上查下命令,比如解 gz.tar 需要用什么命令 解压 tar 需要什么命令,解压 zip 需要什么命令,现在好了,需要解压文件时直接 x file_name 就完成了。

+

z

作用和 autojump 相同,autojump 是使用 j 作为启动键,z 是用 z 作为启动键,但是查阅资料后解释 z 的速度更快一些,z 是使用 shell 直接编写的,而 autojump 则是用 Python 编写(又黑 Python )。。。

+

这个是用来在终端中启用搜索的命令,比如 输入 google Python 会自动用默认浏览器打开 google 并用 Python 作为关键字进行查询。同时也支持 baidu、bing。

+

我现在的插件列表如下:

+

plugins=(git d sublime extract z web-search)

+

git 的 Aliases 见: https://github.com/robbyrussell/oh-my-zsh/wiki/Plugin:git

+ +
+ + + + + + + +
+ + + + + + +
+ +
+ + + +
+ + + + + + + + +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git "a/2017/\350\242\253-Chrome-\345\235\221\344\272\206\344\270\200\346\254\241/index.html" "b/2017/\350\242\253-Chrome-\345\235\221\344\272\206\344\270\200\346\254\241/index.html" new file mode 100644 index 0000000000..797d8fff6d --- /dev/null +++ "b/2017/\350\242\253-Chrome-\345\235\221\344\272\206\344\270\200\346\254\241/index.html" @@ -0,0 +1,487 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 被 Chrome 坑了一次 | 贾攀的流水账 + + + + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

贾攀的流水账

+ +
+

Panmax's Blog

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+ + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ 被 Chrome 坑了一次 +

+ + +
+ + + + +
+ + +

今天继续研究单点登录,正常来说完成登录回跳到 client 端后,业务系统本身应该写一个自己的session,为了测试,我搭了个很简单的 client 端,但是发现 session 一直写不进去,用 Chrome 的调试工具看到 response 确实有写 Cookies 的操作,但是浏览器中却没有保存这个Cookies,折腾了小一天的时候,后来我抱着没啥希望的态度,用safari浏览器试了下,居然没有任何问题,然后用 Firefox 试了下,也没有问题。。。接着我尝试清除 Chrome 中的Cookies,发现问题解决了。

+

恩,写流水账好开心。

+ +
+ + + + + + + +
+ + + + + + +
+ +
+ + + +
+ + + + + + + + +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git "a/2017/\350\247\243\345\206\263Pycharm\344\270\255\346\200\273\346\230\257\344\270\215\345\260\217\345\277\203\346\213\226\346\213\275\344\273\243\347\240\201\347\232\204\351\227\256\351\242\230/index.html" "b/2017/\350\247\243\345\206\263Pycharm\344\270\255\346\200\273\346\230\257\344\270\215\345\260\217\345\277\203\346\213\226\346\213\275\344\273\243\347\240\201\347\232\204\351\227\256\351\242\230/index.html" new file mode 100644 index 0000000000..e4ea7649f0 --- /dev/null +++ "b/2017/\350\247\243\345\206\263Pycharm\344\270\255\346\200\273\346\230\257\344\270\215\345\260\217\345\277\203\346\213\226\346\213\275\344\273\243\347\240\201\347\232\204\351\227\256\351\242\230/index.html" @@ -0,0 +1,506 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 解决Pycharm中总是不小心拖拽代码的问题 | 贾攀的流水账 + + + + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

贾攀的流水账

+ +
+

Panmax's Blog

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+ + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ 解决Pycharm中总是不小心拖拽代码的问题 +

+ + +
+ + + + +
+ + +

我一直想转 VIM 党,但转了好多次了都没转成功,后来就放弃了,给了一个安慰自己的说法:「某些事情,键盘就是没有鼠标快。」所以依旧在使用 Pycharm 作为我的主要开发工具。

+

但是。。

+

因为我是鼠标党或者触摸板党,所以经常不小心在用鼠标的时候拖拽代码,尤其是升级完 Sierra 后(其实我也不太清楚是因为换了Magic Mouse2 以后还是升级导致的),就经常不小心拖拽代码,将代码弄乱,每次都要按 command + z 撤销,然后还要反复检查是不是搞乱了,大大影响我的工作效率,今天突然想到,设置中是不是能关闭代码拖拽的功能,于是通过关键词 drag 找了找,果然,没有找到。

+

然而我并没有放弃,在 Editor –> General 中看到一项叫 Enable Drag’n’Drop functionality in Editor ,把这个项前边的勾勾去掉后,发现奇迹般的貌似成功了,好吧,运气不错。。

+

开门红~

+
+

2017-04-08 UPDATE:

+

由于最近的工作需要,要读一些 Java 代码,默认 IDEA 配置中会自动将 import 和单行函数折叠,为了取消这个限制,需要在 Preferences > Editor > General > Code Folding在右侧窗口选择哪些要折叠,哪些不需要折叠。

+ +
+ + + + + + + +
+ + + + + + +
+ +
+ + + +
+ + + + + + + + +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git "a/2017/\350\247\243\345\206\263\345\234\250-SAE-\344\270\255\344\275\277\347\224\250-Flask-SQLAlchemy-\345\207\272\347\216\260-MySQL-server-has-gone-away-\347\232\204\351\227\256\351\242\230/index.html" "b/2017/\350\247\243\345\206\263\345\234\250-SAE-\344\270\255\344\275\277\347\224\250-Flask-SQLAlchemy-\345\207\272\347\216\260-MySQL-server-has-gone-away-\347\232\204\351\227\256\351\242\230/index.html" new file mode 100644 index 0000000000..8143b3f162 --- /dev/null +++ "b/2017/\350\247\243\345\206\263\345\234\250-SAE-\344\270\255\344\275\277\347\224\250-Flask-SQLAlchemy-\345\207\272\347\216\260-MySQL-server-has-gone-away-\347\232\204\351\227\256\351\242\230/index.html" @@ -0,0 +1,492 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 解决在 SAE 中使用 Flask-SQLAlchemy 出现 MySQL server has gone away 的问题 | 贾攀的流水账 + + + + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

贾攀的流水账

+ +
+

Panmax's Blog

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+ + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ 解决在 SAE 中使用 Flask-SQLAlchemy 出现 MySQL server has gone away 的问题 +

+ + +
+ + + + +
+ + +

这段时间尝试把 进销存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参数的原文解释:

+
+

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.

+
+ +
+ + + + + + + +
+ + + + + + +
+ +
+ + + +
+ + + + + + + + +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2018/Awsome-Abbreviation/index.html b/2018/Awsome-Abbreviation/index.html new file mode 100644 index 0000000000..98114101ab --- /dev/null +++ b/2018/Awsome-Abbreviation/index.html @@ -0,0 +1,531 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 程序员需要知道的缩写和专业名词 | 贾攀的流水账 + + + + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

贾攀的流水账

+ +
+

Panmax's Blog

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+ + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ 程序员需要知道的缩写和专业名词 +

+ + +
+ + + + +
+ + +
+

程序员的世界里充斥着很多的专业名词和英文缩写,我打算对一些常见的词汇进行一个汇总,同时会在 GitHub 上进行同步:https://github.com/Panmax/Awsome-Programmer-Abbreviation,欢迎 PR。

+
+

英文缩写

API

应用程序接口(英语:Application Programming Interface,简称:API),又称为应用编程接口,就是软件系统不同组成部分衔接的约定。由于近年来软件的规模日益庞大,常常需要把复杂的系统划分成小的组成部分,编程接口的设计十分重要。程序设计的实践中,编程接口的设计首先要使软件系统的职责得到合理划分。良好的接口设计可以降低系统各部分的相互依赖,提高组成单元的内聚性,降低组成单元间的耦合程度,从而提高系统的维护性和扩展性。

+

ACID

ACID,是指数据库管理系统(DBMS)在写入或更新资料的过程中,为保证事务(transaction)是正确可靠的,所必须具备的四个特性:原子性(atomicity,或称不可分割性)、一致性(consistency)、隔离性(isolation,又称独立性)、持久性(durability)。

+

AJAX

AJAX即“Asynchronous JavaScript and XML”(异步的 JavaScript 与 XML 技术),指的是一套综合了多项技术的浏览器端网页开发技术。

+

CAS

    +
  1. 比较并交换(compare and swap, CAS),是原子操作的一种,可用于在多线程编程中实现不被打断的数据交换操作,从而避免多线程同时改写某一数据时由于执行顺序不确定性以及中断的不可预知性产生的数据不一致问题。 该操作通过将内存中的值与指定数据进行比较,当数值一样时将内存中的数据替换为新的值。
  2. +
  3. 集中式认证服务(英语:Central Authentication Service,缩写CAS)是一种针对万维网的单点登录协议。它的目的是允许一个用户访问多个应用程序,而只需提供一次凭证(如用户名和密码)。它还允许web应用程序在没有获得用户的安全凭据(如密码)的情况下对用户进行身份验证。“CAS”也指实现了该协议的软件包。
  4. +
+

JPA

JPA 是 Java Persistence API 的简称,中文名 Java 持久层 API,是 JDK 5.0 注解或 XML 描述对象-关系表的映射关系,并将运行期的实体对象持久化到数据库中。

+

JSON

JSON(JavaScript Object Notation)是一种轻量级的数据交换语言,以文字为基础,且易于让人阅读。尽管 JSON 是 Javascript 的一个子集,但JSON是独立于语言的文本格式,并且采用了类似于 C语言 家族的一些习惯。

+

POJO

POJO(Plain Ordinary Java Object)简单的 Java 对象,实际就是普通 Java Beans。使用 POJO 名称是为了避免和 EJB 混淆起来,而且简称比较直接。其中有一些属性及其 getter setter 方法的类,没有业务逻辑,有时可以作为VO(Value Object) 或 DTO(Data Transform Object) 来使用。当然,如果你有一个简单的运算属性也是可以的,但不允许有业务方法,也不能携带有 connection 之类的方法。

+

DSL

领域专用语言(Domain Specific Language/DSL),其基本思想是「求专不求全」,不像通用目的语言那样目标范围涵盖一切软件问题,而是专门针对某一特定问题的计算机语言。

+

GC

在计算机科学中,垃圾回收(英语:Garbage Collection,缩写为GC)是一种自动的内存管理机制。当一个电脑上的动态内存不再需要时,就应该予以释放,以让出内存,这种内存资源管理,称为垃圾回收。垃圾回收器可以让程序员减轻许多负担,也减少程序员犯错的机会。垃圾回收最早起源于LISP语言。目前许多语言如 Smalltalk、Java、C# 和 D 语言都支持垃圾回收器。

+

DML

数据操纵语言(Data Manipulation Language, DML)是 SQL 语言中,负责对数据库对象运行数据访问工作的指令集,以 INSERT、UPDATE、DELETE 三种指令为核心,分别代表插入、更新与删除,是开发以数据为中心的应用程序必定会使用到的指令,因此有很多开发人员都把加上SQL的SELECT语句的四大指令以“CRUD”来称呼。

+

DDL

数据定义语言(Data Definition Language,DDL)是 SQL 语言集中负责数据结构定义与数据库对象定义的语言,由 CREATE、ALTER 与 DROP 三个语法所组成,最早是由Codasyl(Conference on Data Systems Languages)数据模型开始,现在被纳入 SQL 指令中作为其中一个子集。

+

DI

Dependency Injection,依赖注入。在软件工程中,依赖注入是种实现控制反转用于解决依赖性设计模式。一个依赖关系指的是可被利用的一种对象(即服务提供端) 。依赖注入是将所依赖的传递给将使用的从属对象(即客户端)。该服务是将会变成客户端的状态的一部分。 传递服务给客户端,而非允许客户端来建立或寻找服务,是本设计模式的基本要求。

+

DNS

域名系统(英文:Domain Name System)是互联网的一项服务。它作为将域名和IP地址相互映射的一个分布式数据库,能够使人更方便地访问互联网。DNS使用TCP和UDP端口53。当前,对于每一级域名长度的限制是63个字符,域名总长度则不能超过253个字符。

+

GUI

图形用户界面(Graphical User Interface)是指采用图形方式显示的计算机操作用户界面。与早期计算机使用的命令行界面相比,图形界面对于用户来说在视觉上更易于接受。

+

HTTP

超文本传输协议(英文:HyperText Transfer ProtocolP)是一种用于分布式、协作式和超媒体信息系统的应用层协议。HTTP是万维网的数据通信的基础。

+

IOC

控制反转(Inversion of Control,缩写为IoC),是面向对象编程中的一种设计原则,可以用来减低计算机代码之间的耦合度。其中最常见的方式叫做依赖注入(Dependency Injection,简称DI),还有一种方式叫“依赖查找”(Dependency Lookup)。通过控制反转,对象在被创建的时候,由一个调控系统内所有对象的外界实体,将其所依赖的对象的引用传递给它。也可以说,依赖被注入到对象中。

+

JWT

JSON Web Token(JWT)是一个非常轻巧的规范。这个规范允许我们使用JWT在用户和服务器之间传递安全可靠的信息,特别适用于分布式站点的单点登录(SSO)场景。

+

LDAP

轻型目录存取协定(英文:Lightweight Directory Access Protocol)是一个开放的,中立的,工业标准的应用协议,通过IP协议提供访问控制和维护分布式信息的目录信息。

+

MVC

MVC模式(Model–view–controller)是软件工程中的一种软件架构模式,把软件系统分为三个基本部分:模型(Model)、视图(View)和控制器(Controller)。MVC 模式的目的是实现一种动态的程序设计,使后续对程序的修改和扩展简化,并且使程序某一部分的重复利用成为可能。除此之外,此模式通过对复杂度的简化,使程序结构更加直观。

+

MVP

Model-view-presenter,简称MVP,是电脑软件设计工程中一种对针对MVC模式,再审议后所延伸提出的一种软件设计模式。被广范用于便捷自动化单元测试和在呈现逻辑中改良分离关注点(separation of concerns)。

+

MVVM

MVVM(Model–view–viewmodel)是一种软件架构模式,有助于将图形用户界面的开发与业务逻辑或后端逻辑(数据模型)的开发分离开来,这是通过置标语言或 GUI 代码实现的。

+

OLAP

联机分析处理(英语:On-Line Analytical Processing),是一套以多维度方式分析数据,而能弹性地提供积存(英语:Roll-up)、下钻(英语:Drill-down)、和透视分析(英语:pivot)等操作,呈现集成性决策信息的方法,多用于决策支持系统、商务智能或数据仓库。其主要的功能,在于方便大规模数据分析及统计计算,对决策提供参考和支持。与之相区别的是联机交易处理(OLTP)。

+

SQL

SQL(结构化查询语言)是一种特定目的程序语言,用于管理关系数据库管理系统(RDBMS),或在关系流数据管理系统(RDSMS)中进行流处理。

+

SPA

单页 Web 应用(single page web application),就是只有一张 Web 页面的应用,是加载单个 HTML 页面并在用户与应用程序交互时动态更新该页面的 Web 应用程序。

+

SOA

面向服务的体系结构(英语:service-oriented architecture)并不特指一种技术,而是一种分散式运算的软件设计方法。软件的部分组件(呼叫者),可以透过网络上的通用协定呼叫另一个应用软件元件执行、运作,让呼叫者获得服务。SOA原则上采用开放标准、与软件资源进行交互并采用表示的标准方式。因此应能跨越厂商、产品与技术。一项服务应视为一个独立的功能单元,可以远端存取并独立执行与更新,例如在线上线查询信用卡账单。

+

SOAP

SOAP(原为Simple Object Access Protocol的首字母缩写,即简单对象访问协议)是交换数据的一种协议规范,使用在计算机网络Web服务(web service)中,交换带结构信息。SOAP为了简化网页服务器(Web Server)从XML数据库中提取数据时,节省去格式化页面时间,以及不同应用程序之间按照HTTP通信协议,遵从XML格式执行资料互换,使其抽象于语言实现、平台和硬件。

+

NoSQL

NoSQL 是对不同于传统的关系数据库的数据库管理系统的统称。

+

XML

可扩展标记语言(英语:eXtensible Markup Language,简称:XML),是一种标记语言。标记指计算机所能理解的信息符号,通过此种标记,计算机之间可以处理包含各种信息的文章等。如何定义这些标记,既可以选择国际通用的标记语言,比如HTML,也可以使用像XML这样由相关人士自由决定的标记语言,这就是语言的可扩展性。XML是从标准通用标记语言(SGML)中简化修改出来的。它主要用到的有可扩展标记语言、可扩展样式语言(XSL)、XBRL和XPath等。

+
+

专业名词

前端后端

前端(英语:front-end)和后端(英语:back-end)是描述进程开始和结束的通用词汇。前端作用于采集输入信息,后端进行处理。计算机程序的界面样式,视觉呈现属于前端。

+

乐观锁

在关系数据库管理系统里,乐观并发控制(又名“乐观锁”,Optimistic Concurrency Control,缩写“OCC”)是一种并发控制的方法。它假设多用户并发的事务在处理时不会彼此互相影响,各事务能够在不产生锁的情况下处理各自影响的那部分数据。在提交数据更新之前,每个事务会先检查在该事务读取数据后,有没有其他事务又修改了该数据。如果其他事务有更新的话,正在提交的事务会进行回滚。

+

悲观锁

在关系数据库管理系统里,悲观并发控制(又名“悲观锁”,Pessimistic Concurrency Control,缩写“PCC”)是一种并发控制的方法。它可以阻止一个事务以影响其他用户的方式来修改数据。如果一个事务执行的操作读某行数据应用了锁,那只有当这个事务把锁释放,其他事务才能够执行与该锁冲突的操作。

+

自旋锁

自旋锁是计算机科学用于多线程同步的一种锁,线程反复检查锁变量是否可用。由于线程在这一过程中保持执行,因此是一种忙等待。一旦获取了自旋锁,线程会一直保持该锁,直至显式释放自旋锁。

+

递归

递归(英语:Recursion),又译为递回,在数学与计算机科学中,是指在函数的定义中使用函数自身的方法。递归一词还较常用于描述以自相似方法重复事物的过程。例如,当两面镜子相互之间近似平行时,镜中嵌套的图像是以无限递归的形式出现的。也可以理解为自我复制的过程。

+

主键

主键,又称主码(英语:primary key或unique key)。数据库表中对储存数据对象予以唯一和完整标识的数据列或属性的组合。一个数据列只能有一个主键,且主键的取值不能缺失,即不能为空值(Null)。

+

外键

外键(英语:foreign key,台湾译外来键,又称外部键)。其实在关系数据库中,每个数据表都是由关系来连系彼此的关系,父数据表(Parent Entity)的主键(primary key)会放在另一个数据表,当做属性以创建彼此的关系,而这个属性就是外键。

+

B/S结构

浏览器-服务器(Browser/Server)结构,与C/S结构不同,其客户端不需要安装专门的软件,只需要浏览器即可,浏览器通过Web服务器与数据库进行交互,可以方便的在不同平台下工作;服务器端可采用高性能计算机,并安装Oracle、Sybase、Informix等大型数据库。B/S结构简化了客户端的工作,它是随着Internet技术兴起而产生的,对C/S技术的改进,但该结构下服务器端的工作较重,对服务器的性能要求更高。

+

C/S结构

主从式架构 (英语:Client–server model) 也称客户端-服务器(Client/Server)架构、C/S架构,是一种网络架构,它把客户端 (Client) (通常是一个采用图形用户界面的程序)与服务器 (Server) 区分开来。每一个客户端软件的实例都可以向一个服务器或应用程序服务器发出请求。有很多不同类型的服务器,例如文件服务器、游戏服务器等。

+

Web服务

根据W3C的定义,Web服务(Web service)应当是一个软件系统,用以支持网络间不同机器的互动操作。网络服务通常是许多应用程序接口(API)所组成的,它们透过网络,例如国际互联网(Internet)的远程服务器端,执行客户所提交服务的请求。

+ +
+ + + + + + + +
+ + + + + + +
+ +
+ + + +
+ + + + + + + + +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2018/FUCK-AdBlock/index.html b/2018/FUCK-AdBlock/index.html new file mode 100644 index 0000000000..981e07a913 --- /dev/null +++ b/2018/FUCK-AdBlock/index.html @@ -0,0 +1,491 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 被 AdBlock 坑了 | 贾攀的流水账 + + + + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

贾攀的流水账

+ +
+

Panmax's Blog

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+ + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ 被 AdBlock 坑了 +

+ + +
+ + + + +
+ + +

今天写了一个接口,用 postman 测试没有问题,但是用 swagger 测试一直无法访问。

+

刚开始怀疑是浏览器缓存,换火狐后发现也访问不了。然后怀疑是开了代理的原因,关掉 Surge 后发现还是不行。接着怀疑是 swagger 的 bug,Google 搜索 swagger admin banners 的关键字并没有发现什么有用的信息。

+

这个接口的 URL 是:/api/admin/banners,我修改了接口对应的 URL 后再次尝试,发现又正常了,然后经过各种尝试,发现只要路径中带有 admin/banners 就无法访问,我把浏览器发送的请求转为 cURL 命令,使用终端发送也是正常的,所以确定问题应该出在浏览器身上。

+

这时候想起来我在上家公司工作时,写的一个广告接口,URL 中带有 advertisement,刚开始也是无法请求,最后发现是装了屏蔽广告的插件导致的,这次一定也是因为这个问题,所以我尝试把 AdBlock 关掉,再次测试,一切 ok。

+

这中间差不多花费了20多分钟来排查问题,没有很快找到问题的一个重要原因是,我的火狐浏览器恰好也装了 AdBlock,所以早早的就把浏览器的问题排除了。

+

最后兜了个圈又回到原点。

+ +
+ + + + + + + +
+ + + + + + +
+ +
+ + + +
+ + + + + + + + +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git "a/2018/Gradle-\347\254\224\350\256\260/1.png" "b/2018/Gradle-\347\254\224\350\256\260/1.png" new file mode 100644 index 0000000000..16576d4646 Binary files /dev/null and "b/2018/Gradle-\347\254\224\350\256\260/1.png" differ diff --git "a/2018/Gradle-\347\254\224\350\256\260/2.png" "b/2018/Gradle-\347\254\224\350\256\260/2.png" new file mode 100644 index 0000000000..8a82756747 Binary files /dev/null and "b/2018/Gradle-\347\254\224\350\256\260/2.png" differ diff --git "a/2018/Gradle-\347\254\224\350\256\260/index.html" "b/2018/Gradle-\347\254\224\350\256\260/index.html" new file mode 100644 index 0000000000..6603cf29f7 --- /dev/null +++ "b/2018/Gradle-\347\254\224\350\256\260/index.html" @@ -0,0 +1,537 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Gradle 笔记 | 贾攀的流水账 + + + + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

贾攀的流水账

+ +
+

Panmax's Blog

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+ + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ Gradle 笔记 +

+ + +
+ + + + +
+ + +

Gradle的构建生命周期

+

Gradle 项目可以使用 Maven Plugin 将构建上传到 Maven 仓库中:

1
2
3
4
5
6
7
8
9
10
11
apply plugin: 'maven'
...
uploadArchives {
repositories.mavenDeployer {
repository(url: "http://localhost:8088/nexus/content/repositories/snapshots/") {
authentication(userName: "admin", password: "admin123")
pom.groupId = "com.juvenxu"
pom.artifactId = "account-captcha"
}
}
}
+

想在 build 时发布版本导 Maven 仓库中,只需要添加一行任务依赖配置即可:

1
build.dependsOn 'uploadArchives'
+

通过 gradle -q tasks 显示所有的任务

任务名称缩写

Gradle提高效率的一个办法就是能够在命令行输入任务名的驼峰简写,当你的任务名称非常长的时候这很有用

+

命令行选项

    +
  • -i:Gradle默认不会输出很多信息,你可以使用-i选项改变日志级别为INFO
  • +
  • -s:如果运行时错误发生打印堆栈信息
  • +
  • -q:只打印错误信息
  • +
  • -?-h,–help:打印所有的命令行选项
  • +
  • -b,–build-file:Gradle默认执行build.gradle脚本,如果想执行其他脚本可以使用这个命令,比如gradle -b test.gradle
  • +
  • –offline:在离线模式运行build,Gradle只检查本地缓存中的依赖
  • +
  • -D, –system-prop:Gradle作为JVM进程运行,你可以提供一个系统属性比如:-Dmyprop=myValue
  • +
  • -P,–project-prop:项目属性可以作为你构建脚本的一个变量,你可以传递一个属性值给build脚本,比如:-Pmyprop=myValue
  • +
  • tasks:显示项目中所有可运行的任务
  • +
  • properties:打印你项目中所有的属性值
  • +
+

指定 Main-Class

1
2
3
4
5
jar {
manifest {
attributes 'Main-Class': 'com.jpanj.hello.Hello'
}
}
+

gradle wrapper 的目录结构

+

所以,gradle目录gradlewgradlew.bat 都应该放在版本控制内。

+

包装器可以根据需求自定义,如访问外网受限时:

1
2
3
4
5
6
7
8
task wrapper(type: Wrapper) {
//Requested Gradle version
gradleVersion = '1.2'
//Target URL to retrieve Gradle wrapper distribution
distributionUrl = 'http://myenterprise.com/gradle/dists'
//Path where wrapper will be unzipped relative to Gradle home directory
distributionPath = 'gradle-dists'
}
+

更多包装器的特性查看:http://gradle.org/docs/current/dsl/org.gradle.api.tasks.wrapper.Wrapper.html

+

Gradle允许通过外部属性来定义自己的变量

外部属性一般存储在键值对中,要添加一个属性,需要使用ext命名空间:
1
2
3
4
5
6
7
8
9
10
//Only initial declaration of extra property requires you to use ext namespace
project.ext.myProp = 'myValue'
ext {
someOtherProp = 123
}

//Using ext namespace to access extra property is optional
assert myProp == 'myValue'
println project.someOtherProp
ext.someOtherProp = 567
+
外部属性可以定义在一个属性文件中: 通过在 /.gradle路径 或者项目根目录下的 gradle.properties 文件来定义属性,可以直接注入到项目中:

假设在 gradle.properties 文件中定义了下面的属性:

+
1
2
exampleProp = myValue
someOtherProp = 455
+

可以在项目中访问这两个变量:

+
1
2
3
4
5
assert project.exampleProp == 'myValue'

task printGradleProperty << {
println "Second property: $someOtherProp"
}
+
定义属性的其他方法
    +
  • 通过 -P 命令行选项来定义项目属性
  • +
  • 通过 -D 命令行选项来定义系统属性
  • +
  • 环境属性遵循这个模式:ORG_GRADLE_PROJECT_propertyName=someValue
  • +
+

当任务创建的时候你可以添加任意多个动作,每一个任务都有一个动作清单,他们在运行的时候是执行的

1
2
3
4
5
6
7
8
9
10
11
12
13
task printVersion {
//任务的初始声明可以添加first和last动作
doFirst {
println "Before reading the project version"
}

doLast {
println "Version: $version"
}
}

printVersion.doFirst { println "11111" }
printVersion.doLast { println "22222" }
+

输出

+
1
2
3
4
5
> Task :printVersion
11111
Before reading the project version
Version: 0.1
22222
+
+

当你想添加动作的那个任务不是你自己写的时候这会非常有用,你可以添加一些自定义的逻辑,比如你可以添加 doFirst 动作到 compile-Java 任务来检查项目是否包含至少一个 source 文件。

+
+

dependsOn方法用来声明一个任务依赖于一个或者多个任务

1
2
3
4
5
6
7
8
9
10
11
task first << { println "first" }
task second << { println "second" }

//声明多个依赖
task printVersion(dependsOn: [second, first]) << {
logger.quiet "Version: $version"
}

task third << { println "third" }
//通过任务名称来声明依赖
third.dependsOn('printVersion')
+

输出

+
1
2
3
4
first
second
Version: 0.1
third
+
+

Gradle 并不保证依赖的任务能够按顺序执行,dependsOn方法只是定义这些任务应该在这个任务之前执行,但是这些依赖的任务具体怎么执行它并不关心(也就是和 [second, first] 顺序 无关),因为任务不是顺序执行的,就可以并发的执行来提高性能。

+
+

可以使用 finalizedBy 使一个任务结束后自动触发另一个

1
2
3
4
task first << { println "first" }
task second << { println "second" }
//声明first结束后执行second任务
first.finalizedBy second
+

first 结束后自动触发任务 second

+
1
2
3
4
5
6

> Task :first
first

> Task :second
second
+

allprojects 和 subprojects

你可以用 allprojects 方法给所有的项目添加 groupversion 属性,由于根项目不需要 Java 插件,你可以使用 subprojects 给所有子项目添加Java插件:

+
1
2
3
4
5
6
7
8
allprojects {
group = 'com.manning.gia'
version = '0.1'
}

subprojects {
apply plugin: 'java'
}
+ +
+ + + + + + + +
+ + + + + + +
+ +
+ + + +
+ + + + + + + + +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git "a/2018/SELinux-\345\274\200\345\220\257\345\257\274\350\207\264-Nginx-\345\220\257\345\212\250\345\244\261\350\264\245/index.html" "b/2018/SELinux-\345\274\200\345\220\257\345\257\274\350\207\264-Nginx-\345\220\257\345\212\250\345\244\261\350\264\245/index.html" new file mode 100644 index 0000000000..630199f7b3 --- /dev/null +++ "b/2018/SELinux-\345\274\200\345\220\257\345\257\274\350\207\264-Nginx-\345\220\257\345\212\250\345\244\261\350\264\245/index.html" @@ -0,0 +1,500 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + SELinux 开启导致 Nginx 启动失败 | 贾攀的流水账 + + + + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

贾攀的流水账

+ +
+

Panmax's Blog

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+ + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ SELinux 开启导致 Nginx 启动失败 +

+ + +
+ + + + +
+ + +

今天在启动一个 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 导致的,平时用到的不多,直接将其关闭即可。

+

查看状态

+
1
2
/usr/sbin/sestatus -v      ##如果SELinux status参数为enabled即为开启状态
SELinux status: enabled
+

or

+
1
getenforce                 ##也可以用这个命令检查
+

关闭SELinux:

+

临时关闭(不用重启机器):

+
1
2
setenforce 0                  ##设置SELinux 成为permissive模式
##setenforce 1 设置SELinux 成为enforcing模式
+

修改配置文件需要重启机器:

+
1
2
修改/etc/selinux/config 文件
将SELINUX=enforcing改为SELINUX=disabled
+

重启机器即可

+

看了下系统中并没有进程在占用 9090 端口。最后通过 http://www.err123.com/2017/08/29/nginx-emerg-bind-to-0-0-0-0-8081-failed-13-permission-denied/?lang=en 这篇文章的最后一个方案解决了这个问题。

+

已上外链已失效。

+

大致看了下 SELinxu 的作用一个 Linux 的强化方案,大部分情况下是需要启动的 https://www.zhihu.com/question/20559538

+ +
+ + + + + + + +
+ + + + + + +
+ +
+ + + +
+ + + + + + + + +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git "a/2018/SpringBoot-\344\270\255\347\273\237\344\270\200\345\214\205\350\243\205\345\223\215\345\272\224/index.html" "b/2018/SpringBoot-\344\270\255\347\273\237\344\270\200\345\214\205\350\243\205\345\223\215\345\272\224/index.html" new file mode 100644 index 0000000000..0f17ea8f3e --- /dev/null +++ "b/2018/SpringBoot-\344\270\255\347\273\237\344\270\200\345\214\205\350\243\205\345\223\215\345\272\224/index.html" @@ -0,0 +1,512 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + SpringBoot 中统一包装响应 | 贾攀的流水账 + + + + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

贾攀的流水账

+ +
+

Panmax's Blog

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+ + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ SpringBoot 中统一包装响应 +

+ + +
+ + + + +
+ + +

SpringBoot 中可以基于 ControllerAdviceHttpMessageConverter 实现对数据返回的包装。

+

实现如下,先来写一个 POJO 来定义一下返回格式:

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
import com.example.demo.common.exception.base.ErrorCode;
import lombok.AllArgsConstructor;
import lombok.Getter;
import org.springframework.http.HttpStatus;

@Getter
@AllArgsConstructor
public class Response<T> {

private int code = HttpStatus.OK.value();

private String msg = "success";

private T data;

public Response(T data) {
this.data = data;
}

public Response(int code, String msg) {
this.code = code;
this.msg = msg;
}

public Response(int code, T data) {
this.code = code;
this.data = data;
}

public Response(ErrorCode errorCode) {
this.code = errorCode.getCode();
this.msg = errorCode.getMessage();
}

public Response(ErrorCode errorCode, T data) {
this.code = errorCode.getCode();
this.msg = errorCode.getMessage();
this.data = data;
}
}
+
+

这里用到了 lomboklombok 的使用介绍不在本文范围内。

+
+

用一个 ResponseBodyAdvice 类的实现包装 Controller 的返回值:

+

以下是我以前的实现方式:

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
import com.example.demo.common.RequestContextHolder;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.MethodParameter;
import org.springframework.core.annotation.Order;
import org.springframework.http.MediaType;
import org.springframework.http.server.ServerHttpRequest;
import org.springframework.http.server.ServerHttpResponse;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice;

@ControllerAdvice
public class FormatResponseBodyAdvice implements ResponseBodyAdvice {
private static Logger logger = LoggerFactory.getLogger(FormatResponseBodyAdvice.class);

@Autowired
private ObjectMapper objectMapper;

@Override
public boolean supports(MethodParameter returnType, Class converterType) {
return true;
}

@Override
public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType, Class selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) {

Object wrapperBody = body;
try {
if (!(body instanceof Response)) {
if (body instanceof String) {
wrapperBody = objectMapper.writeValueAsString(new Response<>(body));
} else {
wrapperBody = new Response<>(body);
}
}
} catch (Exception e) {
logger.error("request uri path: {}, format response body error", request.getURI().getPath(), e);
}
return wrapperBody;
}

}
+

为什么要对返回类型是 String 时进行特殊处理呢?因为如果直接返回 new Response<>(body) 的话,在使用时返回 String 类型的话,会报类型转换异常,当时也没有理解什么原因导致的,所以最后使用了 jacksonResponse 又做了一次序列化。

+

今天找到了导致这个异常的原因:

+
+

因为在所有的 HttpMessageConverter 实例集合中,StringHttpMessageConverter 要比其它的 Converter 排得靠前一些。我们需要将处理 Object 类型的 HttpMessageConverter 放得靠前一些,这可以在 Configuration 类中完成:

+
+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import org.springframework.context.annotation.Configuration;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

import java.util.List;

@Configuration
public class WebConfiguration implements WebMvcConfigurer {

@Override
public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
converters.add(0, new MappingJackson2HttpMessageConverter());
}
}
+

然后 FormatResponseBodyAdvice 就可以修改为如下实现:

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
import org.springframework.core.MethodParameter;
import org.springframework.http.MediaType;
import org.springframework.http.server.ServerHttpRequest;
import org.springframework.http.server.ServerHttpResponse;
import org.springframework.http.server.ServletServerHttpRequest;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice;


@ControllerAdvice
public class FormatResponseBodyAdvice implements ResponseBodyAdvice {

@Override
public boolean supports(MethodParameter returnType, Class converterType) {
return true;
}

@Override
public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType,
Class selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) {

if (!(body instanceof Response)) {
return new Response<>(body);
}

return body;

}
}
+

比之前的实现方式优雅了很多而且不用再处理 jackson 的异常了。

+

写一个 Controller 来尝试一下:

+
1
2
3
4
5
6
7
8
9
@RestController
public class HelloController {

@GetMapping("/hello")
public String hello() {
return "hello world!";
}

}
+

请求这个端点得到结果:

+
1
2
3
4
5
{
"code": 200,
"msg": "success",
"data": "hello world!"
}
+

说明我们的配置是成功的,同时可以在相应头中看到:

+
1
content-type: application/json;charset=UTF-8
+

如果是之前的实现方式,这里的值就是:

+
1
content-type: html/text
+

也不太符合 restful 规范。

+ +
+ + + + + + + +
+ + + + + + +
+ +
+ + + +
+ + + + + + + + +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2018/centos-install-epel-repo/index.html b/2018/centos-install-epel-repo/index.html new file mode 100644 index 0000000000..f65bff81a5 --- /dev/null +++ b/2018/centos-install-epel-repo/index.html @@ -0,0 +1,503 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Centos 安装并启用 EPEL 源 | 贾攀的流水账 + + + + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

贾攀的流水账

+ +
+

Panmax's Blog

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+ + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ Centos 安装并启用 EPEL 源 +

+ + +
+ + + + +
+ + +

Centos 默认提供的软件源资源很少,很多常用软件都没有:如 nginx,htop 等。

+

EPEL(Extra Packages for Enterprise Linux) 是由 Fedora Special Interest Group 维护的 Enterprise Linux(RHEL、CentOS)中经常用到的包。

+

通过 EPEL 可以很容易地通过 yum 命令从 EPEL 源上获取在 CentOS 自带源上没有的软件。

+

首先安装 epel-release:

+
1
yum install epel-release
+

大多数网站到了这一步就告诉你安装好了,但是在我尝试的时候,发现这种方式 EPEL 源默认并不会生效,可以通过下边的命令进行验证:

+
1
yum repolist | grep epel
+

如果发现有类似下边的结果,说明 EPEL 源已生效:

+
1
epel/x86_64           Extra Packages for Enterprise Linux 7 - x86_64      12,716
+

如果没有输出这条命令,说明 EPEL 源默认没有开启,在安装软件时还需要手动指定源:

+
1
yum --enablerepo=epel install nginx
+

这种方式使用时比较麻烦,我们可以通过修改 EPEL 的配置文件来启用它。

+
1
vi /etc/yum.repos.d/epel.repo
+

可以看到里边有多个组,将 [epel] 组内的 enabled=0 改成 enabled=1

+

这样就可以开启 EPEL 源了。

+
+

参考:https://unix.stackexchange.com/questions/165916/trying-to-enable-epel-on-centos-6-and-it-wont-show-in-repolist

+
+ +
+ + + + + + + +
+ + + + + + +
+ +
+ + + +
+ + + + + + + + +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2018/docker-compose-ELK/index.html b/2018/docker-compose-ELK/index.html new file mode 100644 index 0000000000..3c33a34cc3 --- /dev/null +++ b/2018/docker-compose-ELK/index.html @@ -0,0 +1,489 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 使用 docker-compose 一键部署 ELK | 贾攀的流水账 + + + + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

贾攀的流水账

+ +
+

Panmax's Blog

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+ + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ 使用 docker-compose 一键部署 ELK +

+ + +
+ + + + +
+ + +

前两天使用docker 通过一个一个启动的方式,将 ELK 部署了起来,但是逐个启动的方式有些麻烦,所以写了个 docker-compose.yml 来一键启动:

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
version: '2'
services:
elasticsearch:
image: docker.elastic.co/elasticsearch/elasticsearch:6.4.0
environment:
- discovery.type=single-node
volumes:
- /etc/localtime:/etc/localtime
- /data01/docker-es/data:/usr/share/elasticsearch/data
# ports:
# - "9200:9200"
# - "9300:9300"
logstash:
image: docker.elastic.co/logstash/logstash:6.4.0
volumes:
- ./logstash.conf:/usr/share/logstash/pipeline/logstash.conf
ports:
- "4560:4560"
links:
- elasticsearch
kibana:
image: docker.elastic.co/kibana/kibana:6.4.0
environment:
- ELASTICSEARCH_URL=http://elasticsearch:9200
volumes:
- /etc/localtime:/etc/localtime
ports:
- "5601:5601"
links:
- elasticsearch
+

logstash.conf

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
input {
tcp {
mode => "server"
host => "0.0.0.0"
port => 4560
codec => json
}
}
output {
elasticsearch {
hosts => ["http://elasticsearch:9200"]
index => "%{[service]}-%{+YYYY.MM.dd}"
}
stdout { codec => rubydebug }
}
+ +
+ + + + + + + +
+ + + + + + +
+ +
+ + + +
+ + + + + + + + +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2018/docker-mysql-utf8mb4/index.html b/2018/docker-mysql-utf8mb4/index.html new file mode 100644 index 0000000000..47fcc96e16 --- /dev/null +++ b/2018/docker-mysql-utf8mb4/index.html @@ -0,0 +1,504 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + docker 部署 MySQL 使用 utf8mb4 字符集 | 贾攀的流水账 + + + + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

贾攀的流水账

+ +
+

Panmax's Blog

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+ + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ docker 部署 MySQL 使用 utf8mb4 字符集 +

+ + +
+ + + + +
+ + +

背景

+

utf8 是 MySQL 早期版本中支持的一种字符集,只支持最长三个字节的 UTF-8 字符,也就是 Unicode 中的基本多文本平面。这可能是因为在 MySQL 发布初期,基本多文种平面之外的字符确实很少用到。而在 MySQL5.5.3 版本后,要在 MySQL 中保存 4 字节长度的 UTF-8 字符,就可以使用 utf8mb4 字符集了。例如可以用 utf8mb4 字符编码直接存储 emoj 表情,而不是存表情的替换字符。

+
+

正文

相信好多人都被 emoji 表情在 MySQL 中存储的问题坑过,被坑过的人都记住了在 MySQL 中创建库表时要使用 utf8mb4,而不是 utf8。

+

但如果 MySQL 的服务端没有设置为 utf8mb4 的话,使用 jdbc 往里写 emoji 同样会报错,即便是指定了 characterEncoding=utf8 也不会起作用,并且 characterEncoding 不支持设置为 utf8mb4

+

报错通常为:

+
1
Incorrect string value: '\xF0\x9F...' for column 'xxx' at row 1
+

解决办法是将服务端的默认字符集改为 utf8mb4。我们的 MySQL 是使用 docker 启动的,需要把配置文件映射进去。在这里我们踩过一次坑,之前我们是将 my.conf 文件映射到容器的 /etc/my.cnf,实际使用时发现配置文件并没有生效,需要映射到 /etc/mysql/my.cnf 才能生效。

+

解决问题:

先查看一下当前数据库的字符集:

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
> show variables like '%char%';

+--------------------------+----------------------------+
| Variable_name | Value |
+--------------------------+----------------------------+
| character_set_client | latin1 |
| character_set_connection | latin1 |
| character_set_database | latin1 |
| character_set_filesystem | binary |
| character_set_results | latin1 |
| character_set_server | latin1 |
| character_set_system | utf8 |
| character_sets_dir | /usr/share/mysql/charsets/ |
+--------------------------+----------------------------+
+

发现默认都是 latin1,现在我们在 my.conf 中的相应模块中加入如下配置,然后重启 MySQL:

+
1
2
3
4
5
6
7
8
9
[client]
default-character-set=utf8mb4

[mysql]
default-character-set=utf8mb4

[mysqld]
character-set-server=utf8mb4
collation-server=utf8mb4_general_ci
+

再次查看字符集:

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
> show variables like '%char%';

+--------------------------+----------------------------+
| Variable_name | Value |
+--------------------------+----------------------------+
| character_set_client | utf8mb4 |
| character_set_connection | utf8mb4 |
| character_set_database | utf8mb4 |
| character_set_filesystem | binary |
| character_set_results | utf8mb4 |
| character_set_server | utf8mb4 |
| character_set_system | utf8 |
| character_sets_dir | /usr/share/mysql/charsets/ |
+--------------------------+----------------------------+
+

发现已经更改为了 utf8mb4,再次尝试插入 emoji 成功,问题解决。

+

补充说明:

jdbc 在连接 MySQL 时,如果不指定 characterEncoding 会默认使用 MySQL 服务端 的字符集,因为之前我们的 MySQL 服务端字符集为 latin1,所以手动指定了一下 characterEncoding=utf8,但这样使用的是 utf8 编码建立连接,所以依旧不能插入 emoji。

+

在修改为 utf8mb4 之后也就不用设置 characterEncoding=utf8useUnicode=true 参数了(我尝试了下,不去掉也没有什么问题)。

+

docker MySQL 启动命令

1
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
+ +
+ + + + + + + +
+ + + + + + +
+ +
+ + + +
+ + + + + + + + +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git "a/2018/docker-\344\275\277\347\224\250\347\254\224\350\256\260/index.html" "b/2018/docker-\344\275\277\347\224\250\347\254\224\350\256\260/index.html" new file mode 100644 index 0000000000..05a9225eb7 --- /dev/null +++ "b/2018/docker-\344\275\277\347\224\250\347\254\224\350\256\260/index.html" @@ -0,0 +1,496 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + docker 使用笔记 | 贾攀的流水账 + + + + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

贾攀的流水账

+ +
+

Panmax's Blog

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+ + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ docker 使用笔记 +

+ + +
+ + + + +
+ + +

常用启动命令

1
docker run -d -p 5001:5000 --rm --name xxx jiapan/some-image:label
+

编译镜像

1
docker build -t jiapan/some-image:label .
+

推送镜像到仓库

1
docker push jiapan/some-image:label
+

进入正在运行的docker

1
docker exec -it <container_name> /bin/bash
+

更改镜像时区(适用于 Ubuntu)

Dockerfile 内添加:

+
1
2
RUN echo "Asia/Shanghai" > /etc/timezone
RUN dpkg-reconfigure -f noninteractive tzdata
+ +
+ + + + + + + +
+ + + + + + +
+ +
+ + + +
+ + + + + + + + +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2018/eureka-StringCache-get-new-knowledge/index.html b/2018/eureka-StringCache-get-new-knowledge/index.html new file mode 100644 index 0000000000..fa64c8586a --- /dev/null +++ b/2018/eureka-StringCache-get-new-knowledge/index.html @@ -0,0 +1,523 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 阅读 Eureka 的 StringCache 源码 get 到的知识 | 贾攀的流水账 + + + + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

贾攀的流水账

+ +
+

Panmax's Blog

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+ + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ 阅读 Eureka 的 StringCache 源码 get 到的知识 +

+ + +
+ + + + +
+ + +

今天在读 Eureka 源码时,看到了它里边实现了一个工具类 StringCache 阅读后我产生了几个疑问,查阅资料后一一进行了解决,受益良多并以此文进行记录。

+

StringCache 实现了一个字符串缓存,代码如下

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
public class StringCache {

public static final int LENGTH_LIMIT = 38;

private static final StringCache INSTANCE = new StringCache();

private final ReadWriteLock lock = new ReentrantReadWriteLock();
private final Map<String, WeakReference<String>> cache = new WeakHashMap<String, WeakReference<String>>();
private final int lengthLimit;

public StringCache() {
this(LENGTH_LIMIT);
}

public StringCache(int lengthLimit) {
this.lengthLimit = lengthLimit;
}

public String cachedValueOf(final String str) {
if (str != null && (lengthLimit < 0 || str.length() <= lengthLimit)) {
// Return value from cache if available
try {
lock.readLock().lock();
WeakReference<String> ref = cache.get(str);
if (ref != null) {
return ref.get();
}
} finally {
lock.readLock().unlock();
}

// Update cache with new content
try {
lock.writeLock().lock();
WeakReference<String> ref = cache.get(str);
if (ref != null) {
return ref.get();
}
cache.put(str, new WeakReference<>(str));
} finally {
lock.writeLock().unlock();
}
return str;
}
return str;
}

public int size() {
try {
lock.readLock().lock();
return cache.size();
} finally {
lock.readLock().unlock();
}
}

public static String intern(String original) {
return INSTANCE.cachedValueOf(original);
}
}
+

什么是字符串常量池?

1
2
3
String s = "a" + "bc";
String t = "ab" + "c";
System.out.println(s == t);
+

上边这段程序会打印 true(尽管我们没有使用正确比较字符串的 equals 方法)

+

当编译器优化字符串的字面值时,它看到 st 有相同的值,因为字符串在 Java 中是不可变的,所以提供同一个字符串对象也是安全的,因此 st 指向了同一个对象并且节省了一丢丢的内存。

+

「字符串常量池」的灵感来源于这样的想法:所有已定义的字符串都存储在一个「池子」中,在创建新的 String 对象前,编译器需要检查这个字符串是否已经被定义,若已经在「池子」中存在就直接拿出来用。

+

也就是说 Java 编译器已经用字符串常量池实现了字符串缓存的特性,在我们直接使用双引号来声明 String 对象时会自动利用以上特性,如果不是用双引号声明的,可以用 String 提供的 intern() 方法。intern() 方法会从字符串常量池中查询当前字符串是否存在,若不存在就会将当前字符串放入常量池中。

+

示例程序:

+
1
2
3
4
5
6
7
8
9
10
String a1 = "aaa";
String a2 = "aaa";
String a3 = new String("aaa");
System.out.println(a1 == a2); // true
System.out.println(a1 == a3); // false
System.out.println(a1 == a3.intern()); // true

String b1 = new String("bbb").intern();
String b2 = "bbb";
System.out.println(b1 == b2); // true
+

为什么 Eureka 要再造轮子?

既然 Java 编译器已经对相同的字符串进行了优化,为什么 Eureka 还要再造一个轮子呢,因为字符串常量池在存储大量的字符串后,会出现严重的性能问题。

+

以下解释来自美团点评技术团队编写的 深入解析String#intern 一文:

+
+

Java 使用 JNI 调用 C++ 实现的 StringTable 的 intern 方法,StringTable 的 intern 方法跟 Java 中的 HashMap 的实现是差不多的,只是不能自动扩容。默认大小是1009。

+
+
+

要注意的是,String 的 String Pool 是一个固定大小的 Hashtable,默认值大小长度是1009,如果放进 String Pool 的 String 非常多,就会造成 Hash 冲突严重,从而导致链表会很长,而链表长了后直接会造成的影响就是当调用 String.intern 时性能会大幅下降(因为要一个一个找)。

+
+

好了原因解释清楚了,我们来看一下具体实现中有那哪些问题。

+

WeakHashMap 和 HashMap 有什么区别?

StringCache 的代码不难理解,大致就是声明一个锁和一个 Map,取值时先获取锁,如果存在就直接从 Mapget 出来然后返回,不存在就 put 进去作为缓存以便下次使用。

+

这里声明的 Map 类型是 WeakHashMap,这种 Map 的特点是,当除了自身有对 key 的引用外,此 key 没有其他引用那么这个 map 会自动丢弃此值。

+

示例程序:

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
String a = new String("a");
String b = new String("b");
Map<String, String> weakmap = new WeakHashMap();
Map<String, String> map = new HashMap();
map.put(a, "aaa");
map.put(b, "bbb");

weakmap.put(a, "aaa");
weakmap.put(b, "bbb");

map.remove(a);
a = null;
b = null;

System.gc();
Iterator i = map.entrySet().iterator();
while (i.hasNext()) {
Map.Entry en = (Map.Entry)i.next();
System.out.println("map: "+en.getKey()+":"+en.getValue());
}

Iterator j = weakmap.entrySet().iterator();
while (j.hasNext()) {
Map.Entry en = (Map.Entry)j.next();
System.out.println("weakmap: "+en.getKey()+":"+en.getValue());
}
+

我们声明了两个 Map 对象,一个是 HashMap,一个是 WeakHashMap,同时向两个 map 中放入 ab 两个对象,从 HashMapremovea 并且将 ab 都指向 null 时,WeakHashMap 中的 a 将自动被回收掉。出现这个状况的原因是,对于 a 对象而言,当从 HashMapremovea 并且将 a 指向 null 后,除了 WeakHashMap 中还保存 a 外已经没有指向 a 的指针了,所以 WeakHashMap 会自动舍弃掉 a,而对于 b 对象虽然指向了null,但 HashMap 中还有指向 b 的指针,所以 WeakHashMap 将会保留 b

+

以上程序得到的结果是:

+
1
2
map: b:bbb
weakmap: b:bbb
+

WeakReference 和普通的引用有什么区别?

可以看到我们的 StringCache 中的 Map 值类型用的是 WeakReference<String>,如果你希望能随时取得某对象的信息,但又不想影响此对象的垃圾收集,那么你应该用 Weak Reference 来记住此对象,而不是用一般的 reference

+

如果不这样用,会导致我们 Map 的值也会引用我们想缓存的字符串,这就导致即使 key 已经没有任何地方引用了,这个 WeakHashMap 也不会丢弃此值。

+

ReentrantReadWriteLock 有什么特性?

ReentrantReadWriteLock 是一个读写锁:分为读锁和写锁,多个读锁不互斥,读锁与写锁互斥,这是由 jvm 自己控制的,你只要上好相应的锁即可。如果你的代码只读数据,可以很多人同时读,但不能同时写,那就上读锁;如果你的代码修改数据,只能有一个人在写,且不能同时读取,那就上写锁。总之,读的时候上读锁,写的时候上写锁。

+

线程进入读锁的前提条件:

    +
  • 没有其他线程的写锁,
  • +
  • 没有写请求或者有写请求,但调用线程和持有锁的线程是同一个
  • +
+

线程进入写锁的前提条件:

    +
  • 没有其他线程的读锁
  • +
  • 没有其他线程的写锁
  • +
+ +
+ + + + + + + +
+ + + + + + +
+ +
+ + + +
+ + + + + + + + +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2018/fried-chicken-breast-tutorial/1.jpg b/2018/fried-chicken-breast-tutorial/1.jpg new file mode 100644 index 0000000000..e8d4e87ae6 Binary files /dev/null and b/2018/fried-chicken-breast-tutorial/1.jpg differ diff --git a/2018/fried-chicken-breast-tutorial/2.jpg b/2018/fried-chicken-breast-tutorial/2.jpg new file mode 100644 index 0000000000..3e9cda751b Binary files /dev/null and b/2018/fried-chicken-breast-tutorial/2.jpg differ diff --git a/2018/fried-chicken-breast-tutorial/3.jpg b/2018/fried-chicken-breast-tutorial/3.jpg new file mode 100644 index 0000000000..eff41c2f77 Binary files /dev/null and b/2018/fried-chicken-breast-tutorial/3.jpg differ diff --git a/2018/fried-chicken-breast-tutorial/index.html b/2018/fried-chicken-breast-tutorial/index.html new file mode 100644 index 0000000000..f6a0fd92f5 --- /dev/null +++ b/2018/fried-chicken-breast-tutorial/index.html @@ -0,0 +1,505 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Panmax 的香煎鸡胸肉教程 | 贾攀的流水账 + + + + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

贾攀的流水账

+ +
+

Panmax's Blog

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+ + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ Panmax 的香煎鸡胸肉教程 +

+ + +
+ + + + +
+ + +

可能很多人不知道,我的第二职业是厨师,之后会考虑写一些简单易做的美食文章。

+

今天尝试做了一次煎鸡胸肉,味道非常棒!有健身需求的朋友们可以了解一下。

+

做法非常简单:

+
    +
  1. 鸡胸肉划上斜刀用盐和料酒腌十分钟
  2. +
  3. 然后锅里倒一点点油(我用的橄榄油)油稍微冒烟后下鸡胸
  4. +
  5. 煎至两面变色后加入一大勺蚝油
  6. +
  7. 然后煎至两面金黄
  8. +
  9. 撒上黑胡椒再煎两分钟即可出锅
  10. +
  11. 装盘时可以用盐水煮芦笋和柠檬做装饰
  12. +
+

是不是非常简单?

+

最后,上几张图:

+

+

+

+ +
+ + + + + + + +
+ + + + + + +
+ +
+ + + +
+ + + + + + + + +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git "a/2018/hadoop-\344\270\212\344\274\240\346\226\207\344\273\266\346\227\266\346\212\245-Checksum-error/index.html" "b/2018/hadoop-\344\270\212\344\274\240\346\226\207\344\273\266\346\227\266\346\212\245-Checksum-error/index.html" new file mode 100644 index 0000000000..9fb6edac0e --- /dev/null +++ "b/2018/hadoop-\344\270\212\344\274\240\346\226\207\344\273\266\346\227\266\346\212\245-Checksum-error/index.html" @@ -0,0 +1,497 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + hadoop 上传文件时报 Checksum error | 贾攀的流水账 + + + + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

贾攀的流水账

+ +
+

Panmax's Blog

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+ + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ hadoop 上传文件时报 Checksum error +

+ + +
+ + + + +
+ + +

今天在用 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 系统为了保证数据的一致性,会对文件生成相应的校验文件,并在读写的时候进行校验,确保数据的准确性。

+

在上传的过程中,Hadoop 将通过 FSInputChecker 判断需要上传的文件是否存在进行校验的 crc 文件,即 .fb_friend.csv.crc,如果存在 crc 文件,将会对其内容一致性进行校验,如果校验失败,则停止上传该文件。

+

在使用 hadoop fs -getmerge srcDir destFile 命令时,本地磁盘一定会生成相应的 .crc 文件。

+

所以如果需要修改 getmerge 获取的文件的内容,再次上传到 DFS 时,可以采取以下 2 种策略进行规避:

+
    +
  1. 删除 .crc 文件

    +
  2. +
  3. getmerge 获取的文件修改后重新命名,如使用 mv 操作,再次上传到 DFS 中。

    +
  4. +
+ +
+ + + + + + + +
+ + + + + + +
+ +
+ + + +
+ + + + + + + + +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2018/install-jps-on-centos/1.png b/2018/install-jps-on-centos/1.png new file mode 100644 index 0000000000..e496f2b800 Binary files /dev/null and b/2018/install-jps-on-centos/1.png differ diff --git a/2018/install-jps-on-centos/index.html b/2018/install-jps-on-centos/index.html new file mode 100644 index 0000000000..eefec78e58 --- /dev/null +++ b/2018/install-jps-on-centos/index.html @@ -0,0 +1,494 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 解决在 CentOS 通过 yum 安装的 Java 没有 jps 的问题 | 贾攀的流水账 + + + + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

贾攀的流水账

+ +
+

Panmax's Blog

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+ + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ 解决在 CentOS 通过 yum 安装的 Java 没有 jps 的问题 +

+ + +
+ + + + +
+ + +

在公司的 CentOS 上通过 yum 安装了一个 Java,但是使用时发现没有 jps 命令,解决方法是安装 jdk-devel 这个包,它提供了 jps 工具。

+

先查看有哪些可用的安装包:

+

yum list | grep jdk-devel

+

+

然后找到对应自己 Java 版本和系统的那个包进行安装:

+

sudo yum install java-1.8.0-openjdk-devel.x86_64

+

搞定~

+ +
+ + + + + + + +
+ + + + + + +
+ +
+ + + +
+ + + + + + + + +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2018/jiapan-macbook/1.png b/2018/jiapan-macbook/1.png new file mode 100644 index 0000000000..caea900f41 Binary files /dev/null and b/2018/jiapan-macbook/1.png differ diff --git a/2018/jiapan-macbook/2.png b/2018/jiapan-macbook/2.png new file mode 100644 index 0000000000..135c2f9b64 Binary files /dev/null and b/2018/jiapan-macbook/2.png differ diff --git a/2018/jiapan-macbook/3.png b/2018/jiapan-macbook/3.png new file mode 100644 index 0000000000..72e42585ca Binary files /dev/null and b/2018/jiapan-macbook/3.png differ diff --git a/2018/jiapan-macbook/4.png b/2018/jiapan-macbook/4.png new file mode 100644 index 0000000000..66e0bbaf1c Binary files /dev/null and b/2018/jiapan-macbook/4.png differ diff --git a/2018/jiapan-macbook/5.png b/2018/jiapan-macbook/5.png new file mode 100644 index 0000000000..e2222c64b3 Binary files /dev/null and b/2018/jiapan-macbook/5.png differ diff --git a/2018/jiapan-macbook/6.png b/2018/jiapan-macbook/6.png new file mode 100644 index 0000000000..7d4dcecc7e Binary files /dev/null and b/2018/jiapan-macbook/6.png differ diff --git a/2018/jiapan-macbook/7.png b/2018/jiapan-macbook/7.png new file mode 100644 index 0000000000..deacc3def3 Binary files /dev/null and b/2018/jiapan-macbook/7.png differ diff --git a/2018/jiapan-macbook/index.html b/2018/jiapan-macbook/index.html new file mode 100644 index 0000000000..574b49c92b --- /dev/null +++ b/2018/jiapan-macbook/index.html @@ -0,0 +1,579 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 贾攀的 Macbook 使用指北 | 贾攀的流水账 + + + + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

贾攀的流水账

+ +
+

Panmax's Blog

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+ + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ 贾攀的 Macbook 使用指北 +

+ + +
+ + + + +
+ + +
+

工欲善其事,必先利其器

+
+

算起来我用 Macbook 也有三年多的时间了,中间由于一次工作原因,开发环境一直有问题,就重装了一次系统,也仅仅这一次,而且这次其实也是冤枉了系统,当时是因为我的 host 文件没有配置正确导致的,可以说 macOS 是相当稳定的,并且也是开发利器,我写这篇文章的目的主要是为了之后自己在换新的 Macbook 时有记录可寻,同时帮助其他 Macbook 用户来发现一些好用的工具。

+

以下神器排名分先后

iTerm2:终端神器

iTerm2 是 Mac 下最好的终端工具,大部分功能都是开箱即用,简单介绍下 iTerm2 的特色功能:

+

智能选中

在 iTerm2 中,双击选中单词,三击选中整行,四击智能选中(智能规则可配置),可以识别网址,引号引起的字符串,邮箱地址等。(很多时候双击的选中就已经很智能了)

+

在 iTerm2 中,选中即复制。即任何选中状态的字符串都被放到了系统剪切板中。

+

巧用 Command 键

按住⌘键:

+
    +
  • 可以拖拽选中的字符串
  • +
  • 点击 URL:调用默认浏览器访问该网址
  • +
  • 点击文件:调用默认程序打开文件
  • +
  • 点击文件夹:在 finder 中打开该文件夹
  • +
  • 同时按住 Option 键,可以以矩形选中
  • +
+

常用快捷键

    +
  • 切换 tab:⌘+←, ⌘+→, ⌘+{, ⌘+}⌘+数字 直接定位到该 tab
  • +
  • 新建 tab:⌘+t
  • +
  • 顺序切换 pane:⌘+[, ⌘+]
  • +
  • 按方向切换 pane:⌘+Option+方向键
  • +
  • 切分屏幕:⌘+d 水平切分,⌘+Shift+d 垂直切分
  • +
  • 智能查找,支持正则查找:⌘+f
  • +
+

+

用于搜索关键字,按 Tab 键可以自动补全单词,且补全的单词可以直接粘贴到其他地方

+

+

分屏功能很实用啊有木有

+

自动完成

iTerm2 可以自动补齐命令,输入若干字符,按 ⌘+; 弹出自动补齐窗口,列出当前可用的命令。

+

Exposé Tabs

⌘+Option+e 全屏展示所有的 tab,可以搜索

+

+

高亮当前鼠标的位置

一个标签页中开的窗口太多,有时候会找不到当前的鼠标,⌘+/ 找到它。

+

+

配色

你可以自由定制喜欢的配色,这里 收集了大量 iTerm2 的主题,你可以选择使用。我用的是Zenburn。在其 github repo 里下载对应的xxx.itermcolors文件,双击安装使用。

+
+

zsh:最强 shell

都用了这么好用的终端了,不考虑再换个 shell 吗?

+

zsh 的安装方法和介绍见:http://macshuo.com/?p=676

+
+

Moom:窗口调节神器

macOS 系统不能像 Windows 那样最大化是不是很不爽?一言不合就全屏!用 Moom 来解决这个问题吧!

+

安装后,将鼠标悬浮在你想调整窗口的全屏按钮上(就是那个绿色按钮),下方就会出现一些扩展选项:

+

+

从左到右依次为:最大化、将窗口平铺在屏幕左半边、将窗口平铺在屏幕右半边、将窗口平铺在屏幕上半边、将窗口平铺在屏幕下半边

+

这几个选项非常实用,比如你想打开一个网页同时打开一个笔记工具,这时你就可以直接让浏览器占用左半边,笔记工具占用右半边,不需要自己手动拖拽调整大小啦。

+
+

Paste:剪切板神器

复制粘贴是我们日常工作和开发中常用的功能,Paste 为我们提供了剪切板历史记录的功能,我们可以通过 cmd+shift+v 来查看记录,通过方向键选择我们需要的内容后敲回车完成之前复制内容的粘贴:

+

+
+

Keyboard Maestro:设置快捷键神器

我日常用 HHKB 来码字,这个键盘最大的优点就是小巧,最大的缺点也是小巧,很多键是没有的,比如方向键。

+

我通过 Keyboard Maestro 来设置一些组合键作为方向键,同时设置另外一些组合键作为 App 启动热键。

+

介绍看我另一篇博客:使用-KM-处理-HHKB-方向键/

+
+

Karabiner-Elements

这条是后来补充的,此时我已经将 HHKB 组合实现方向键的功能由楼上的 Keyboard Maestro 改为了 Karabiner-Elements,Keyboard Maestro 只留下了通过组合键启动应用的功能,Karabiner-Elements 可以更多的对键盘进行自定义,比如为了防止误触发,我开起了敲击 command+q 两次才退出应用的功能,同时还开启了当我接入外接键盘时,自动禁用自带键盘的功能。

+

+

+
+

Go2shell

+

已有楼下的 OpenInTerminal 代替

+
+

当你在 finder 中进入一个目录后,这时你想用命令行在这个目录中做一些操作,你需要手动打开终端然后一层一层 cd 进去。

+

Go2shell 来解救你吧,安装完之后,会在你的 finder 上部出现它的 logo,不管你当前在哪个目录,如果你想让你的命令行也进到这个目录中时,只需点一下那个小 logo 就行了:

+

+
+

OpenInTerminal

比 Go2shell 功能更丰富,OpenInTerminal 不仅可以直接打开终端 并 cd 到相应目录,同时还提供了复制路径、用编辑器打开的便捷功能。

+

+
+

Surge

不多介绍,官方定义为:「高级网络工具箱」。

+

+
+

Things3

我最喜欢的 GTD 应用,没有之一。

+

+
+

Enpass

我们普遍都有很多不同的帐号,生活中还有各种重要信息需要记忆,单纯靠脑子真的很难记忆和管理。而所有账号使用同一密码绝对是巨大的安全隐患,因此我们还需要一款安全可靠,而且足够方便使用的密码管理器软件。

+

Enpass 是一款安全可靠的跨平台密码管理器软件,提供了包括 Windows、Mac、Linux 以及 iOS、Android 在内的几乎所有平台的客户端,并且提供主流浏览器的一键登录扩展,基本能覆盖你所有的密码应用场景。

+

+
+

Bartender 3

Bartender 3 是一款Mac菜单栏自定义工具,简单说就是可以将指定的程序图标隐藏起来,需要时呼出。

+

+
+

TODO…

+ +
+ + + + + + + +
+ + + + + + +
+ +
+ + + +
+ + + + + + + + +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2018/lose-weight/index.html b/2018/lose-weight/index.html new file mode 100644 index 0000000000..53cca4adfc --- /dev/null +++ b/2018/lose-weight/index.html @@ -0,0 +1,662 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 2018 减肥计划 | 贾攀的流水账 + + + + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

贾攀的流水账

+ +
+

Panmax's Blog

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+ + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ 2018 减肥计划 +

+ + +
+ + + + +
+ + +

今年优先级最高的事情是 lose weight,给自己设置了两个时间节点:第一个节点是自己的生日(农历5月12),目标是从 90kg 减至 80kg,第二个节点是我拍脑袋想的日期:222天后(农历9月18),减到 70kg,立文为据。明天起每日博客记录体重变化。

+

本次算是人生中的第二次减肥,上一次从 98kg 减到了 69kg,但是没有注意保持,两年时间又回来了,这一次再用 200 多天减下去,再之后准备上一些器械,争取练出 6 块腹肌🌚。

+

2018.06.18 - 2018.06.24

    +
  • 星期一:77.4
  • +
  • 星期二:77
  • +
  • 星期三:77.3
  • +
  • 星期四:76.5
  • +
  • 星期五:76.5
  • +
  • 星期六:76.3
  • +
  • 星期日:76.5
  • +
+

2018.06.11 - 2018.06.17

    +
  • 星期一:79.4
  • +
  • 星期二:78.2
  • +
  • 星期三:77.4
  • +
  • 星期四:77.1
  • +
  • 星期五:76.7
  • +
  • 星期六:76.8
  • +
  • 星期日:77.6
  • +
+

2018.06.04 - 2018.06.10

    +
  • 星期一:81.2
  • +
  • 星期二:79.7
  • +
  • 星期四:78.5
  • +
  • 星期五:78.2
  • +
  • 星期六:78.6
  • +
  • 星期日:79.3
  • +
+

2018.05.28 - 2018.06.03

    +
  • 星期一:80.8
  • +
  • 星期二:80.2
  • +
  • 星期三:79.6
  • +
  • 星期四:79.3
  • +
  • 星期五:78.9
  • +
  • 星期六:79.7
  • +
+

2018.05.21 - 2018.05.27

+

本周计划:80.5

+
+
    +
  • 星期一:81.3
  • +
  • 星期二:80.5
  • +
  • 星期三:79.9
  • +
  • 星期四:79.9
  • +
  • 星期五:79.9
  • +
  • 星期六:81
  • +
+

2018.05.14 - 2018.05.20

    +
  • 星期一:82.6
  • +
  • 星期三:81.8
  • +
  • 星期四:81.6
  • +
  • 星期五:81.5
  • +
  • 星期日:81.5
  • +
+

2018.05.07 - 2018.05.13

    +
  • 星期一:82.9
  • +
  • 星期二:82.9
  • +
  • 星期三:82.6
  • +
  • 星期四:82.8
  • +
  • 星期五:82.0
  • +
  • 星期日:82.0
  • +
+

2018.04.30 - 2018.05.06

    +
  • 星期二:83.5
  • +
  • 星期四:83.1
  • +
  • 星期五:82.7
  • +
+

2018.04.23 - 2018.04.29

    +
  • 星期一:85.3
  • +
  • 星期二:84.4
  • +
  • 星期三:83.4
  • +
  • 星期四:83.4
  • +
  • 星期五:83.5
  • +
  • 星期六:84.1
  • +
+

2018.04.16 - 2018.04.22

    +
  • 星期一:85.9
  • +
  • 星期二:85.9
  • +
  • 星期三:84.7
  • +
  • 星期四:84.4
  • +
  • 星期五:83.2
  • +
+

2018.04.09 - 2018.04.15

+

本周计划:85.0

+
+
    +
  • 星期一:85.6
  • +
  • 星期二:85.4
  • +
  • 星期三:85.7
  • +
  • 星期五:85.4
  • +
  • 星期六:84.9
  • +
  • 星期日:85.2
  • +
+

2018.04.02 - 2018.04.08

+

本周计划:85.4(未完成。本周过了个清明,没有好好控制,体重也没称)

+
+
    +
  • 星期一:86.2
  • +
  • 星期二:85.1(今晚团建吃自助,估计明天会爆炸)
  • +
  • 星期五:86.1
  • +
+

2018.03.26 - 2018.04.01

+

本周计划:86.0(未达成)

+
+
    +
  • 星期一:87.0
  • +
  • 星期二:86.9
      +
    • 练臂
    • +
    +
  • +
  • 星期三:86.5
      +
    • 练背
    • +
    +
  • +
  • 星期四:86.0
      +
    • 练腿
    • +
    +
  • +
  • 星期五:85.5
  • +
  • 星期六:86.3
  • +
  • 星期日:86.7(周末两天太放纵了)
  • +
+

2018.03.19 - 2018.03.25

+

本周计划:87.2(达成)

+
+
    +
  • 星期一:87.8(上周末搬家,没有好好运动,吃的东西也没有控制特别注意)
  • +
  • 星期二:87.8
  • +
  • 星期三:87.6
  • +
  • 星期四:87.5
  • +
  • 星期五:86.7
      +
    • 卧推:10 * 4
    • +
    • 哑铃卧推:10 * 2
    • +
    • 器械推胸:10 * 4
    • +
    • 跑步:40min
    • +
    +
  • +
  • 星期六:86.5
      +
    • 硬拉:10 * 6
    • +
    • 哑铃划船:10 * 6
    • +
    • 水平划船:10 * 6
    • +
    • 高位下拉:10 * 6
    • +
    • 椭圆机:40min
    • +
    +
  • +
  • 星期日:86.4
      +
    • 腿屈伸 10 * 6
    • +
    • 哈克深蹲 10 * 6
    • +
    • 私教体验课 60 min
    • +
    • 椭圆机:45min
    • +
    +
  • +
+

2018.03.12 - 2018.03.18

+

本周计划:87.8(达成)

+
+
    +
  • 星期二:88.3
  • +
  • 星期三:87.8
  • +
  • 星期四:87.7
  • +
  • 星期五:87.7
  • +
  • 星期六:87.7
  • +
  • 星期日:87.7(WTF???连续4天87.7🙃)
  • +
+

2018.03.05 - 2018.03.11

+

本周目标:88.5(达成)

+
+
    +
  • 星期一:90.1
  • +
  • 星期二:89.8
  • +
  • 星期三:89.8
  • +
  • 星期四:89.1
  • +
  • 星期五:88.7
  • +
  • 星期六:88.9(一顿羊蝎子回到解放前)
  • +
  • 星期日:88.4(💪🏻)
  • +
+ +
+ + + + + + + +
+ + + + + + +
+ +
+ + + +
+ + + + + + + + +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2018/neo4j-count-relationships-skill/index.html b/2018/neo4j-count-relationships-skill/index.html new file mode 100644 index 0000000000..4bbddcf94e --- /dev/null +++ b/2018/neo4j-count-relationships-skill/index.html @@ -0,0 +1,497 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Neo4j 统计关系数量时的技巧 | 贾攀的流水账 + + + + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

贾攀的流水账

+ +
+

Panmax's Blog

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+ + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ Neo4j 统计关系数量时的技巧 +

+ + +
+ + + + +
+ + +

今天产品经理给了我一个需求,让我统计下我们数据中的某些类型关系的数量,因为涉及到保密,我就假设他让我统计的关系名称为 FRIENDFATHER,其中 FRIEND 在创建时没有指定方向,FATHER 创建时有方向。

+

当统计这两种关系的数量时,我的建议是 Cypher 语句中不要带有节点标签,或者只在一端带标签,因为在我的测试中,两端都带有节点标签时,查询会超时(因为我们的数据量确实比较大):

+

不建议的写法:

1
MATCH (:Person)-[r:FATHER]->(:Person) return COUNT(r);
+

建议的写法:

1
2
3
MATCH ()-[r:FATHER]->() return COUNT(r);
or
MATCH (:Person)-[r:FATHER]->() return COUNT(r);
+

在查询 FRIEND 这种在创建时没有指定方向的关系是,也需要用带方向的查询语句,因为 Neo4j 实际存储时是带有方向的,详情见:http://blog.csdn.net/hwz2311245/article/details/54602706),在不指定方向的情况下,我这里的查询也是超时:

+

不建议的写法:

1
MATCH ()-[r:FRIEND]-() return COUNT(r);
+

建议的写法

1
MATCH ()-[r:FRIEND]->() return COUNT(r);
+ +
+ + + + + + + +
+ + + + + + +
+ +
+ + + +
+ + + + + + + + +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2018/neo4j-import-dynamic-relationship-type/1.png b/2018/neo4j-import-dynamic-relationship-type/1.png new file mode 100644 index 0000000000..0de3235c90 Binary files /dev/null and b/2018/neo4j-import-dynamic-relationship-type/1.png differ diff --git a/2018/neo4j-import-dynamic-relationship-type/index.html b/2018/neo4j-import-dynamic-relationship-type/index.html new file mode 100644 index 0000000000..33776232f5 --- /dev/null +++ b/2018/neo4j-import-dynamic-relationship-type/index.html @@ -0,0 +1,508 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Neo4j 导入动态类型关系 | 贾攀的流水账 + + + + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

贾攀的流水账

+ +
+

Panmax's Blog

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+ + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ Neo4j 导入动态类型关系 +

+ + +
+ + + + +
+ + +

今天在构建一批新的关系时,需要用 LOAD CSV 批量导入一些关系数据进去,但是这次的关系类型并不是固定的,而是在文件中指定的,CSV 文件格式如下:

+
1
2
3
4
5
6
7
8
9
10
AWI9o1sbC5n_pY7tOUdL|AWI96tqyetLN4cQh5GnC|EMERGENCY|紧急联络人
AWI85duqetLN4cQhYTVc|AWI-DASVC5n_pY7tH2i5|EMERGENCY|紧急联络人
AWI85duqetLN4cQhYTVc|AWI-DASVC5n_pY7tH2i5|RELATIVE|Spouse
AWI9-Vm7etLN4cQhC6gp|AWI9lGsNetLN4cQhK_8U|EMERGENCY|紧急联络人
AWI8pkY9etLN4cQhqdrQ|AWI8dIx9C5n_pY7tBKBJ|EMERGENCY|紧急联络人
AWI8pkY9etLN4cQhqdrQ|AWI8dIx9C5n_pY7tBKBJ|OTHER|Other
AWI-ZirYC5n_pY7tIn7b|AWI-d3TaC5n_pY7tUjVr|EMERGENCY|紧急联络人
AWI-ZirYC5n_pY7tIn7b|AWI-d3TaC5n_pY7tUjVr|RELATIVE|Cousin
AWI8UPZ3etLN4cQhsFoT|AWI9ngEwetLN4cQhS4hI|EMERGENCY|紧急联络人
AWI-CJ7jetLN4cQhNwgt|AWI-UkO6etLN4cQhDYhR|EMERGENCY|紧急联络人
+

第一列和第二列分别两个 Person 节点的 UUID,第三列是关系类型(type),第四列是关系的名称(name)。

+

我刚开始这样写的导入语句:

+
1
2
3
4
5
USING PERIODIC COMMIT 10000
LOAD CSV FROM 'file:////relation_all.csv' AS line FIELDTERMINATOR '|'
MATCH (p1:Person {uuid: line[0]})
MATCH (p2:Person {uuid: line[1]})
MERGE (p1)-[:line[2] {name: line[3]}]->(p2)
+

但是发现无法执行,由错误信息可知(下图所示),Neo4j 原始的 LOAD CSV 语句是不支持动态创建关系类型的。

+

+

我之前在介绍 Neo4j 冷启动预热缓存 时介绍过一个插件:APOC,这个插件功能非常强大,比如提供了很多好用的路径算法和强大的函数,之后有机会的话会慢慢介绍,今天介绍一下他的动态创建关系的函数 apoc.create.relationship,函数说明如下:

+

apoc.create.relationship(person1,'KNOWS',{key:value,…​}, person2) create relationship with dynamic rel-type

+

所以我的导入语句只需要改成:

+
1
2
3
4
5
6
7
USING PERIODIC COMMIT 10000
LOAD CSV FROM 'file:////relation_all.csv' AS line FIELDTERMINATOR '|'
MATCH (p1:Person {uuid: line[0]})
MATCH (p2:Person {uuid: line[1]})
WITH p1, p2, line
CALL apoc.create.relationship(p1, line[2], {name: line[3]}, p2) YIELD rel
RETURN rel
+

就完事大吉了。

+

但是考虑到这个 CSV 文件中的关系可能存在重复,所以我通过文档找到了另一个函数:

+

apoc.merge.relationship(startNode, relType, {key:value, …​}, {key:value, …​}, endNode) - merge relationship with dynamic type

+

这个函数中需要传两组 key-value,第一组是用来判断关系是否重复的,第二组是一些其他属性。

+

最终的导入语句如下:

+
1
2
3
4
5
6
7
USING PERIODIC COMMIT 10000
LOAD CSV FROM 'file:////relation_all.csv' AS line FIELDTERMINATOR '|'
MATCH (p1:Person {uuid: line[0]})
MATCH (p2:Person {uuid: line[1]})
WITH p1, p2, line
CALL apoc.merge.relationship(p1, line[2], {name: line[3]}, {}, p2) YIELD rel
RETURN rel
+ +
+ + + + + + + +
+ + + + + + +
+ +
+ + + +
+ + + + + + + + +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2018/neo4j-tutorial-about/1.png b/2018/neo4j-tutorial-about/1.png new file mode 100644 index 0000000000..c3cc21570c Binary files /dev/null and b/2018/neo4j-tutorial-about/1.png differ diff --git a/2018/neo4j-tutorial-about/index.html b/2018/neo4j-tutorial-about/index.html new file mode 100644 index 0000000000..fd4a562400 --- /dev/null +++ b/2018/neo4j-tutorial-about/index.html @@ -0,0 +1,513 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Neo4j 入门教程 - 关于 Neo4j | 贾攀的流水账 + + + + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

贾攀的流水账

+ +
+

Panmax's Blog

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+ + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ Neo4j 入门教程 - 关于 Neo4j +

+ + +
+ + + + +
+ + +
+

Neo4j 是世界上最流行的图数据库管理系统(DBMS),同样是最流行的 NoSQL 数据库之一。

+
+

+

Neo4j 是什么

Neo4j 以图的方式存储和展示数据,数据由节点和节点间的关系来表示。

+

Neo4j 数据库(和任何图数据库一样)与关系型数据库(如:MS Access、SQL Server、MySQL)有很大不同。关系数据库使用表、行和列来存储数据,他们以表格的形式展示数据。

+

Neo4j 不使用表、行或者列存储或展示数据。

+

Neo4j 用来做什么

Neo4j 非常适合存储有很多关联关系的数据。这是图数据库可以发挥巨大作用的地方。实际上,像 Neo4j 这样的图数据库在处理关系数据方面要优于关系型数据库。

+

图模型通常不需要预定义结构,你不需要在加载数据前创建数据库结构(就像在关系型数据库中那样)。在 Neo4j 中,数据就是结构。Neo4j 是一个 「结构可选」的 DBMS。

+

然而 Neo4j 能更好地处理关系数据的主要原因在于它允许你创建关系。Neo4j 是围绕关系而建立的。它不需要设置主键/外键约束来预先确定哪些字段或者哪些数据间有关系。在 Neo4j 中,只要在你需要时添加任何点之间的关系就行了。

+

所以,这使得 Neo4j 非常合适社交网络应用,比如 Facebook、Twitter 等。同时,Neo4j 还可以应用于很多其他领域。

+

以下是一些 Neo4j 主要应用领域:

+
    +
  • 社交网络
  • +
  • 实时产品推荐
  • +
  • 网络架构图
  • +
  • 欺诈识别
  • +
  • 访问管理
  • +
  • 基于图的数字资产搜索
  • +
  • 主数据管理
  • +
+ +
+ + + + + + + +
+ + + + + + +
+ +
+ + + +
+ + + + + + + + +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2018/neo4j-tutorial-browser/1.png b/2018/neo4j-tutorial-browser/1.png new file mode 100644 index 0000000000..31b299e42e Binary files /dev/null and b/2018/neo4j-tutorial-browser/1.png differ diff --git a/2018/neo4j-tutorial-browser/2.png b/2018/neo4j-tutorial-browser/2.png new file mode 100644 index 0000000000..e8174088e6 Binary files /dev/null and b/2018/neo4j-tutorial-browser/2.png differ diff --git a/2018/neo4j-tutorial-browser/3.png b/2018/neo4j-tutorial-browser/3.png new file mode 100644 index 0000000000..2ff15a91e1 Binary files /dev/null and b/2018/neo4j-tutorial-browser/3.png differ diff --git a/2018/neo4j-tutorial-browser/index.html b/2018/neo4j-tutorial-browser/index.html new file mode 100644 index 0000000000..4899f4adeb --- /dev/null +++ b/2018/neo4j-tutorial-browser/index.html @@ -0,0 +1,511 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Neo4j 入门教程 - Neo4j 浏览器 | 贾攀的流水账 + + + + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

贾攀的流水账

+ +
+

Panmax's Blog

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+ + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ Neo4j 入门教程 - Neo4j 浏览器 +

+ + +
+ + + + +
+ + +
+

Neo4j 浏览器是一个可以通过 Web 浏览器运行的图形用户界面(GUI)

+
+
+

Neo4j 浏览器可以用来添加数据、运行查询语句、创建关系等等。它还提供了一种简单的方式来可视化数据库中的数据。

+

概览

下图是 Neo4j 浏览器概览

+

Neo4j 浏览器概览

+

编辑器

这里是你输入查询语句和命令的地方,比如创建或检索数据。你可以随时通过输入 :help 并按下回车键(或者点击编辑器右侧的「运行」箭头)来获取帮助。

+

这里是展示查询结果的地方,每个结果有自己的框架,新的结果框会出现在前一个结果框的上边。所以如果需要的话,你可以向下滚动并查看之前的查询结果。你可以随时使用 :clear 命令清空这个

+

标签、节点、关系

这些代表了数据库中的数据。点击顶部的任意图标都会在框架的底部显示可选信息。

+

侧边栏

侧边栏有多个选线,例如查看数据库详情,查看或修改 Neo4j 浏览器设置,查看 Neo4j 文档等等。

+

点击一个选项会打开更宽一些的侧边栏并提供该选项的详情。

+

比如,点击「数据库」图标会打开有关数据库的详细信息。

+

+

框架视图选项

你能够以不同的方式查看数据。例如,点击 「Table」将会以表格的方式显示节点和关系。

+

下图是一个以表格方式显示数据的例子:

+

+ +
+ + + + + + + +
+ + + + + + +
+ +
+ + + +
+ + + + + + + + +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2018/neo4j-tutorial-create-constraint/1.png b/2018/neo4j-tutorial-create-constraint/1.png new file mode 100644 index 0000000000..0f17528b3c Binary files /dev/null and b/2018/neo4j-tutorial-create-constraint/1.png differ diff --git a/2018/neo4j-tutorial-create-constraint/2.png b/2018/neo4j-tutorial-create-constraint/2.png new file mode 100644 index 0000000000..af10d78312 Binary files /dev/null and b/2018/neo4j-tutorial-create-constraint/2.png differ diff --git a/2018/neo4j-tutorial-create-constraint/3.png b/2018/neo4j-tutorial-create-constraint/3.png new file mode 100644 index 0000000000..2489cf9e03 Binary files /dev/null and b/2018/neo4j-tutorial-create-constraint/3.png differ diff --git a/2018/neo4j-tutorial-create-constraint/index.html b/2018/neo4j-tutorial-create-constraint/index.html new file mode 100644 index 0000000000..4478250ce2 --- /dev/null +++ b/2018/neo4j-tutorial-create-constraint/index.html @@ -0,0 +1,531 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Neo4j 入门教程 - 使用 Cypher 创建约束 | 贾攀的流水账 + + + + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

贾攀的流水账

+ +
+

Panmax's Blog

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+ + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ Neo4j 入门教程 - 使用 Cypher 创建约束 +

+ + +
+ + + + +
+ + +
+

约束允许你对节点或关系的输入数据进行限制。

+
+

约束有助于数据的完整性,因为它们阻止用户输入错误的数据类型。如果某个用户在应用了约束时输入了错误的类型会收到错误消息。

+

约束类型

在 Neo4j 中你可以创建唯一约束和属性存在约束。

+

唯一约束

+
    +
  • 指定该属性必须包含唯一值(比如两个 Artist 节点不允许有相同值的 name 属性。)
  • +
+

属性存在约束

+
    +
  • 确保具有特定标签的节点或具有特定类型的关系都存在某个属性(属性存在约束只在 Neo4j 企业版中可用)
  • +
+

创建唯一约束

在 Neo4j 中创建唯一约束需要使用 CREATE CONSTRAINT ON 语句,像下边这样:

+
1
CREATE CONSTRAINT ON (a:Artist) ASSERT a.name IS UNIQUE
+

在上边的例子中,我们为 Artist 标签的所有节点的 name 属性创建了唯一约束。

+

我们的语句执行成功后,展示如下信息:

+

+
+

当你创建一个唯一约束时,Neo4j 将同时创建一个索引。Cypher 将使用该索引进行查询,就像使用其他索引一样。
因此不需要单独创建索引了,如果你尝试在已经有索引的情况下创建约束,你将会收到一个错误。

+
+

查看约束

约束(和索引)成为数据库模式的一部分。

+

我们可以通过使用 :schema 名来来查看我们刚刚创建的约束,就像下边这样:

+
1
:schems
+

你将会看到新创建的约束以及使用它创建的索引,也可以看到我们之前创建的索引:

+

+

测试约束

我们可以通过尝试创建两个相同的艺术家来测试这个约束是否起作用。

+

执行下边的语句两次:

+
1
2
CREATE (a:Artist {name: "周杰伦"}) 
RETURN a
+

第一次运行这条语句时,节点将会被创建。第二次运行时,你应该会收到以下错误信息:

+

+

属性存在约束

属性存在约束能够确保具有特定标签的所有节点具有特定的属性。比如你可以指定 Artist 标签的所有节点都必须包含 name 属性。

+

使用 ASSERT exists(variable.propertyName) 语法来创建属性存在约束。像下边这样:

+
1
CREATE CONSTRAINT ON (a:Artist) ASSERT exists(a.name)
+
+

请注意,属性存在约束只能在 Neo4j 企业版中使用。

+
+ +
+ + + + + + + +
+ + + + + + +
+ +
+ + + +
+ + + + + + + + +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2018/neo4j-tutorial-create-index/1.png b/2018/neo4j-tutorial-create-index/1.png new file mode 100644 index 0000000000..786fd59cc1 Binary files /dev/null and b/2018/neo4j-tutorial-create-index/1.png differ diff --git a/2018/neo4j-tutorial-create-index/2.png b/2018/neo4j-tutorial-create-index/2.png new file mode 100644 index 0000000000..ee2d1ff13a Binary files /dev/null and b/2018/neo4j-tutorial-create-index/2.png differ diff --git a/2018/neo4j-tutorial-create-index/index.html b/2018/neo4j-tutorial-create-index/index.html new file mode 100644 index 0000000000..a68dc770b7 --- /dev/null +++ b/2018/neo4j-tutorial-create-index/index.html @@ -0,0 +1,519 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Neo4j 入门教程 - 使用 Cypher 创建索引 | 贾攀的流水账 + + + + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

贾攀的流水账

+ +
+

Panmax's Blog

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+ + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ Neo4j 入门教程 - 使用 Cypher 创建索引 +

+ + +
+ + + + +
+ + +
+

索引是一种数据结构,可以提高数据库数据检索的速度。

+
+

在 Neo4j 中,你可以给有标签的点的任何属性创建索引。一旦你创建了一个索引,Neo4j 将会管理它,在数据更新时保持最新的索引。

+

使用 CREATE INDEX ON 语句创建索引,像下边这样:

+
1
CREATE INDEX ON :Album(name)
+

在上边的例子中,我们为所有标签为 Album 的点的 name 属性创建了一个索引。

+

语句执行成功后,将展示如下信息:

+

+
+

当你创建一个索引时,Neo4j 会在后台进行操作。如果你的数据库很大,可能需要一段时间。只有当 Neo4j 完成索引创建后,这个索引才会被上线并用于查询。

+
+

查看索引

索引(约束)成为了数据库模式的一部分。

+

在 Neo4j 浏览器中,你可以使用 :schema 命令查看所有索引和约束。

+

来试一试吧:

+
1
:schema
+

你可以看到一个索引和约束的列表:

+

+

索引提示

索引创建完成后,当你在执行查询时会自动使用。

+

然而 Neo4j 也允许你强制提示一个或多个索引,你可以在你的查询语句中使用 USING INDEX ... 创建一个索引提示。

+

所以上边的示例可以像下边这样强制索引:

+
1
2
3
MATCH (a:Album {name: "猛龙过江"}) 
USING INDEX a:Album(name)
RETURN a
+

我们也可以使用多个提示,为每个想强制的索引添加一个新的 USING INDEX 即可。

+

是否有必要索引?

当 Neo4j 创建索引时,它会在数据库中创建冗余的副本,因此使用索引会占用更多的硬盘空间并减慢写入速度。

+

因此在决定索引哪些数据时你需要进行一些权衡。

+

一般来说当你知道某些节点数量很多时,创建索引是个不错的主意。或者你发现查询时间太长可以尝试通过添加索引来解决。

+ +
+ + + + + + + +
+ + + + + + +
+ +
+ + + +
+ + + + + + + + +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2018/neo4j-tutorial-create-node/1.png b/2018/neo4j-tutorial-create-node/1.png new file mode 100644 index 0000000000..eb897a43cd Binary files /dev/null and b/2018/neo4j-tutorial-create-node/1.png differ diff --git a/2018/neo4j-tutorial-create-node/2.png b/2018/neo4j-tutorial-create-node/2.png new file mode 100644 index 0000000000..fb55f4c9aa Binary files /dev/null and b/2018/neo4j-tutorial-create-node/2.png differ diff --git a/2018/neo4j-tutorial-create-node/index.html b/2018/neo4j-tutorial-create-node/index.html new file mode 100644 index 0000000000..c813f4bc27 --- /dev/null +++ b/2018/neo4j-tutorial-create-node/index.html @@ -0,0 +1,519 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Neo4j 入门教程 - 使用 Cypher 创建节点 | 贾攀的流水账 + + + + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

贾攀的流水账

+ +
+

Panmax's Blog

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+ + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ Neo4j 入门教程 - 使用 Cypher 创建节点 +

+ + +
+ + + + +
+ + +
+

要用 Cypher 创建节点和关系,请使用 CREATE 语句

+
+
+

这个语句由 CREATE 组成,后边跟上你要创建的点或关系。

+

举例

我们来创建一个包含乐队名和他们专辑的音乐数据库。

+

第一个乐队被称为筷子兄弟,我们创建一个艺术家节点,并称之为筷子兄弟

+

我们第一个点看起来像下边这样

+

+

下边是创建筷子兄弟节点的 Cypher CREATE 语句:

+
1
CREATE (a:Artist { name : "筷子兄弟" })
+

这个 Cypher 语句创建一个带有 Artist 标签的节点,节点有一个 name 属性,该属性的值是筷子兄弟

+

a 前缀是我们提供的变量名,我们可以使用任意变量名。如果我们需要在后边的语句中用到这个点(上边的情况中我们没有用到),这个变量就会是很有用的。注意,变量仅限于在单条语句中使用。

+

到 Neo4j 浏览器中执行上边的语句,该语句将创建一个节点。

+

一旦 Neo4j 创建完节点,你将看到这样的消息:

+

+

展示节点

CREATE 语句创建节点但是不展示节点。为了展示节点我们需要在它后边跟上 RETURN 语句。

+

我们来创建另一个节点,这次我们创建一个专辑,与之前不同的是这次我们在后边跟上 RETURN 语句

+
1
2
CREATE (b:Album { name : "猛龙过江", released : "2014" })
RETURN b
+

上边语句创建了一个带有 Album 标签的节点,它有两个属性:namereleased

+

注意,我们通过使用它的变量名(本例中是 b)返回了这个节点。

+

创建多个节点

我们可以通过用逗号分隔来一次性创建多个节点:

+
1
2
CREATE (a:Album { name: "我们是太阳"}), (b:Album { name: "小水果"}) 
RETURN a,b
+

或者可以使用多个 CREATE 语句:

+
1
2
3
CREATE (a:Album { name: "我们是太阳"}) 
CREATE (b:Album { name: "小水果"})
RETURN a,b
+

接下来,我们将在节点间建立关系。

+ +
+ + + + + + + +
+ + + + + + +
+ +
+ + + +
+ + + + + + + + +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2018/neo4j-tutorial-create-repationship/1.png b/2018/neo4j-tutorial-create-repationship/1.png new file mode 100644 index 0000000000..84b69dd9fc Binary files /dev/null and b/2018/neo4j-tutorial-create-repationship/1.png differ diff --git a/2018/neo4j-tutorial-create-repationship/2.png b/2018/neo4j-tutorial-create-repationship/2.png new file mode 100644 index 0000000000..bbefe10934 Binary files /dev/null and b/2018/neo4j-tutorial-create-repationship/2.png differ diff --git a/2018/neo4j-tutorial-create-repationship/3.png b/2018/neo4j-tutorial-create-repationship/3.png new file mode 100644 index 0000000000..64286ef0e6 Binary files /dev/null and b/2018/neo4j-tutorial-create-repationship/3.png differ diff --git a/2018/neo4j-tutorial-create-repationship/index.html b/2018/neo4j-tutorial-create-repationship/index.html new file mode 100644 index 0000000000..f3f2ab73d9 --- /dev/null +++ b/2018/neo4j-tutorial-create-repationship/index.html @@ -0,0 +1,519 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Neo4j 入门教程 - 使用 Cypher 创建关系 | 贾攀的流水账 + + + + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

贾攀的流水账

+ +
+

Panmax's Blog

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+ + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ Neo4j 入门教程 - 使用 Cypher 创建关系 +

+ + +
+ + + + +
+ + +
+

就像在 Neo4j 中创建节点一样,可以用 CREATE 来创建节点间的关系。

+
+
+

创建关系的语句由 CREATE 组成,后边跟着要创建的关系详情。

+

举例

我们在先前创建的点之间创建一个关系,首先创建一个乐队和专辑之间的关系。

+

我们将创建如下关系

+

+

这是 Cypher 的 CREATE 创建上边关系的语句:

+
1
2
3
4
MATCH (a:Artist),(b:Album)
WHERE a.name = "筷子兄弟" AND b.name = "猛龙过江"
CREATE (a)-[r:RELEASED]->(b)
RETURN r
+

上边代码的解释

首先我们使用 MATCH 语句查找我们要创建关系的两个点。

+

可能有很多节点带有 ArtistAlbum 标签,所以我们需要找到我们感兴趣的节点。在这个例子中,我们使用属性值来过滤它:使用之前赋值给每个节点的 name 属性。

+

接下来是用来创建关系的 CREATE 语句,在这个例子中,它通过我们在第一行中给出的变量名称(ab)来引用两个节点,关系是通过字符画模式,用箭头指示关系方向来建立的:(a)-[r:RELEASED]->(b)

+

我们给这个关系一个变量名 r 并且给了一个 RELEASE 类型(乐队发行专辑)。关系类型和节点的标签概念类似。

+

添加更多关系

上边是一个非常简单的例子,Neo4j 擅长的事情是处理很多相互关联的关系。

+

为了看到继续创建更多节点和它们之间的关系是多么容易,让我们在刚刚的基础上继续构建。我们来创建一个点外加两个关系。

+

我们将要达到下边图效果

+

+

这张图展示了王太利在筷子兄弟乐队中参与演奏,在专辑中进行表演并且专辑是由他来创作的

+

我们为王太利创建一个节点:

+
1
CREATE (p:Person { name: "王太利" })
+

现在来创建关系并返回图:

+
1
2
3
4
MATCH (a:Artist),(b:Album),(p:Person)
WHERE a.name = "筷子兄弟" AND b.name = "猛龙过江" AND p.name = "王太利"
CREATE (p)-[:PRODUCED]->(b), (p)-[:PERFORMED_ON]->(b), (p)-[:PLAYS_IN]->(a)
RETURN a,b,p
+

执行完成后你应该就可以看到前边截图中的图了。

+

+ +
+ + + + + + + +
+ + + + + + +
+ +
+ + + +
+ + + + + + + + +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2018/neo4j-tutorial-cypher/index.html b/2018/neo4j-tutorial-cypher/index.html new file mode 100644 index 0000000000..5ac18af6bd --- /dev/null +++ b/2018/neo4j-tutorial-cypher/index.html @@ -0,0 +1,525 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Neo4j 入门教程 - 查询语言 Cypher | 贾攀的流水账 + + + + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

贾攀的流水账

+ +
+

Panmax's Blog

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+ + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ Neo4j 入门教程 - 查询语言 Cypher +

+ + +
+ + + + +
+ + +
+

Neo4j 有自己的查询语言称为 Cypher。Cypher 使用与 SQL(结构化查询语言)类似的语法。

+
+
+

举例

下边是一条 Cypher 语句:

+
1
2
MATCH (p:Person { name:"Homer Flinstone" })
RETURN p
+

这条 Cypher 语句返回属性 nameHomer FlinstonePerson 节点。

+

如果通过 SQL 来查询关系型数据库,看起来可能更像这样:

+
1
2
SELECT * FROM Person
WHERE name = "Homer Flinstone";
+

不过请记住,Neo4j 不像关系数据库模型那样将数据存储在表中,Neo4j 的所有数据都在节点和关系中存储。所以上边的 Cypher 语句查询的是节点、节点的标签和节点的属性,而 SQL 查询的是表、行和列。

+

SQL 被设计为适用于关系数据库管理系统(DBMS)。Neo4j 是一个 NoSQL DBMS,所以它不使用关系模型同样也不使用 SQL。

+

Cypher 是专门为 Neo4j 的数据模型而设计,用来查询节点及其相互关系的。

+

字符画语法

Cypher 使用字符画来表示模式,使得我们在第一次学习这门语言时很容易记住它。如果你忘记了如何编写,只需要想一想图的样子就会对你有所帮助。

+
1
(a)-[:KNOWS]->(b)
+

主要记住如下几点:

+
    +
  • 节点由圆括号表示,看起来像是圆圈。就像这样:(node)
  • +
  • 关系用箭头来表示。像这样: ->
  • +
  • 关系相关的信息可以插入到方括号中。像这样:[:KNOWS]
  • +
+

定义数据

在使用 Cypher 时请记住以下几点:

+
    +
  • 节点通常有标签(一个或多个)。比如:”Person”,”User”,”Actor”,”Customer”,”Employee”等
  • +
  • 节点通常有属性,属性提供有关节点的额外信息。比如:”name”,”age”,”born”等
  • +
  • 关系也可以有属性
  • +
  • 关系通常有一个类型(类似于节点的标签)。比如:”KNOWS”,”LIKES”,”WORKS_FOR”,”PURCHASED”等
  • +
+

让我们再来看一下上边的例子:

+
1
2
MATCH (p:Person { name:"Homer Flinstone" })
RETURN p
+

我们可以看到:

+
    +
  • 节点被圆括号 () 包围
  • +
  • Person 是节点的标签
  • +
  • name 是节点的属性
  • +
+ +
+ + + + + + + +
+ + + + + + +
+ +
+ + + +
+ + + + + + + + +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2018/neo4j-tutorial-delete-node-using-cypher/1.png b/2018/neo4j-tutorial-delete-node-using-cypher/1.png new file mode 100644 index 0000000000..381033ab03 Binary files /dev/null and b/2018/neo4j-tutorial-delete-node-using-cypher/1.png differ diff --git a/2018/neo4j-tutorial-delete-node-using-cypher/index.html b/2018/neo4j-tutorial-delete-node-using-cypher/index.html new file mode 100644 index 0000000000..968358fc90 --- /dev/null +++ b/2018/neo4j-tutorial-delete-node-using-cypher/index.html @@ -0,0 +1,511 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Neo4j 入门教程 - 使用 Cypher 删除节点 | 贾攀的流水账 + + + + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

贾攀的流水账

+ +
+

Panmax's Blog

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+ + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ Neo4j 入门教程 - 使用 Cypher 删除节点 +

+ + +
+ + + + +
+ + +
+

要使用 Cpyher 删除节点和关系,可使用 DELETE 子句。

+
+

MATCH 语句中使用 DELETE 子句来删除任何匹配的数据。

+

因此 DELETE 子句用在之前例子中的 RETURN 子句的地方。

+

举例

下边的语句删除标签为 Albumname 属性为 Panmax 的节点:

+
1
MATCH (a:Album {name: "Panmax"}) DELETE a;
+
+

在实际删除前认真检查语句是否删除的是正确的数据是个不错的主意。
为此可以先使用 RETURN 子句构造语句,然后运行它。这样可以检查要删除的是不是正确的数据。一旦你对匹配的结果数据满意后,只需将 RETURN 子句改为 DELETE 子句即可。

+
+

删除多个节点

你也可以一次性删除多个节点。只需要让你的 MATCH 语句包含所有你想要删除的节点就行了。

+
1
2
MATCH (a:Artist {name: "jiapan"}), (b:Album {name: "Panmax"}) 
DELETE a, b
+

删除所有节点

你可以通过省略过滤条件来删除数据库中的所有节点,就像我们从数据库中选取所有节点一样,你也可以删除它们。

+
1
MATCH (n) DELETE n;
+

删除带有关系的节点

在删除节点时有一个小细节需要注意,就是你只能删除没有连接任何关系的节点。换句话说,在删除节点本身前,必须先删除和它相关的关系。

+

如果你尝试在具有关系的节点上执行上述 DELETE 语句,你将看到如下所示的错误消息:

+

+

这个错误消息告诉我们,我们在删除节点前必须先删除它的关系。

+

幸运的是有一种便捷的方式可以做到这一点,我们会在下一课来介绍它。

+ +
+ + + + + + + +
+ + + + + + +
+ +
+ + + +
+ + + + + + + + +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2018/neo4j-tutorial-delete-relationship-using-cypher/1.png b/2018/neo4j-tutorial-delete-relationship-using-cypher/1.png new file mode 100644 index 0000000000..51a50e3d70 Binary files /dev/null and b/2018/neo4j-tutorial-delete-relationship-using-cypher/1.png differ diff --git a/2018/neo4j-tutorial-delete-relationship-using-cypher/2.png b/2018/neo4j-tutorial-delete-relationship-using-cypher/2.png new file mode 100644 index 0000000000..f6a2ce208b Binary files /dev/null and b/2018/neo4j-tutorial-delete-relationship-using-cypher/2.png differ diff --git a/2018/neo4j-tutorial-delete-relationship-using-cypher/3.png b/2018/neo4j-tutorial-delete-relationship-using-cypher/3.png new file mode 100644 index 0000000000..de96a4e459 Binary files /dev/null and b/2018/neo4j-tutorial-delete-relationship-using-cypher/3.png differ diff --git a/2018/neo4j-tutorial-delete-relationship-using-cypher/4.png b/2018/neo4j-tutorial-delete-relationship-using-cypher/4.png new file mode 100644 index 0000000000..7c58a8ddce Binary files /dev/null and b/2018/neo4j-tutorial-delete-relationship-using-cypher/4.png differ diff --git a/2018/neo4j-tutorial-delete-relationship-using-cypher/index.html b/2018/neo4j-tutorial-delete-relationship-using-cypher/index.html new file mode 100644 index 0000000000..34b3c92384 --- /dev/null +++ b/2018/neo4j-tutorial-delete-relationship-using-cypher/index.html @@ -0,0 +1,529 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Neo4j 入门教程 - 使用 Cypher 删除关系 | 贾攀的流水账 + + + + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

贾攀的流水账

+ +
+

Panmax's Blog

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+ + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ Neo4j 入门教程 - 使用 Cypher 删除关系 +

+ + +
+ + + + +
+ + +
+

你可以像删除节点一样删除关系 - 通过匹配你想要删除的关系。

+
+

你可以一次性删除一个或多个关系,甚至可以删除数据库中的所有关系。

+

首先,作为复习,以下是我们之前创建的关系:

+

+

我们来删除类型为 RELEASED 的关系。

+

有几种方法可以解决这个问题,我们来看其中的三种。

+

下边的语句范围非常广 - 它将删除所有类型为 RELEASED 的关系。

+
1
2
MATCH ()-[r:RELEASED]-() 
DELETE r
+

你也可以写的更具体一些,就像这样:

+
1
2
MATCH (:Artist)-[r:RELEASED]-(:Album) 
DELETE r
+

上边的语句将匹配所有的 Artist 节点和 Album 节点间具有 RELEASED 的关系。

+

你甚至可以更具体一些,就像这样:

+
1
2
MATCH (:Artist {name: "筷子兄弟"})-[r:RELEASED]-(:Album {name: "猛龙过江"}) 
DELETE r
+

上边的任意一条语句都可以将 RELEASED 关系删掉,图将看起来是这样的:

+

+

删除有关联关系的节点

节点存在关系将不能被删除,如果我们尝试执行下边的语句:

+
1
MATCH (a:Artist {name: "筷子兄弟"}) DELETE a
+

会看到如下错误:

+

+

这是因为节点上有连接的关系。

+

一种选择是删除所有的关系,然后再删除节点。

+

另一种选择是使用 DETACH DELETE 子句。DETACH DELETE 子句允许你删除一个节点的同时删除与其相连的所有关系。

+

所以我们可以将上面的语句改为:

+
1
MATCH (a:Artist {name: "筷子兄弟"}) DETACH DELETE a
+

执行这条语句将看到下边的成功消息:

+

+

删除整个数据库

你可以进一步使用 DETACH DELETE 并删除整个数据库。

+

只需将过滤条件去掉就可以删除所有的点和关系了。

+

继续来执行下边的语句:

+
1
MATCH (n) DETACH DELETE n
+

至此,我们的数据库中不再有任何数据。所以这节课就作为我们 Neoj4 入门教程的最后一课吧🙂

+

如果你有兴趣了解更多关于 Neo4j 的知识,请查看 https://neo4j.com/docs/developer-manual/current/

+ +
+ + + + + + + +
+ + + + + + +
+ +
+ + + +
+ + + + + + + + +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2018/neo4j-tutorial-drop-index-and-constraint-using-cypher/1.png b/2018/neo4j-tutorial-drop-index-and-constraint-using-cypher/1.png new file mode 100644 index 0000000000..f58f6db86d Binary files /dev/null and b/2018/neo4j-tutorial-drop-index-and-constraint-using-cypher/1.png differ diff --git a/2018/neo4j-tutorial-drop-index-and-constraint-using-cypher/2.png b/2018/neo4j-tutorial-drop-index-and-constraint-using-cypher/2.png new file mode 100644 index 0000000000..40daadf6a0 Binary files /dev/null and b/2018/neo4j-tutorial-drop-index-and-constraint-using-cypher/2.png differ diff --git a/2018/neo4j-tutorial-drop-index-and-constraint-using-cypher/3.png b/2018/neo4j-tutorial-drop-index-and-constraint-using-cypher/3.png new file mode 100644 index 0000000000..efc597245e Binary files /dev/null and b/2018/neo4j-tutorial-drop-index-and-constraint-using-cypher/3.png differ diff --git a/2018/neo4j-tutorial-drop-index-and-constraint-using-cypher/4.png b/2018/neo4j-tutorial-drop-index-and-constraint-using-cypher/4.png new file mode 100644 index 0000000000..263621537d Binary files /dev/null and b/2018/neo4j-tutorial-drop-index-and-constraint-using-cypher/4.png differ diff --git a/2018/neo4j-tutorial-drop-index-and-constraint-using-cypher/index.html b/2018/neo4j-tutorial-drop-index-and-constraint-using-cypher/index.html new file mode 100644 index 0000000000..b1efd6ce02 --- /dev/null +++ b/2018/neo4j-tutorial-drop-index-and-constraint-using-cypher/index.html @@ -0,0 +1,519 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Neo4j 入门教程 - 使用 Cypher 删除索引和约束 | 贾攀的流水账 + + + + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

贾攀的流水账

+ +
+

Panmax's Blog

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+ + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ Neo4j 入门教程 - 使用 Cypher 删除索引和约束 +

+ + +
+ + + + +
+ + +
+

你可以使用 DROP INDEX ON 语句删除索引,这将从数据库中删除索引。

+
+

因此要删除我们之前创建的索引,我们可以使用以下语句:

+
1
DROP INDEX ON :Album(name);
+

语句执行成功后会展示以下消息:

+

+

查看模式

你现在可以使用 :schema 命令来验证对应的索引是否已经从模式中删除。

+

只需输入:

+
1
:schema
+

可以看到索引已经不在模式中了:

+

+
+

你可以使用 DROP CONSTRAINT 语句删除约束,这将从数据库中删除约束和相关索引。

+
+

那么让我们来删除之前创建的约束(和它关联的索引)吧,我们可以使用下边的语句:

+
1
DROP CONSTRAINT ON (a:Artist) ASSERT a.name IS UNIQUE
+

语句执行成功后会展示下边的消息:

+

+

查看模式

你现在可以使用 :schema 命令来验证对应的索引(和相关联的约束)是否已经从模式中删除。

+

只需输入:

+
1
:schema
+

可以看到约束已经不在模式中了:

+

+ +
+ + + + + + + +
+ + + + + + +
+ +
+ + + +
+ + + + + + + + +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2018/neo4j-tutorial-import-data-from-csv-file-using-cypher/1.png b/2018/neo4j-tutorial-import-data-from-csv-file-using-cypher/1.png new file mode 100644 index 0000000000..562e214278 Binary files /dev/null and b/2018/neo4j-tutorial-import-data-from-csv-file-using-cypher/1.png differ diff --git a/2018/neo4j-tutorial-import-data-from-csv-file-using-cypher/2.png b/2018/neo4j-tutorial-import-data-from-csv-file-using-cypher/2.png new file mode 100644 index 0000000000..e75e3012af Binary files /dev/null and b/2018/neo4j-tutorial-import-data-from-csv-file-using-cypher/2.png differ diff --git a/2018/neo4j-tutorial-import-data-from-csv-file-using-cypher/3.png b/2018/neo4j-tutorial-import-data-from-csv-file-using-cypher/3.png new file mode 100644 index 0000000000..3c1620a5c2 Binary files /dev/null and b/2018/neo4j-tutorial-import-data-from-csv-file-using-cypher/3.png differ diff --git a/2018/neo4j-tutorial-import-data-from-csv-file-using-cypher/4.png b/2018/neo4j-tutorial-import-data-from-csv-file-using-cypher/4.png new file mode 100644 index 0000000000..db579aa812 Binary files /dev/null and b/2018/neo4j-tutorial-import-data-from-csv-file-using-cypher/4.png differ diff --git a/2018/neo4j-tutorial-import-data-from-csv-file-using-cypher/genres.csv b/2018/neo4j-tutorial-import-data-from-csv-file-using-cypher/genres.csv new file mode 100644 index 0000000000..f8928c2422 --- /dev/null +++ b/2018/neo4j-tutorial-import-data-from-csv-file-using-cypher/genres.csv @@ -0,0 +1,115 @@ +1,"A Cappella" +2,"Acid" +3,"Acoustic" +4,"Alternative" +5,"Ambient" +6,"Avantgarde" +7,"Bass" +8,"Beat" +9,"Bebob" +10,"Big Band" +11,"Black Metal" +12,"Bluegrass" +13,"Blues" +14,"Booty Bass" +15,"BritPop" +16,"Cabaret" +17,"Celtic" +18,"Chamber Music" +19,"Christian Rap" +20,"Christian Rock" +21,"Classic Rock" +22,"Classical" +23,"Club" +24,"Club-House" +25,"Comedy" +26,"Contemporary Christian" +27,"Country" +28,"Cult" +29,"Dance" +30,"Death Metal" +31,"Disco" +32,"Dream" +33,"Drum & Bass" +34,"Drum Solo" +35,"Duet" +36,"Easy Listening" +37,"Electronic" +38,"Ethnic" +39,"Folk" +40,"Folk/Rock" +41,"Folklore" +42,"Freestyle" +43,"Funk" +44,"Fusion" +45,"Game" +46,"Gangsta Rap" +47,"Gospel" +48,"Gothic" +49,"Gothic Rock" +50,"Grunge" +51,"Hard Rock" +52,"Hardcore" +53,"Heavy Metal" +54,"Hip-Hop" +55,"House" +56,"Humour" +57,"Indie" +58,"Industrial" +59,"Instrumental" +60,"Instrumental Pop" +61,"Instrumental Rock" +62,"Jazz" +63,"Jazz+Funk" +64,"Jungle" +65,"Latin" +66,"Lo-Fi" +67,"Meditative" +68,"Metal" +69,"Musical" +70,"New Age" +71,"New Wave" +72,"Noise" +73,"Oldies" +74,"Opera" +75,"Other" +76,"Polka" +77,"Polsk Punk" +78,"Pop" +79,"Pop-Folk" +80,"Pop/Funk" +81,"Power Ballad" +82,"Progressive Rock" +83,"Psychedelic" +84,"Psychedelic Rock" +85,"Punk" +86,"Punk Rock" +87,"R&B" +88,"Rap" +89,"Rave" +90,"Reggae" +91,"Retro" +92,"Revival" +93,"Rhythmic Soul" +94,"Rock" +95,"Rock & Roll" +96,"Salsa" +97,"Samba" +98,"Satire" +99,"Ska" +100,"Sonata" +101,"Soul" +102,"Soundtrack" +103,"Southern Rock" +104,"Swing" +105,"Symphonic Rock" +106,"Symphony" +107,"Synthpop" +108,"Tango" +109,"Techno" +110,"Techno-Industrial" +111,"Thrash Metal" +112,"Top 40" +113,"Trance" +114,"Tribal" +115,"Vocal" diff --git a/2018/neo4j-tutorial-import-data-from-csv-file-using-cypher/index.html b/2018/neo4j-tutorial-import-data-from-csv-file-using-cypher/index.html new file mode 100644 index 0000000000..a4092fef98 --- /dev/null +++ b/2018/neo4j-tutorial-import-data-from-csv-file-using-cypher/index.html @@ -0,0 +1,545 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Neo4j 入门教程 - 使用 Cypher 导入来自 CSV 文件的数据 | 贾攀的流水账 + + + + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

贾攀的流水账

+ +
+

Panmax's Blog

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+ + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ Neo4j 入门教程 - 使用 Cypher 导入来自 CSV 文件的数据 +

+ + +
+ + + + +
+ + +
+

你可以将 CSV 文件中的数据导入到 Neo4j 数据库中,为此我们来学习下 Cypher 中的 LOAD CSV 语句。

+
+

将 CSV 文件导入到 Neo4j 的能力,可以实现从其他类型的数据库来导入数据(比如关系型数据库)。

+

在 Neo4j 中,你可以通过本地或远端 URL 来加载 CSV 文件。

+

要访问本地(在数据库服务器上)文件,使用 file:/// 路径。除此之外,可以使用任何 HTTPS,HTTP 和 FTP 协议。

+

读取 CSV 文件

我们使用 HTTP 协议加载一个名为 genres.csv 的 CSV 文件。它不是一个大文件,这个列表里包含了 115 个音乐流派,所以它将创建 115 个节点(和 230 个属性)。

+

这个文件上传到了开放的网络中,所以你可以在你的 Neo4j 浏览器中运行下边的代码,它可以直接导入到你的数据库中。

+
1
2
LOAD CSV FROM 'https://jpanj.com/2018/neo4j-tutorial-import-data-from-csv-file-using-cypher/genres.csv' AS line
CREATE (:Genre {genreId: line[0], name: line[1]})
+
+

你也可以忽略 CSV 文件中的某些字段,比如,如果你不希望将第一个字段导入到数据库中,可以从上边的代码中省略 genreId: line[0],

+
+

运行上边的 Cypher 语句会产生以下成功消息:

+

+

你可以通过以下查询来查看刚刚新创建的节点:

+
1
MATCH (n:Genre) RETURN n
+

下边是通过数据可视化界面看到的节点结果:

+

+

导入包含标题的 CSV 文件

之前的 CSV 文件不包含任何标题,如果 CSV 文件包含标题,可以使用 WITH HEADERS

+

使用这个方法还允许你通过它的列名(标题名)来引用每个字段。

+

我们有另一个带标题的 CSV 文件,该文件包含专辑曲目列表。

+

同样,这个文件不大,列表中包含了 32 个专辑,所以它将创建 32 个节点(和 96 个属性)。

+

这个文件也上传到了开放的网络中,所以你可以在你的 Neo4j 浏览器中运行下边的代码,它可以直接导入到你的数据库中。

+
1
2
LOAD CSV WITH HEADERS FROM 'https://jpanj.com/2018/neo4j-tutorial-import-data-from-csv-file-using-cypher/tracks.csv' AS line
CREATE (:Track { trackId: line.Id, name: line.Track, length: line.Length})
+

这将产生下边的成功消息:

+

+

下边的查询语句可以查看新创建的节点:

+
1
MATCH (n:Track) RETURN n
+

同样我们通过可视化框架看到的节点的结果。

+

点击 Table 图标可以看到每个点和它的三个属性值:

+

+

自定义分隔符

如果需要的话你可以指定自定义字段分隔符,假如 CSV 文件中的分隔符是分号的话,你可以指定使用分号作为分隔符而不是逗号。

+

只需将 FIELDTERMINATOR 子句添加到语句中就可以做到了,像下边这样:

+
1
2
3
LOAD CSV WITH HEADERS FROM 'https://jpanj.com/2018/neo4j-tutorial-import-data-from-csv-file-using-cypher/tracks.csv' AS line
FIELDTERMINATOR ';'
CREATE (:Track { trackId: line.Id, name: line.Track, length: line.Length})
+

导入大文件

如果你需要导入包含大量数据的文件,可以使用 PERODIC COMMIT 来处理。

+

在 Neo4j 中使用定期提交功能可以在导入一定数量的行之后提交一次数据,这减少了事务状态的内存开销。

+

默认是 1000 行,所以数据会每 1000 行提交一次。

+

要使用定期提交,只需在语句开头插入 USING PERIODIC COMMIT (在 LOAD CSV 之前)。

+

下边有个例子:

+
1
2
3
USING PERIODIC COMMIT
LOAD CSV WITH HEADERS FROM 'https://jpanj.com/2018/neo4j-tutorial-import-data-from-csv-file-using-cypher/tracks.csv' AS line
CREATE (:Track { trackId: line.Id, name: line.Track, length: line.Length})
+

设置定期提交频率

你还可以将 1000 行的默认值更改为另一个数字,只需将数字加在 USING PERIODIC COMMIT 后边就行了,就像这样:

+
1
2
3
USING PERIODIC COMMIT 800
LOAD CSV WITH HEADERS FROM 'https://jpanj.com/2018/neo4j-tutorial-import-data-from-csv-file-using-cypher/tracks.csv' AS line
CREATE (:Track { trackId: line.Id, name: line.Track, length: line.Length})
+

CSV 的格式要求

以下是使用 LOAD CSV 时应该如何格式化 CSV 文件的一些要求:

+
    +
  • 字符编码必须是 UTF-8
  • +
  • 行终止标识和系统有关,例如在 Unix 中是 \n,在 Windows 上是 \r\n
  • +
  • 分隔符必须是逗号,除非用通过 FIELDTERMINATOR 特殊指定
  • +
  • 如果字符串是用双引号引起来的,数据读入后会将双引号去掉
  • +
  • 任何需要转义的字符都可以通过反斜线 \ 来转义
  • +
+ +
+ + + + + + + +
+ + + + + + +
+ +
+ + + +
+ + + + + + + + +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2018/neo4j-tutorial-import-data-from-csv-file-using-cypher/tracks.csv b/2018/neo4j-tutorial-import-data-from-csv-file-using-cypher/tracks.csv new file mode 100644 index 0000000000..3394dcf08d --- /dev/null +++ b/2018/neo4j-tutorial-import-data-from-csv-file-using-cypher/tracks.csv @@ -0,0 +1,33 @@ +Id,Track,Length +1,"Innocent Exile",03:52:00 +2,"Killers",05:01:00 +3,"Prodigal Son",06:12:00 +4,"Purgatory",03:20:00 +5,"Drifter",04:49:00 +6,"Tailgunner",04:15:00 +7,"Holy Smoke",03:49:00 +8,"No Prayer For The Dying",04:23:00 +9,"Public Enema Number One",04:14:00 +10,"Fates Warning",04:10:00 +11,"The Assassin",04:18:00 +12,"Run Silent Run Deep",04:35:00 +13,"Hooks In You",04:07:00 +14,"Bring Your Daughter To The Slaughter",04:44:00 +15,"Mother Russia",05:32:00 +16,"Where Eagles Dare",06:13:00 +17,"Revelations",06:49:00 +18,"Flight Of Icarus",03:50:00 +19,"Die With Your Boots On",05:26:00 +20,"The Trooper",04:12:00 +21,"Still Life",04:56:00 +22,"Quest For Fire",03:42:00 +23,"Sun And Steel",03:27:00 +24,"To Tame A Land",07:26:00 +25,"Caught Somewhere In Time",07:26:00 +26,"Wasted Years",05:08:00 +27,"Sea Of Madness",05:42:00 +28,"Heaven Can Wait",07:21:00 +29,"The Loneliness Of The Long Distance Runner",06:31:00 +30,"Stranger In A Strange Land",05:44:00 +31,"Deja Vu",04:56:00 +32,"Alexander The Great",08:36:00 diff --git a/2018/neo4j-tutorial-installation/1.png b/2018/neo4j-tutorial-installation/1.png new file mode 100644 index 0000000000..b771b666ae Binary files /dev/null and b/2018/neo4j-tutorial-installation/1.png differ diff --git a/2018/neo4j-tutorial-installation/2.png b/2018/neo4j-tutorial-installation/2.png new file mode 100644 index 0000000000..78e11b7c6d Binary files /dev/null and b/2018/neo4j-tutorial-installation/2.png differ diff --git a/2018/neo4j-tutorial-installation/3.png b/2018/neo4j-tutorial-installation/3.png new file mode 100644 index 0000000000..78ca0f6e95 Binary files /dev/null and b/2018/neo4j-tutorial-installation/3.png differ diff --git a/2018/neo4j-tutorial-installation/index.html b/2018/neo4j-tutorial-installation/index.html new file mode 100644 index 0000000000..f153cc8d20 --- /dev/null +++ b/2018/neo4j-tutorial-installation/index.html @@ -0,0 +1,540 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Neo4j 入门教程 - 安装 | 贾攀的流水账 + + + + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

贾攀的流水账

+ +
+

Panmax's Blog

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+ + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ Neo4j 入门教程 - 安装 +

+ + +
+ + + + +
+ + +

本篇来简单介绍下如何下载并安装 Neo4j,篇目很短,因为真的很简单。

+

下载 Neo4j

首先在 https://neo4j.com/download/ 下载 Neo4j。你可以选择企业体验版或者免费的社区版,这里我是用的社区版。点击 Download 按钮即可开始下载。

+

网站会自动下载适合您操作系统的文件,如果你不想要这个,可以选择通过 这个链接 选择另一个操作系统的版本。

+

安装 Neo4j

当文件下载下来后,就可以安装 Neo4j 了。下载页面包含了将 Neo4j 安装到你的操作系统的一步步指导说明,我在这里介绍下 Mac、Windows 和 Linux 的安装。这里列出的说明,是为了让你快速了解安装 Neo4j 所涉及的步骤,实际步骤可能会随着未来的版本而变化,所以请务必按照下载时网站上的说明来进行安装。当你下载 Neo4j 时,Neo4j 会在感谢页面展示这些说明。

+

Mac (dmg)

这个安装程序包含了运行 Neo4j 所需要的 Java 版本。

+
    +
  1. 打开你刚刚下载好的 dmg 文件
  2. +
  3. 将 Neo4j 的图标拖拽到你的应用目录中
  4. +
  5. 在应用目录中打开 Neo4j,你可能会被系统询问是否是你从互联网上下载的这个程序,不要担心,确认即可
  6. +
  7. 点击 Start 按钮来启动 Neo4j 的服务
  8. +
  9. 在你的浏览器中打开程序提供给你的 URL
  10. +
  11. neo4j 账户修改密码
  12. +
+

Linux/Unix (tar/tar.gz)

    +
  1. 打开你的终端
  2. +
  3. 使用 tar -xvf <file> 来提取存档的内容。比如 tar -xvf neo4j-community-3.2.8-unix.tar,如果你下载的是 tar.gz 的压缩包,那么使用 tar -zxvf 来进行解压
  4. +
  5. 使用 $NEO4J_HOME/bin/neo4j console 来运行 Neo4j,或者用 $NEO4J_HOME/bin/neo4j start 让服务进程在后台运行
  6. +
  7. 在你的本机浏览器访问 http://localhost:7474
  8. +
  9. neo4j 账户修改密码
  10. +
+

Windows (exe)

这个安装程序包含了运行 Neo4j 所需要的 Java 版本。

+
    +
  1. 运行你刚刚下载的安装程序,你可能需要给这个程序的安装权限来授权
  2. +
  3. 按照提示选择运行 Neo4j 的选项
  4. +
  5. 点击 Start 按钮来启动 Neo4j 服务器
  6. +
  7. 在浏览器中打开程序提供给你的 URL
  8. +
  9. neo4j 账户修改密码
  10. +
+

Windows (zip)

    +
  1. 首先安装 JDK8
  2. +
  3. 找到压缩包,点击右键进行解压
  4. +
  5. 把解压出的文件放到服务器的主目录中,顶级目录称为 NEO4J_HOME,比如 D:\neo4j\
  6. +
  7. 使用 zip 包中提供的 Windows PowerShell 来启动和管理 Neo4j
  8. +
  9. 在浏览器中访问 http://localhost:7474
  10. +
  11. neo4j 账户修改密码
  12. +
+

启动并连接到 Neo4j 服务

1. 启动服务

这里是一个已经启动起来的 Neo4j 服务,启动方法取决于你的操作系统,我这里用 Mac 来举例,在应用目录中点击 Neo4j Community Edition 3.2.6,点击打开窗口中 Start 按钮即可启动 Neo4j 服务。

+

+

服务启动后,在浏览器中打开 http://localhost:7474 然后按照提示进行操作。

+

下图是我第一次进入的界面(未来版本可能会看到不同的界面)

+

+

2. 登录

使用界面上提供的用户名和密码来登录,默认的密码是 neo4j

+

第一次登录时,系统会提示你修改密码

+

3. 结果

密码修改完成后这个界面将会被展示

+

+

在这里,你可以使用当前界面提供的链接来学习更多关于 Neo4j 的知识以及如何创建数据库和运行查询语句

+ +
+ + + + + + + +
+ + + + + + +
+ +
+ + + +
+ + + + + + + + +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2018/neo4j-tutorial-select-data-with-match-using-cypher/1.png b/2018/neo4j-tutorial-select-data-with-match-using-cypher/1.png new file mode 100644 index 0000000000..9253a12ffb Binary files /dev/null and b/2018/neo4j-tutorial-select-data-with-match-using-cypher/1.png differ diff --git a/2018/neo4j-tutorial-select-data-with-match-using-cypher/2.png b/2018/neo4j-tutorial-select-data-with-match-using-cypher/2.png new file mode 100644 index 0000000000..f42a3c367b Binary files /dev/null and b/2018/neo4j-tutorial-select-data-with-match-using-cypher/2.png differ diff --git a/2018/neo4j-tutorial-select-data-with-match-using-cypher/3.png b/2018/neo4j-tutorial-select-data-with-match-using-cypher/3.png new file mode 100644 index 0000000000..e7329a6eb3 Binary files /dev/null and b/2018/neo4j-tutorial-select-data-with-match-using-cypher/3.png differ diff --git a/2018/neo4j-tutorial-select-data-with-match-using-cypher/4.png b/2018/neo4j-tutorial-select-data-with-match-using-cypher/4.png new file mode 100644 index 0000000000..a5d6d205d0 Binary files /dev/null and b/2018/neo4j-tutorial-select-data-with-match-using-cypher/4.png differ diff --git a/2018/neo4j-tutorial-select-data-with-match-using-cypher/5.png b/2018/neo4j-tutorial-select-data-with-match-using-cypher/5.png new file mode 100644 index 0000000000..4b1f0e709e Binary files /dev/null and b/2018/neo4j-tutorial-select-data-with-match-using-cypher/5.png differ diff --git a/2018/neo4j-tutorial-select-data-with-match-using-cypher/6.png b/2018/neo4j-tutorial-select-data-with-match-using-cypher/6.png new file mode 100644 index 0000000000..9346182816 Binary files /dev/null and b/2018/neo4j-tutorial-select-data-with-match-using-cypher/6.png differ diff --git a/2018/neo4j-tutorial-select-data-with-match-using-cypher/index.html b/2018/neo4j-tutorial-select-data-with-match-using-cypher/index.html new file mode 100644 index 0000000000..c8df90f704 --- /dev/null +++ b/2018/neo4j-tutorial-select-data-with-match-using-cypher/index.html @@ -0,0 +1,533 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Neo4j 入门教程 - 使用 Cypher 的 MATCH 语句选取数据 | 贾攀的流水账 + + + + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

贾攀的流水账

+ +
+

Panmax's Blog

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+ + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ Neo4j 入门教程 - 使用 Cypher 的 MATCH 语句选取数据 +

+ + +
+ + + + +
+ + +
+

Cypher 的 MATCH 语句允许你查询符合条件的数据。你可以使用 MATCH 来返回数据或对这些数据执行一些其他操作。

+
+

MATCH 语句用于匹配给定的条件,但实际上它并不返回数据。为了从 MATCH 语句返回数据,我们仍然需要使用 RETURN 子句。

+

检索一个节点

这里有个使用 MATCH 语句检索一个节点的的例子。

+
1
2
3
MATCH (p:Person)
WHERE p.name = "王太利"
RETURN p
+

WHERE 子句与 SQL 的 WHERE 子句工作方式相同,它允许你提供额外的条件来缩小查询范围。

+

同时你可以在不使用 WHERE 子句的情况下获得相同的结果。你可以通过使用像创建节点那样的符号来查询节点。

+

下边的代码提供和上边语句相同的结果:

+
1
2
MATCH (p:Person {name: "王太利"})
RETURN p
+

运行上边任意一条查询语句将会看到如下的节点被展示出来:

+

+

你可能已经注意到,点击一个节点会展开一个分成三部分的外部圆,每个部分有不同的选项:

+

+

点击底部选项将展开节点的关系:

+

+

关系

你还可以通过 MATCH 语句遍历关系。事实上,这才是 Neoj4 真正擅长的事情之一。

+

举个栗子,如果我们想找出哪个乐队发布了名为「猛龙过江」的专辑,可以使用如下查询语句:

+
1
2
3
MATCH (a:Artist)-[:RELEASED]->(b:Album)
WHERE b.name = "猛龙过江"
RETURN a
+

这将返回以下节点:

+

+

可以看到我们在 MATCH 中使用的模式几乎是不言自明的,它匹配了所有发布过名为 猛龙过江 专辑的乐队。

+

我们使用了变量(a,b)以便在稍后的查询中引用他们。我们没有为关系提供任何变量,因为我们不需要在之后的查询中引用关系。

+

你可能还会注意到第一行使用的是我们在创建关系时相同的模式,这突出了 Cypher 语言的简单性,我们可以在不同的上下文中使用相同的模式(比如创建数据和遍历数据)。

+

返回全部节点

你可以通过省略过滤条件来返回数据库中所有的节点。因此以下查询将返回数据库中的所有节点:

+
1
MATCH (n) return n;
+

我们所有的节点将被返回:

+

+

你还可以点击侧面的 Table 图标用表格来展示数据:

+

+
+

返回所有节点时要小心,在大型数据库中执行这个操作可能会产生很大的性能影响。通常建议限制结果以避免意想不到的问题。

+
+

限制结果

使用 LIMIT 来限制输出记录的数量,当你不确定结果集有多大时,使用 LIMIT 是个好主意。

+

因此我们可以简单的将 LIMIT 5 追加到前边的语句上来将输出限制为5条记录:

+
1
2
MATCH (n) RETURN n 
LIMIT 5
+ +
+ + + + + + + +
+ + + + + + +
+ +
+ + + +
+ + + + + + + + +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2018/neo4j-with-multiple-types/1.png b/2018/neo4j-with-multiple-types/1.png new file mode 100644 index 0000000000..55975b4866 Binary files /dev/null and b/2018/neo4j-with-multiple-types/1.png differ diff --git a/2018/neo4j-with-multiple-types/2.png b/2018/neo4j-with-multiple-types/2.png new file mode 100644 index 0000000000..355dc00243 Binary files /dev/null and b/2018/neo4j-with-multiple-types/2.png differ diff --git a/2018/neo4j-with-multiple-types/index.html b/2018/neo4j-with-multiple-types/index.html new file mode 100644 index 0000000000..2296a063da --- /dev/null +++ b/2018/neo4j-with-multiple-types/index.html @@ -0,0 +1,507 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Neo4j 关系不支持多种类型 | 贾攀的流水账 + + + + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

贾攀的流水账

+ +
+

Panmax's Blog

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+ + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ Neo4j 关系不支持多种类型 +

+ + +
+ + + + +
+ + +

Neo4j 中创建节点时,可以指定多个标签:

+
1
CREATE (n:Person:China)
+

但是在创建关系时,只能指定一种类型,其实官方通过这两个不同词汇(标签 和 类型)也能体现出节点和关系关于分类方面的不同。

+

如下图所示,在尝试用多种类型创建关系时,会报错:

+

+

+

一个节点可以有多个标签,一个关系只能有一种类型。

+

GitHub issues 中也有一个简短的解释:

+
+

This is unfortunately out of scope right now the property-graph model fared well with a single relationship-type so far.

+
+
+

I’d suggest you create multiple relationships between your two nodes that is the way to go.

+
+

属性图模型在单一关系类型时表现更好,如果需要在两个节点间表示多个关系,直接创建多条关系就可以了。

+ +
+ + + + + + + +
+ + + + + + +
+ +
+ + + +
+ + + + + + + + +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2018/resolve-ssh-slow/index.html b/2018/resolve-ssh-slow/index.html new file mode 100644 index 0000000000..d98b22051a --- /dev/null +++ b/2018/resolve-ssh-slow/index.html @@ -0,0 +1,495 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 解决 ssh 登录慢的问题 | 贾攀的流水账 + + + + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

贾攀的流水账

+ +
+

Panmax's Blog

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+ + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ 解决 ssh 登录慢的问题 +

+ + +
+ + + + +
+ + +

OpenSSH 在用户登录的时候会验证 IP,它根据用户的 IP 使用反向 DNS 找到主机名,再使用 DNS 找到 IP 地址,最后匹配一下登录的 IP 是否合法。如果客户机的 IP 没有域名,或者 DNS 服务器很慢或不通,那么登录就会很花时间。

+

解决办法:

+

在目标服务器上修改 sshd 服务器端配置,并重启 sshd。

+

vi /etc/ssh/sshd_config

+

设置 UseDNSno 即可。

+

最后

+

systemctl restart sshd

+
+

参考:https://www.cnblogs.com/ggjucheng/p/3348499.html

+
+ +
+ + + + + + + +
+ + + + + + +
+ +
+ + + +
+ + + + + + + + +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2018/run-in-background/index.html b/2018/run-in-background/index.html new file mode 100644 index 0000000000..3b8bbe5a1b --- /dev/null +++ b/2018/run-in-background/index.html @@ -0,0 +1,493 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 让前台程序转为后台运行 | 贾攀的流水账 + + + + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

贾攀的流水账

+ +
+

Panmax's Blog

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+ + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ 让前台程序转为后台运行 +

+ + +
+ + + + +
+ + +

我们在登录服务器,执行一个很耗时的任务时,通常我们会使用 nohup + & 的方式执行,如果我们在启动时,忘记加上这 nohup 该如何补救呢?

+
    +
  1. 首先使用 control + z 让当前进程挂起(Suspend)。
  2. +
  3. 然后我们使用 jobs 查看它的作业号。
  4. +
  5. 再用 bg %jobspec 来将它放入后台并继续运行。
  6. +
  7. 最后使用 disown -h %jobspec 来使这个作业忽略 HUP 信号。
  8. +
+

这个方法可以用在 scp 的命令中,在没有设置 ssh 无密码登录的情况下,我们不能使用 nohup 来执行 scp 命令,所以只能在开始大文件拷贝后,通过上述流程来让这个作业放置在后台执行。

+ +
+ + + + + + + +
+ + + + + + +
+ +
+ + + +
+ + + + + + + + +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2018/shangqiu-video/daoxiang.m4a b/2018/shangqiu-video/daoxiang.m4a new file mode 100644 index 0000000000..5676b563b3 Binary files /dev/null and b/2018/shangqiu-video/daoxiang.m4a differ diff --git a/2018/shangqiu-video/index.html b/2018/shangqiu-video/index.html new file mode 100644 index 0000000000..1c520c6cd2 --- /dev/null +++ b/2018/shangqiu-video/index.html @@ -0,0 +1,490 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 山丘 - 李宗盛 | 贾攀的流水账 + + + + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

贾攀的流水账

+ +
+

Panmax's Blog

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+ + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ 山丘 - 李宗盛 +

+ + +
+ + + + +
+ + + + +
+

想说却还没说的 还很多
攒着是因为想写成歌
让人轻轻地唱着 淡淡地记着
就算终于忘了 也值了
说不定我一生涓滴意念
侥幸汇成河
然后我俩各自一端
望着大河弯弯 终于敢放胆
嘻皮笑脸 面对 人生的难
也许我们从未成熟
还没能晓得 就快要老了
尽管心里活着的还是那个
年轻人
因为不安而频频回首
无知地索求 羞耻于求救
不知疲倦地翻越 每一个山丘
越过山丘 虽然已白了头
喋喋不休 时不我予的哀愁
还未如愿见着不朽
就把自己先搞丢
越过山丘 才发现无人等候
喋喋不休 再也唤不回温柔
为何记不得上一次是谁给的拥抱
在什么时候
我没有刻意隐藏 也无意让你感伤
多少次我们无醉不欢
咒骂人生太短 唏嘘相见恨晚
让女人把妆哭花了 也不管
遗憾我们从未成熟
还没能晓得 就已经老了
尽力却仍不明白
身边的年轻人
给自己随便找个理由
向情爱的挑逗 命运的左右
不自量力地还手 直至死方休
越过山丘 虽然已白了头
喋喋不休 时不我予的哀愁
还未如愿见着不朽
就把自己先搞丢
越过山丘 才发现无人等候
喋喋不休 再也唤不回了温柔
为何记不得上一次是谁给的拥抱
在什么时候
越过山丘 虽然已白了头
喋喋不休 时不我予的哀愁
还未如愿见着不朽
就把自己先搞丢
越过山丘 才发现无人等候
喋喋不休 再也唤不回了温柔
为何记不得上一次是谁给的拥抱
在什么时候
喋喋不休 时不我予的哀愁
向情爱的挑逗 命运的左右
不自量力地还手 直至死方休
为何记不得上一次是谁给的拥抱
在什么时候

+
+ +
+ + + + + + + +
+ + + + + + +
+ +
+ + + +
+ + + + + + + + +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2018/spring-boot-with-mysql-jpa/1.png b/2018/spring-boot-with-mysql-jpa/1.png new file mode 100644 index 0000000000..4d9e6f70db Binary files /dev/null and b/2018/spring-boot-with-mysql-jpa/1.png differ diff --git a/2018/spring-boot-with-mysql-jpa/2.png b/2018/spring-boot-with-mysql-jpa/2.png new file mode 100644 index 0000000000..60c018d8fd Binary files /dev/null and b/2018/spring-boot-with-mysql-jpa/2.png differ diff --git a/2018/spring-boot-with-mysql-jpa/3.png b/2018/spring-boot-with-mysql-jpa/3.png new file mode 100644 index 0000000000..a799664343 Binary files /dev/null and b/2018/spring-boot-with-mysql-jpa/3.png differ diff --git a/2018/spring-boot-with-mysql-jpa/4.png b/2018/spring-boot-with-mysql-jpa/4.png new file mode 100644 index 0000000000..49cf8ea53d Binary files /dev/null and b/2018/spring-boot-with-mysql-jpa/4.png differ diff --git a/2018/spring-boot-with-mysql-jpa/5.png b/2018/spring-boot-with-mysql-jpa/5.png new file mode 100644 index 0000000000..727e704c70 Binary files /dev/null and b/2018/spring-boot-with-mysql-jpa/5.png differ diff --git a/2018/spring-boot-with-mysql-jpa/6.png b/2018/spring-boot-with-mysql-jpa/6.png new file mode 100644 index 0000000000..09f8ec08dd Binary files /dev/null and b/2018/spring-boot-with-mysql-jpa/6.png differ diff --git a/2018/spring-boot-with-mysql-jpa/7.png b/2018/spring-boot-with-mysql-jpa/7.png new file mode 100644 index 0000000000..fe6dcac8ed Binary files /dev/null and b/2018/spring-boot-with-mysql-jpa/7.png differ diff --git a/2018/spring-boot-with-mysql-jpa/index.html b/2018/spring-boot-with-mysql-jpa/index.html new file mode 100644 index 0000000000..3a4ee0faff --- /dev/null +++ b/2018/spring-boot-with-mysql-jpa/index.html @@ -0,0 +1,619 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + SpringBoot(2.0) + JPA + MySQL 实现 Restful CURD API | 贾攀的流水账 + + + + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

贾攀的流水账

+ +
+

Panmax's Blog

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+ + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ SpringBoot(2.0) + JPA + MySQL 实现 Restful CURD API +

+ + +
+ + + + +
+ + +

Spring Boot 将 Spring 框架提升了一个新的水平,极大地缩短了 Spring 项目的配置与设置的时间。你几乎可以零配置的开始一个项目并构建你真正关心的部分。

+

我将通过一个记事本应用来演示一下 JPA 的使用,一篇笔记有标题和内容。我们先来编写增、删、改、查接口,然后使用 postman 来进行测试。

+

创建项目

Spring Boot 提供一个 web 工具叫做 Spring Initializer 来引导一个应用。访问 http://start.spring.io 然后按照下边的步骤来生成一个新的项目:

+
    +
  1. 点击页面上的 Switch to full version
  2. +
  3. 输入如下详情
      +
    • Group: com.example
    • +
    • Artifact: easy-notes
    • +
    • Name: easy-notes
    • +
    • Description: Rest API for Note Application
    • +
    • Package Name: com.example.easynotes
    • +
    • Packaging: jar
    • +
    • Java Version: 1.8
    • +
    • Dependencies: Web,JPA,MySQL
    • +
    +
  4. +
+

输入完所有详情后,将上边的 Generate a 改为 Gradle Project 点击 Generate Project 来生成并下载项目。Spring Initializer 将根据你输入的信息生成项目并提供一个 zip 包含所有项目目录。下一步解压下载下来的 zip 文件,并将导入到你喜欢的 IDE 中。

+

+

探索目录结构

下边是我们记事本程序的目录结构

+

+

让我们来理解几个重要文件和目录的详情

+

EasyNotesApplication

这是我们 Spring Boot 应用的主要入口。

+
1
2
3
4
5
6
7
8
9
10
11
12
package com.example.easynotes;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class EasyNotesApplication {

public static void main(String[] args) {
SpringApplication.run(EasyNotesApplication.class, args);
}
}
+

它包含一个名为 @SpringBootApplication 的简单注解,这个注解是以下 Spring 注解的组合:

+
    +
  • @Configuration:任何使用了 @Configuration注解的类都由 Spring 引导,并且也被视为其他 bean 定义的来源。
  • +
  • @EnableAutoConfig:这个注解告诉 Spring 根据你在 build.gradle 文件中添加的依赖来自动配置你的应用。
    比如,如果 spring-data-jpa 位于 classpath 中,它会通过从 application.properties 文件中读取数据库属性来自动尝试配置一个 DataSource。
  • +
  • @ComponentScan:它告诉 Spring 扫描并引导当前包(com.example.easynotes)和全部子包中定义的其他组件。
  • +
+

main() 方法调用 Spring Boot 的 SpringApplication.run() 方法启动这个应用。

+

resources/

顾名思义,这个目录用于存放所有静态资源、模板和属性文件。

+
    +
  • resources/static 包含静态资源,如 css、js 和图片
  • +
  • resources/templates 包含由 Spring 渲染的服务端模板
  • +
  • resources/application.properties 这个文件非常重要,它包含应用范围的属性,Spring 读取这个文件中定义的属性来配置你的应用,你可以在这个文件中定义服务器默认端口、服务器上下文路径、数据库 URL 等
    可以参考此页面来了解 Spring Boot 中常用的应用属性。
  • +
+

EasyNotesApplicationTests

在这里定义单元测试和集成测试

+

build.gradle

包含所有项目依赖

+

配置 MySQL 数据库

如果 spring-data-jpa 位于 classpath 中,Spring Boot 会尝试从 application.properties文件中读取数据库配置自动配置 DataSource。所以我们只需要添加配置,Spring Boot 将负责其他部分。

+

我更习惯于使用 yml 的方式管理配置,所以我们将 application.properties 删掉,新建名为 application.yml 的文本文件。

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# Spring 数据源 (DataSourceAutoConfiguration & DataSourceProperties)
spring:
datasource:
url: jdbc:mysql://localhost:3306/notes_app?useSSL=false
username: root
password: root
jpa:
hibernate:
ddl-auto: update
properties:
hibernate:
dialect: # Hibernate 属性,SQL 方言使得 Hibernate 为所选数据库生成更好的 SQL
jackson:
serialization:
write-dates-as-timestamps: true
+

我们需要在 MySQL 中创建一个名为 notes_app 的数据库并将配置文件中的 usernamepassword 属性改为你安装的 MySQL 对应的值。

+

spring.jpa.properties.hibernate.dialectspring.jpa.hibernate.ddl-auto 这两个配置是提供给 hibernate 的,Spring Boot 使用 Hibernate 作为默认 JPA 实现。

+

spring.jpa.hibernate.ddl-auto 配置用于数据库初始化,我使用 update 值作为属性。

+

它做了两件事:

+
    +
  • 当你定义一个领域模型,将自动在数据库中创建一个表,并将领域模型的字段映射到表中的对应列。
  • +
  • 对领域模型的任何修改将触发表的更新。例如,如果你修改一个字段的名称或类型或者将其他字段添加到模型中,所有这些修改也会反映在映射表中。
  • +
+

对于 spring.jpa.hibernate.ddl-auto 属性来说使用 update 值对于开发阶段来说非常好,但是对于生产阶段,应该保留这个属性值为 validate,并使用数据库迁移工具来管理数据库结构的修改,如 Flyway

+

创建 Note 模型

接下来创建 Note 模型,我们 Note 模型有如下字段:

+
    +
  • id:自增主键
  • +
  • title:笔记的标题(非空字段)
  • +
  • content:笔记的内容(非空字段)
  • +
  • createAt:笔记的创建时间
  • +
  • updateAt:笔记的更新时间
  • +
+

现在来看一下如何在 Spring 中对它进行建模。在 com.example.easynotes 创建一个名为 model 的包,并添加一个名为 Note.java 的类,内容如下:

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
package com.example.easynotes.model;


import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import org.springframework.data.annotation.CreatedDate;
import org.springframework.data.annotation.LastModifiedDate;
import org.springframework.data.jpa.domain.support.AuditingEntityListener;

import javax.persistence.*;
import javax.validation.constraints.NotBlank;
import java.io.Serializable;
import java.util.Date;

@Entity
@Table(name = "notes")
@EntityListeners(AuditingEntityListener.class)
@JsonIgnoreProperties(value = {"createdAt", "updatedAt"},
allowGetters = true)
public class Note implements Serializable {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;

@NotBlank
private String title;

@NotBlank
private String content;

@Column(nullable = false, updatable = false)
@Temporal(TemporalType.TIMESTAMP)
@CreatedDate
private Date createdAt;

@Column(nullable = false)
@Temporal(TemporalType.TIMESTAMP)
@LastModifiedDate
private Date updatedAt;

// getter and setter
}
+
    +
  • 你所有的域模型必须使用 @Entity 进行注解,他用于将该类标记为持久 Java 类
  • +
  • @Table 注解用于提供此实体将映射到表的详细信息
  • +
  • @Id 注解用于定义主键
  • +
  • @GeneratedValue 注解用于定义主键生成策略,上例中我们声明主键是一个自增字段
  • +
  • @NotBlank 注解用于验证被注释的字段不为 null 或 空
  • +
  • @Column 注解用于定义被映射到注解字段列的属性,可以定一个多个属性如名称、长度、可为空、可更新等

    +

    默认情况下,名为 createAt 的字段将映射到数据库表中名为 create_at 的列,即所有驼峰命名将使用下划线替代,如果你想映射这个字段到不同的列,可以使用以下命令指定它:

    +
    1
    2
    @Column(name = "created_on")
    private String createdAt;
    +
  • +
  • @Temporal 注解与 java.util.Datejava.util.Calendar 类一起使用,它将 Java 对象中的时间和日期转换为兼容数据库的类型,反之亦然。

    +
  • +
  • @JsonIgnoreProperties 注解是一个 Jackson 注解,Spring Boot 使用 Jackson 在 Java 对象和 JSON 直接进行序列化和反序列化
  • +
+

使用这个注解是因为我们不希望客户端通过 rest api 提供 createdAtupdatedAt 的值,如果它们提供这些值,我们会简单忽略他们,但是我们将在 JSON 响应中包含这些值。

+

开启 JPA 审计

Note 模型中,我们分别用 @CreatedDate@LastModifiedDate 注解标注了 createdAtupdatedAt 字段。现在我们想要的效果是只要我们创建或更新实体,这些字段会自动填充。

+

为了做到这一点,我们要做两件事:

+
    +
  • 添加 Spring Data JPA 的 AuditingEntityListener 到领域模型中
    我们已经在 Note 模型中使用注解 @EntityListeners(AuditingEntityListener.class) 来完成了这个工作

    +
  • +
  • 在主应用程序中开启 JPA 审计
    打开 EasyNotesApplication.java 并添加 @EnableJpaAuditing 注解。

    +
  • +
+
1
2
3
4
5
6
7
8
9

@SpringBootApplication
@EnableJpaAuditing
public class EasyNotesApplication {

public static void main(String[] args) {
SpringApplication.run(EasyNotesApplication.class, args);
}
}
+

创建 NoteRepository 访问来自数据库的数据

接下来我们要做的是创建一个仓库来访问数据库中的 Note 数据。

+

Spring Data JPA 让我们覆盖这里,它带有一个 JpaRepository 接口,该接口定义了实体上所有 CURD 操作的方法,JpaRepository 的默认实现为 SimpleJpaRepository

+

现在来创建仓库,首先在 com.example.easynotes 下创建一个名为 repository 的包,然后创建一个名为 NoteRepository 的接口并从 JpaRepository 扩展它:

+
1
2
3
4
5
6
7
8
9
package com.example.easynotes.repository;

import com.example.easynotes.model.Note;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;

@Repository
public interface NoteRepository extends JpaRepository<Note, Long> {
}
+

请注意我们使用 @Repository 注解标注了接口,这会告诉 Spring 在组件扫描期间引导这个仓库。

+

以上这些就是你在仓库层所要做的所有工作了,你现在可以使用像 save()findOne()findAll()count()delete() 等 JpaRepository 方法。

+

你不需要实现这些方法,他们已经由 Spring Data JPA 的 SimpleJpaRepository 实现,这个实现在运行时被 Spring 自动插入。

+

查看 SimpleJpaRepository 文档 中提供的所有方法。

+

创建自定义业务异常

我们将在后边定义 Rest API 用来创建、检索、更新和删除笔记。API 会在数据库找不到具有指定 ID 的笔记时抛出 ResourceNotFoundException 异常。

+

以下是 ResourceNotFoundException 的定义,我们在 com.example.easynotes 中创建一个名为 exception 的包来存放这个异常类。

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
package com.example.easynotes.exception;

import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.ResponseStatus;

@ResponseStatus(value = HttpStatus.NOT_FOUND)
public class ResourceNotFoundException extends RuntimeException {

private String resourceName;

private String fieldName;

private Object fieldValue;

public ResourceNotFoundException( String resourceName, String fieldName, Object fieldValue) {
super(String.format("%s not found with %s : '%s'", resourceName, fieldName, fieldValue));
this.resourceName = resourceName;
this.fieldName = fieldName;
this.fieldValue = fieldValue;
}

public String getResourceName() {
return resourceName;
}

public String getFieldName() {
return fieldName;
}

public Object getFieldValue() {
return fieldValue;
}

}
+

注意,上边的异常类使用了 @ResponseStatus 注解,在你的 Controller 中抛出此异常时,Spring boot 会相应指定的 HTTP 状态码。

+

创建 NoteController

最后一步,我们将编写 REST API 来创建、检索、更新和删除笔记。

+

首先在 com.example.easynotes 中创建一个新的包 controller,然后创建一个新的类 NoteController.java 内容如下

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
package com.example.easynotes.controller;

import com.example.easynotes.repository.NoteRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/api")
public class NoteController {

@Autowired
NoteRepository noteRepository;

// Get All Notes

// Create a new Note

// Get a Single Note

// Update a Note

// Delete a Note

}
+

@RestController 注解是 Spring 中 @Controller@ResponseBody 注解的组合。

+

@Controller 注解用于定义一个控制器,@ResponseBody 注解用来表示方法的返回值应该用作请求的响应体。

+

@RequestMapping("/api") 声明这个控制器中所有 api 的 URL 将以 /api 开头。

+

接下来我们来一个一个实现这些 api。

+

1.获取所有笔记(GET /api/notes)

1
2
3
4
5
// Get All Notes
@GetMapping("/notes")
public List<Note> getAllNotes() {
return noteRepository.findAll();
}
+

上边的方法非常简单,它调用 JapRepository 的 findAll() 方法来检索数据库中所有的笔记并返回整个列表。

+

另外,@GetMapping("/notes") 注解是 @RequestMapping(value="/notes", method=RequestMethod.GET) 的简写形式。

+

2.创建一个新的笔记(POST /api/notes)

1
2
3
4
5
// Create a new Note
@PostMapping("/notes")
public Note createNote(@Valid @RequestBody Note note) {
return noteRepository.save(note);
}
+

@RequestBody 注解用于将请求体与方法参数绑定。

+

@Valid 注解确保请求体是有效的,记不记得我们在 Note 模型中用 @NotBlank 注解标记了 Note 的 title 和 content。

+

如果请求体中没有 title 或 content,Srping 将向客户端返回 400 BadRequest 错误。

+

3.获取单个笔记(GET /api/notes/{noteId})

1
2
3
4
5
6
// Get a Single Note
@GetMapping("/notes/{id}")
public Note getNoteById(@PathVariable(value = "id") Long noteId) {
return noteRepository.findById(noteId)
.orElseThrow(() -> new ResourceNotFoundException("Note", "id", noteId));
}
+

顾名思义,@PathVariable 注解用于将路径变量与方法参数绑定。

+

在上面的方法中,只要没找到指定 ID 的笔记,我们就抛出一个 ResourceNotFoundException 异常。

+

这将导致 Spring Boot 向客户端返回一个 404 Not Found 错误(我们已经为 ResourceNotFoundException 类添加了 @ResponseStatus(value=HttpStatus.NOT_FOUND) 注解)。

+

4.更新笔记(PUT /api/notes/{noteId})

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// Update a Note
@PutMapping("/notes/{id}")
public Note updateNote(@PathVariable(value = "id") Long noteId,
@Valid @RequestBody Note noteDetails) {

Note note = noteRepository.findById(noteId)
.orElseThrow(() -> new ResourceNotFoundException("Note", "id", noteId));

note.setTitle(noteDetails.getTitle());
note.setContent(noteDetails.getContent());

Note updatedNote = noteRepository.save(note);
return updatedNote;
}
+

5.删除笔记(DELETE /api/notes/{noteId})

1
2
3
4
5
6
7
8
9
10
11

// Delete a Note
@DeleteMapping("/notes/{id}")
public ResponseEntity<?> deleteNote(@PathVariable(value = "id") Long noteId) {
Note note = noteRepository.findById(noteId)
.orElseThrow(() -> new ResourceNotFoundException("Note", "id", noteId));

noteRepository.delete(note);

return ResponseEntity.ok().build();
}
+

运行应用

我们已经成功为我们的应用程序构建了所有的 api,现在运行该应用并测试 api。

+

在你的 IDE 中直接运行 EasyNotesApplication 类即可,应用将使用 Spring Boot 的默认 tomcat 端口启动。

+

接下来我们使用 postman 来测试我们的 api。

+

测试 API

使用 POST /api/notes 创建一个新的笔记

+

使用 GET /api/notes 检索全部笔记

+

使用 GET /api/notes/{noteId} 检索单个笔记

+

使用 PUT /api/notes/{noteId} 更新一个笔记

+

使用 DELETE /api/notes/{noteId} 删除一个笔记

+ +
+ + + + + + + +
+ + + + + + +
+ +
+ + + +
+ + + + + + + + +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2018/wechat-login/index.html b/2018/wechat-login/index.html new file mode 100644 index 0000000000..ece7c29ac2 --- /dev/null +++ b/2018/wechat-login/index.html @@ -0,0 +1,496 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 微信登录流程梳理 | 贾攀的流水账 + + + + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

贾攀的流水账

+ +
+

Panmax's Blog

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+ + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ 微信登录流程梳理 +

+ + +
+ + + + +
+ + +

如果没什么意外,接下来要实现一个对接微信登录的需求,今天浏览了下相关文档,简单在这里用自己的文字把整个流程描述一下。因为还没有实际操作,所以可能会有理解上的偏差,真正实现后会再来修改和补充。

+

文档链接:https://mp.weixin.qq.com/wiki?t=resource/res_main&id=mp1421140842

+

首先在微信公众平台中的「开发 - 接口权限 - 网页服务 - 网页帐号 - 网页授权获取用户基本信息」设置一个回调 URL,这里配置一个二级域名就行了,例如 wechat.xxx.com

+

用户在登录时,先通过 snsapi_base 接口获取用户 open_id,然后在我们的系统中通过这个 open_id 来获取用户信息,如果存在则直接完成登录,因为这种方式是用户无感的,使用体验比较友好。但这种方式无法获取用户最新的微信信息,不过通常来说不重要,因为用户会在应用内重新维护自己的资料,所以首次拿到用户头像昵称等信息后就可以了,之后不需要再通过重新获取用户信息来更新了。

+

如果查询 open_id 不存在的话,再通过 snsapi_userinfo 方式来获取用户信息,获取用户信息的流程如下:

+
    +
  1. 发起一个重定向让用户到 snsapi_userinfo 授权页面,用户点击登录按钮,然后系统会访问我们的回调地址并带上一个 code
  2. +
  3. 我们通过 code 换取 access_tokenrefresh_token,可以把 refresh_token 保存起来,这样的话近 30 天内都可以通过这个直接换取 access_token 来拉取用户资料,但通常没必要。
  4. +
  5. 通过 access_toekn + open_id 拉取用户资料。
  6. +
+

拉取完资料后将用户 open_id 和资料保存在我们自己的数据库中,这个步骤可以看具体业务逻辑,大部分业务会在首次登录时将用户资料拉取下来展示在一个编辑页面,用户可以编辑后再提交保存,不管那种方式我们都已经为这个 open_id 创建了用户,这样之后用户再访问我们的应用时就无需再点授权按钮了。

+ +
+ + + + + + + +
+ + + + + + +
+ +
+ + + +
+ + + + + + + + +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git "a/2018/\344\275\277\347\224\250-Supervisord-\345\256\236\347\216\260\350\277\233\347\250\213\347\233\221\346\216\247/index.html" "b/2018/\344\275\277\347\224\250-Supervisord-\345\256\236\347\216\260\350\277\233\347\250\213\347\233\221\346\216\247/index.html" new file mode 100644 index 0000000000..bf8a68a3ee --- /dev/null +++ "b/2018/\344\275\277\347\224\250-Supervisord-\345\256\236\347\216\260\350\277\233\347\250\213\347\233\221\346\216\247/index.html" @@ -0,0 +1,527 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 使用 Supervisord 实现进程监控 | 贾攀的流水账 + + + + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

贾攀的流水账

+ +
+

Panmax's Blog

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+ + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ 使用 Supervisord 实现进程监控 +

+ + +
+ + + + +
+ + +

Supervisord 是用 Python 实现的一款非常实用的进程管理工具,supervisord 还要求管理的程序是非 daemon 程序,supervisord 会帮你把它转成 daemon 程序,可以管理和监控类 UNIX 操作系统上面的进程。它可以同时启动,关闭多个进程,使用起来特别的方便。

+

组成部分

supervisor 主要由两部分组成:

+
    +
  1. supervisord(server 部分):主要负责管理子进程,响应客户端命令以及日志的输出等;
  2. +
  3. supervisorctl(client 部分):命令行客户端,用户可以通过它与不同的 supervisord 进程联系,获取子进程的状态等。
  4. +
+

安装 Supervisord:

在有 Python 环境的 Linux 机器上(基本上所有 Linux 发行版都有),直接通过 sudo easy_install supervisor 即可完成安装。

+

然后初始化配置文件:

+
1
2
mkdir /etc/supervisor
echo_supervisord_conf >/etc/supervisor/supervisord.conf
+

所有需要管理的进程需要在上边的配置文件中进行管理,但是都放在一起并不是一个好主意,一旦管理的进程过多,就很麻烦。

+

所以一般都会新建一个目录来专门放置进程的配置文件,然后通过 include 的方式来获取这些配置信息

+

修改配置文件,在最下边加上:

+
1
2
[include]
files = /etc/supervisor/conf.d/*.conf
+

并且新建相应目录:mkdir /etc/supervisor/conf.d

+

然后可以通过 supervisord 命令启动 supervisord

+
1
2
3
4
$ ps -ef | grep super
root 2675 1 0 2017 ? 00:48:43 /usr/lib64/cmf/agent/build/env/bin/python /usr/lib64/cmf/agent/build/env/bin/supervisord
magneto 6020 27867 0 11:13 pts/7 00:00:00 grep --color=auto super
magneto 27388 1 0 09:50 ? 00:00:01 /usr/bin/python /usr/bin/supervisord
+

可以看到 supervisord 已经被启动了, 然后进入 supervisorctl 的 shell 界面。

+
1
2
3
$ supervisorctl
supervisor> status
supervisor>
+

由于目前没有添加任何需要管理的进程,所以 status 没有输出任何结果,接下来我们添加一个需要管理的进程:

+
1
2
3
4
5
6
7
8
9
cd /etc/supervisor/conf.d
sudo vi cat.conf


写入以下内容:


[program:foo]
command=/bin/cat
+

然后运行以下命令更新配置并启动进程:

+
1
2
3
4
5
6
7
8
$ supervisorctl reread (只更新配置文件)
foo: available

$ supervisorctl update (只启动有改动的进程)
foo: added process group

$ supervisorctl status
foo RUNNING pid 6537, uptime 0:00:18
+

来检查下 cat 进程有没有真的启动了:

+
1
2
3
$ ps -ef | grep cat
magneto 6537 27388 0 11:18 ? 00:00:00 /bin/cat
magneto 6588 27867 0 11:19 pts/7 00:00:00 grep --color=auto cat
+

然后杀掉这个进程号,再次检查有没有重启:

+
1
2
3
4
$ kill -9 6537
$ ps -ef | grep cat
magneto 6736 27388 0 11:20 ? 00:00:00 /bin/cat
magneto 6738 27867 0 11:20 pts/7 00:00:00 grep --color=auto cat
+

进阶,让 supervisord 管理我们的项目进程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
vi eyes-tw


[program:eyes-tw]
command=java -jar /opt/eyes-tw/eyes-tw-0.0.1-SNAPSHOT.jar
stdout_logfile=/opt/eyes-tw/stdout.log
stderr_logfile=/opt/eyes-tw/stderr.log
autostart=true
autorestart=true
startsecs=20


# command 设置启动命令
# stdout_logfile 设置标准输出的输出位置
# stderr_logfile 设置标准错误的输出位置
# autostart 是否在 supervisord 启动时启动此进程
# autorestart 是否在程序异常退出后重启
# startsecs=20 启动 20 秒后没有异常退出,就当作已经正常启动
+

然后再次执行:

+
1
2
3
4
5
6
7
8
$ supervisorctl reread
eyes-tw: available

$ supervisorctl update
eyes-tw: added process group

$ supervisorctl status
eyes-tw RUNNING pid 6537, uptime 0:00:24
+

可以看到我们需要监控的项目进程已经启动成功,其他项目按照上边的 conf 模板添加就行了。

+

我把其他几个项目的配置文件写好后,执行 reread、update,完成了所有项目的启动。

+

现在 supervisorctl status 如下:

+
1
2
3
4
eyes-hk                          RUNNING   pid 32180, uptime 1:09:14
eyes-sea RUNNING pid 2744, uptime 0:51:44
eyes-tw RUNNING pid 31008, uptime 1:17:53
eyes-usa RUNNING pid 32182, uptime 1:09:14
+

命令详解

+
    +
  1. supervisord: 初始启动Supervisord,启动、管理配置中设置的进程;
  2. +
  3. supervisorctl stop(start, restart) xxx,停止(启动,重启)某一个进程(xxx);
  4. +
  5. supervisorctl reread: 只载入最新的配置文件, 并不重启任何进程;
  6. +
  7. supervisorctl reload: 载入最新的配置文件,停止原来的所有进程并按新的配置启动管理所有进程;
  8. +
  9. supervisorctl update: 根据最新的配置文件,启动新配置或有改动的进程,配置没有改动的进程不会受影响而重启;
  10. +
+ +
+ + + + + + + +
+ + + + + + +
+ +
+ + + +
+ + + + + + + + +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git "a/2018/\344\275\277\347\224\250-keepalived-\345\256\236\347\216\260\350\231\232\346\213\237IP-IP\346\274\202\347\247\273/index.html" "b/2018/\344\275\277\347\224\250-keepalived-\345\256\236\347\216\260\350\231\232\346\213\237IP-IP\346\274\202\347\247\273/index.html" new file mode 100644 index 0000000000..2c672c5eca --- /dev/null +++ "b/2018/\344\275\277\347\224\250-keepalived-\345\256\236\347\216\260\350\231\232\346\213\237IP-IP\346\274\202\347\247\273/index.html" @@ -0,0 +1,512 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 使用 keepalived 实现虚拟IP + IP漂移 | 贾攀的流水账 + + + + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

贾攀的流水账

+ +
+

Panmax's Blog

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+ + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ 使用 keepalived 实现虚拟IP + IP漂移 +

+ + +
+ + + + +
+ + +

这段时间在调研 MySQL HA 方面的东西,看到大多数实现方法都是通过虚IP + IP 漂移实现,所以打算先将此过程实现一下。

+

虚IP,就是一个未分配给真实主机的IP,也就是说对外提供数据库服务器的主机除了有一个真实IP外还有一个虚IP,使用这两个 IP 中的任意一个都可以连接到这台主机,所有项目中数据库链接一项配置的都是这个虚IP,当服务器发生故障无法对外提供服务时,动态将这个虚IP切换到备用主机。这个切换的过程我们称之为IP漂移

+

其实现原理主要是靠 TCP/IP 的 ARP 协议。因为 IP 地址只是一个逻辑 地址,在以太网中 MAC 地址才是真正用来进行数据传输的物理地址,每台主机中都有一个 ARP缓存,存储同一个网络内的IP地址与 MAC 地址的对应关系,以太网中的主机发送数据时会先从这个缓存中查询目标 IP 对应的MAC地址,会向这个 MAC 地址发送数据。操作系统会自动维护这个缓存。这就是整个实现的关键。

+

我们可以通过 Keepalived 来实现这个过程。 Keepalived 是一个基于 VRRP 协议(Virtual Router Redundancy Protocol,即虚拟路由冗余协议)来实现的LVS(负载均衡器)服务高可用方案,可以利用其来避免单点故障。

+

一个 LVS 服务会有2台服务器运行 Keepalived,一台为主服务器(MASTER),另一台为备份服务器(BACKUP),但是对外表现为一个虚拟IP,主服务器会发送特定的消息给备份服务器,当备份服务器收不到这个消息的时候,即主服务器宕机的时候,备份服务器就会接管虚拟IP,这时就需要根据 VRRP 的优先级来选举一个 backup 当 master,保证路由器的高可用,继续提供服务,从而保证了高可用性。

+

先来准备两台机器,IP地址如下:

1
2
lc1: 172.24.8.101
lc7: 172.24.8.107
+

我们现在要实现添加一个虚IP:172.24.8.150,当 lc1 机器正常时,172.24.8.150 指向 lc1,当 lc1 出现故障时指向 lc7

+

此时通过 ping 可以看到 172.24.8.150 是无法 ping 通的。

+

在这两台机器上分别安装 Keepalived

1
$ sudo yum install -y keepalived
+

配置 Keepalived

lc1 的配置

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
$ cat keepalived.conf
vrrp_instance VI_1 {
state MASTER
interface enp7s0f0
virtual_router_id 51
priority 101
advert_int 1
authentication {
auth_type PASS
auth_pass 123456
}
virtual_ipaddress {
172.24.8.150
}
}
+

lc7 的配置

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
vrrp_instance VI_1 {
state MASTER
interface enp7s0f0
virtual_router_id 51
priority 100
advert_int 1
authentication {
auth_type PASS
auth_pass 123456
}
virtual_ipaddress {
172.24.8.150
}
}
+

启动 lc1 和 lc7 上的 Keepalived 服务

1
sudo systemctl restart keepalived.service
+

将 Keepalived 加入开机启动项

1
sudo systemctl enable keepalived.service
+

测试

通过 ping 172.24.8.150 发现已经可以通了。

+

查看 lc1 的 IP信息

1
2
3
4
5
6
7
8
9
$ ip addr show enp7s0f0
2: enp7s0f0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc mq state UP qlen 1000
link/ether 6c:92:bf:0d:09:47 brd ff:ff:ff:ff:ff:ff
inet 172.24.8.101/24 brd 172.24.8.255 scope global enp7s0f0
valid_lft forever preferred_lft forever
inet 172.24.8.150/32 scope global enp7s0f0
valid_lft forever preferred_lft forever
inet6 fe80::6e92:bfff:fe0d:947/64 scope link
valid_lft forever preferred_lft forever
+

其中可以看到 inet 172.24.8.150/32 scope global enp7s0f0,说明现在 lc1 是作为虚拟IP的 master 来运行的。

+

查看 lc7 的 IP信息

1
2
3
4
5
6
7
$ ip addr show enp7s0f0
2: enp7s0f0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc mq state UP qlen 1000
link/ether 6c:92:bf:0d:21:49 brd ff:ff:ff:ff:ff:ff
inet 172.24.8.107/24 brd 172.24.8.255 scope global enp7s0f0
valid_lft forever preferred_lft forever
inet6 fe80::6e92:bfff:fe0d:2149/64 scope link
valid_lft forever preferred_lft forever
+

此时 lc7 中没有虚拟IP 的信息。

+

验证 Failover

我们手动停止 lc1 上的 Keepalived 服务:

1
sudo systemctl stop keepalived.service
+

此时 lc1 的 IP信息为:

1
2
3
4
5
6
7
$ ip addr show enp7s0f0
2: enp7s0f0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc mq state UP qlen 1000
link/ether 6c:92:bf:0d:09:47 brd ff:ff:ff:ff:ff:ff
inet 172.24.8.101/24 brd 172.24.8.255 scope global enp7s0f0
valid_lft forever preferred_lft forever
inet6 fe80::6e92:bfff:fe0d:947/64 scope link
valid_lft forever preferred_lft forever
+

可以看到 lc1 已经不在有 虚拟IP 的信息了。

+

查看 lc7 的 IP信息:

1
2
3
4
5
6
7
8
9
ip addr show enp7s0f0
2: enp7s0f0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc mq state UP qlen 1000
link/ether 6c:92:bf:0d:21:49 brd ff:ff:ff:ff:ff:ff
inet 172.24.8.107/24 brd 172.24.8.255 scope global enp7s0f0
valid_lft forever preferred_lft forever
inet 172.24.8.150/32 scope global enp7s0f0
valid_lft forever preferred_lft forever
inet6 fe80::6e92:bfff:fe0d:2149/64 scope link
valid_lft forever preferred_lft forever
+

可以看到 lc7 的 IP信息中 已经有虚拟IP 172.24.8.150 的信息了。

+

此时如果再把 lc1 上的 Keepalived 启动,可以看到 虚拟IP 又重新绑定到了 lc1 上。

+ +
+ + + + + + + +
+ + + + + + +
+ +
+ + + +
+ + + + + + + + +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2019/8-ridiculous-distributed-assumptions/index.html b/2019/8-ridiculous-distributed-assumptions/index.html new file mode 100644 index 0000000000..a8fd979f81 --- /dev/null +++ b/2019/8-ridiculous-distributed-assumptions/index.html @@ -0,0 +1,496 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 8 条荒谬的分布式假设 | 贾攀的流水账 + + + + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

贾攀的流水账

+ +
+

Panmax's Blog

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+ + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ 8 条荒谬的分布式假设 +

+ + +
+ + + + +
+ + +
    +
  1. 网络是稳定的。
  2. +
  3. 网络传输的延迟是零。
  4. +
  5. 网络的带宽是无穷大。
  6. +
  7. 网络是安全的。
  8. +
  9. 网络的拓扑不会改变。
  10. +
  11. 只有一个系统管理员。
  12. +
  13. 传输数据的成本为零。
  14. +
  15. 整个网络是同构的。
  16. +
+

在分布式系统中错误是不可能避免的,我们在分布式系统中,能做的不是避免错误,而是要把错误的处理当成功能写在代码中。

+ +
+ + + + + + + +
+ + + + + + +
+ +
+ + + +
+ + + + + + + + +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2019/Designing-Data-Intensive-Application/1.png b/2019/Designing-Data-Intensive-Application/1.png new file mode 100644 index 0000000000..049a50a2c2 Binary files /dev/null and b/2019/Designing-Data-Intensive-Application/1.png differ diff --git a/2019/Designing-Data-Intensive-Application/2.jpeg b/2019/Designing-Data-Intensive-Application/2.jpeg new file mode 100644 index 0000000000..c61ff09c86 Binary files /dev/null and b/2019/Designing-Data-Intensive-Application/2.jpeg differ diff --git a/2019/Designing-Data-Intensive-Application/3.png b/2019/Designing-Data-Intensive-Application/3.png new file mode 100644 index 0000000000..acecd9e644 Binary files /dev/null and b/2019/Designing-Data-Intensive-Application/3.png differ diff --git a/2019/Designing-Data-Intensive-Application/4.png b/2019/Designing-Data-Intensive-Application/4.png new file mode 100644 index 0000000000..d6c48ea0ee Binary files /dev/null and b/2019/Designing-Data-Intensive-Application/4.png differ diff --git a/2019/Designing-Data-Intensive-Application/5.png b/2019/Designing-Data-Intensive-Application/5.png new file mode 100644 index 0000000000..f9cd0336dd Binary files /dev/null and b/2019/Designing-Data-Intensive-Application/5.png differ diff --git a/2019/Designing-Data-Intensive-Application/index.html b/2019/Designing-Data-Intensive-Application/index.html new file mode 100644 index 0000000000..70fa0c482c --- /dev/null +++ b/2019/Designing-Data-Intensive-Application/index.html @@ -0,0 +1,511 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 好书推荐:《数据密集型应用系统设计》 | 贾攀的流水账 + + + + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

贾攀的流水账

+ +
+

Panmax's Blog

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+ + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ 好书推荐:《数据密集型应用系统设计》 +

+ + +
+ + + + +
+ + +

今天推荐一本我近期读到的质量很高的技术书(也可以说是我今年读到的最好的一本技术类书籍):《数据密集型应用系统设计》,属于「动物书」系列,封面是一只野猪。这本书我从上个月 18 号开始读,每天拿出一个半小时左右阅读,于昨天(12月28号)读完,刚好用了 40 天,全书 500 多页,也算是一本大部头了。

+

+

本书作者 Martin Kleppmann 是英国剑桥大学分布式系统方向的研究员。之前在 LinkedIn 和 Rapportive 等互联网公司做过软件工程师,负责大规模数据基础设施建设。在阅读过程中,我多次惊叹作者的知识面简直广得惊人,也善于举一反三,知识之间互相关联。

+

+

全书脉络清晰,分为三个部分:

+

第一部分介绍数据相关的基本思想,包括如何评价一个数据库(第一章),数据在逻辑上如何组织(第二章),在磁盘中如何分布(第三章),在表现上如何编码(第四章)。这些思想是一个数据系统的基本,无论它是单机的,还是分布式的。

+

第二部分介绍分布式环境下的技术,包括复制(第五章)、分区(第六章)、分布式事务与共识(第七、八、九章)。这些技术大多是基于同构系统的,分布式事务虽然也能在异构系统中应用,但是复杂度要高很多。

+

第三部分介绍异构系统中数据的处理技术,包括批处理(第十章)和流处理(第十一章),最后提出一种以流处理为主的异步数据处理方案,有可能在日后成为构建应用的主流方案(第十二章)。

+

作者在最后一小节还讨论了大数据的伦理问题,尽管在现实世界中、在金钱利益面前,可能无人理会这些事情,但是这些夫子自道,还是很体现作者情怀,可以说这也是全书升华的地方,同时让我对作者肃然起敬再次 +1。

+

书中把软件开发中(以后端为主)常用的技术本质、来龙去脉、使用场景、优点劣势都讲得非常清楚,并且讲解得深入浅出,把复杂的东西简单化,可见作者文笔之深厚。这一本书中囊括了几乎所有数据处理相关工作中可能遇到的内容,而且还提供了非常好的实操性。书中很多问题我在实际场景中也都遇到过,读起来使我醍醐灌顶、击节扼腕,每每读到我之前踩坑的地方都会想:如果我能早点读到这本书能少走很多弯路。

+

书中的配图也很到位,大部分是流程图,有时候文字读不懂的地方,看到配图就会明白,我贴几张图感受一下:

+

ETL 介绍:

+

+

出现脏读的场景:

+

+

跨多个数据中心的多主复制:

+

+

最后再来说一下本书中的一些瑕疵,中文版中有不少错别字,而且有些词汇前后翻译不一致,可能会给读者的阅读带来困扰,尤其是第三部分,明显感觉到译者不太用心了。

+

本书英文版名为:《Designing Data-Intensive Application》,出版于 17 年 3 月份。这本书在网上有个开源的翻译版本,是因为那个开源作者在 17 年读完英文版后,觉得写得很好,而此时国内又没有出版计划,所以在 Github 开始了翻译的漫漫长路。中国的官方版本直到 18 年 9 月才发布,所以阅读过程中实际上可以对照两个版本一起来学习。开源版在线阅读地址:https://vonng.gitbooks.io/ddia-cn/content/

+

最后再立个 Flag,这本书我会在 2020 年进行 2 刷。

+ +
+ + + + + + + +
+ + + + + + +
+ +
+ + + +
+ + + + + + + + +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2019/The-Road-Less-Traveled-ReadNote/index.html b/2019/The-Road-Less-Traveled-ReadNote/index.html new file mode 100644 index 0000000000..0678cd037e --- /dev/null +++ b/2019/The-Road-Less-Traveled-ReadNote/index.html @@ -0,0 +1,557 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 少有人走的路 阅读笔记 | 贾攀的流水账 + + + + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

贾攀的流水账

+ +
+

Panmax's Blog

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+ + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ 少有人走的路 阅读笔记 +

+ + +
+ + + + +
+ + +

第一部分:

自律是解决人生问题最主要的工具,也是消除人生痛苦最重要的方法。

+

解决人生问题的关键在于自律。人若缺少自律,就不可能解决任何麻烦和问题。在某些方面自律,只能解决某些问题,全面的自律才能解决人生所有的问题。

+

人生是一个不断面对问题并解决问题的过程。问题可以开启我们的智慧,激发我们的勇气。为解决问题而努力,我们的思想和心灵就会不断成长,心智就会不断成熟。

+

所谓自律,就是主动要求自己以积极的态度去承受痛苦,解决问题。

+

自律的四个原则:

+
    +
  • 推迟满足感
  • +
  • 承担责任
  • +
  • 忠于事实
  • +
  • 保持平衡
  • +
+

推迟满足感,就是不贪图暂时的安逸,先苦后甜,重新设置人生快乐与痛苦的次序:

+
    +
  • 首先,面对问题并感受痛苦;
  • +
  • 然后,解决问题并享受更大的快乐。
  • +
+

在孩子稚嫩的心中,父母就是他们的上帝,神圣而威严。孩子缺乏其他的模仿对象,自然会把父母处理问题的方式全盘接受下来。如果父母懂得自律、自制和自尊,生活井然有序,孩子就会把这样的生活视为理所当然。而如果父母的生活混乱不堪,一塌糊涂,孩子也会照单全收。

+

对自我价值的认可是自律的基础,因为当一个人觉得自己很有价值时,就会采取一切必要的措施来照顾自己。自律是自我照顾,自我珍惜,而不是自暴自弃。

+

除非存在智力障碍,不然只要花时间学习,就没什么问题解决不了。

+

尽可能早地面对问题,意味着把满足感向后推迟,放弃暂时的安逸或是程度较轻的痛苦,去体验程度较大的痛苦,这才是对待问题和痛苦最明智的办法。现在承受痛苦,将来就可能获得更大的满足感;而现在不谋求解决问题,将来的痛苦会更大,延续的时间也更长。

+

只有通过大量的生活体验,让心灵充分成长,心智足够成熟,我们才能够正确认识自己,客观评定自己和他人应该承担的责任。

+

力图把责任推给别人或是组织,就意味着我们甘愿处于附属地位,把自由和权力拱手交给命运、社会、政府、独裁者和上司。

+

幼小的孩子依赖父母,当然情有可原,如果父母独断专行,孩子也没有选择的余地。头脑清醒的成年人则可不受限制,做出适合自己的选择。

+

我们越是了解事实,处理问题就越是得心应手;对事实了解得越少,思维就越是混乱。

+

有的人一过完青春期,就放弃了绘制地图。他们的地图狭小、模糊、粗略而又肤浅,从而导致对现实的认知过于狭隘和偏激。
大多数人过了中年,就自认为地图完美无缺,世界观没有任何瑕疵,甚至自以为神圣不可侵犯,而对新的信息和资讯缺乏兴趣。
只有极少数幸运者能继续努力,他们不停地探索、扩大和更新自己对于世界的认识,直到生命终结。

+

逃避现实的痛苦是人类的天性,只有通过自律,我们才能逐渐克服现实的痛苦,及时修改自己的地图,逐步成长。我们必须忠于事实,尽管这会带来暂时的痛苦,但远比沉湎于虚假的舒适中要好。

+

自我反省对于我们的生存至关重要。反省内心世界带来的痛苦,往往大于观察外在世界带来的痛苦,所以很多人逃避前者而选择后者。实际上,认识和忠于事实带给我们的非凡价值,将使痛苦显得微不足道。自我反省带来的快乐,甚至远远大于痛苦。

+

对于想进入政治和企业高层领域的人而言,有选择地保留个人意见极为重要。凡事直言不讳的人,极易被上司认为是桀骜不驯,甚至被视为“捣乱分子”,是对组织和集体的威胁。要想在组织或集体中发挥更大的作用,就要注重表达意见的时间、场合和方式。换句话说,一个人应该有选择地表达意见和想法。

+

一个人越是诚实,保持诚实就越是容易,而谎言说得越多,则越要编造更多的谎言自圆其说。敢于面对事实的人,能够心胸坦荡地生活,不必面临良心的折磨和恐惧的威胁。

+

我们在各阶段需要放弃的东西:

+
    +
  • 无需对外界要求作出回应的婴儿状态
  • +
  • 无所不能的幻觉
  • +
  • 完全占有(包括性方面)父亲或母亲(或二者)的欲望
  • +
  • 童年的依赖感
  • +
  • 自己心中被扭曲了的父母形象
  • +
  • 青春期的自以为拥有无穷潜力的感觉
  • +
  • 无拘无束的自由
  • +
  • 青年时期的灵巧与活力
  • +
  • 青春的性吸引力
  • +
  • 长生不老的空想
  • +
  • 对子女的权威
  • +
  • 各种各样暂时性的权力
  • +
  • 身体永远健康
  • +
  • 自我以及生命本身
  • +
+

总体说来,这些就是我们在人生过程中必须放弃的生活环境、个人 欲望和处世态度。放弃这些的过程就是心智完美成长的过程。

+

第二部分 爱

爱,是为了促进自己和他人心智成熟,而不断拓展自我界限,实现自我完善的一种意愿。

+

坠入情网唯一的作用是消除寂寞,而不是有目的地促进心灵的成长。即使经过婚姻,使这一功用延长,也无助于心智的成熟。一旦坠入情网,我们便会以为自己生活在了幸福的巅峰,以为人生无与伦比,达到了登峰造极的境界。在我们眼中,对方近乎十全十美,虽然有缺点和毛病,那也算不上什么,甚至只会提升其价值,增加对方在我们眼中的魅力。在这种时候,我们会觉得心智成熟与否并不重要,重要的是当前的满足感。我们忘记了一个事实:我们和爱人的心智其实都还不完善,需要更多的滋养。

+

要学会自尊自爱,就需要自我滋养。我们需要为自己提供许多与心智有关的养分。

+
    +
  • 我们必须爱惜身体,好好照顾它;
  • +
  • 我们要拥有充足的食物,给自己提供温暖的住所;
  • +
  • 我们也需要休息和运动,张弛有度,而不是永远处在繁忙状态。
  • +
+

勇气,并不意味着永不恐惧,而是面对恐惧时能够坦然行动,克服畏缩心理,大步走向未知的未来。

+

我们应该坦然接受死亡,不妨把它当成“永远的伴侣”,想象它始终与我们并肩而行。
在死亡的指引下,我们会清醒地意识到,人生苦短,爱的时间有限,我们应该好好珍惜和把握。不敢正视死亡,就无法获得人生的真谛,无法理解什么是爱,什么是生活。万物永远处在变化中,死亡是一种正常现象,不肯接受这一事实,我们就永远无法体味生命的宏大意义。

+

真正有爱的人,绝不会随意指责爱的对象,或与对方发生冲突。他们竭力避免给对方造成傲慢的印象。动辄与所爱的人发生冲突,多半是以为自己在见识或道德上高人一等。真心爱一个人,就会承认对方是与自己不同的、完全独立的个体。

+

第三部分 成长与信仰

人们的感受和观点起源于过去的经验,却很少意识到经验并不是放之四海而皆准的法则,他们对自己的世界观并没有完整而深入的认识。

+

对于别人教给我们的一切,包括通常的文化观念以及一切陈规旧习,采取冷静和怀疑的态度,才是心智成熟不可或缺的元素。科学本身很容易成为一种文化偶像,我们亦应保持怀疑的态度。

+

第四部分 恩典

你会意识到,你具备特有的生存能力,对意外事件有着某种特殊的抵抗力,而这并不是你自主选择的结果。

+

人类有潜在的欲望和愤怒,是自然而然的事,本身并不构成问题。只有当意识不愿面对这种情形,不愿承受处理消极情感造成的痛苦,宁可对其视而不见,甚至加以摒弃和排斥时,才导致了心理疾病的产生。

+

要让心智成熟,我们需要聆听潜意识的声音,让意识中对自己的认识更接近真实的自己。

+

我们的肉体可能随着生命周期而改变,不过它早已停止了进化的历程,不会产生新的生理模式。随着年龄的增长,肉体的衰老是不可避免的结果,但在人的一生中,心灵却可以不断进化,乃至发生根本性的改变。换句话说,心灵可以始终生长发育下去,其能力可以与日俱增,直到死亡为止。

+

我们之所以能够成长,在于持续的努力;我们之所以能够付出努力,是因为懂得自尊自爱。对自己的爱使我们愿意接受自律,对别人的爱让我们帮助他们去自我完善。自我完善的爱,是一种典型的进化行为,具有生生不息的特征。在生物世界中,存在着永久而普遍的进化力量,体现在人类身上,就是具有人性的爱。它违反熵增的自然规律,是一种永远走向进步的神奇的力量。

+

阻碍心智成熟最大的障碍就是懒惰,只要克服懒惰,其他阻力都能迎刃而解;如果无法克服懒惰,不论其他条件如何完善,我们都无法取得成功。

+

不管我们精力多么旺盛,野心多么炽烈,智慧多么过人,只要深入反省,就会发现自身懒惰的一面,它是我们内心中熵的力量。在心灵进化的过程中,它始终与我们对抗,阻止我们的心智走向成熟。

+

人们总是觉得新的信息是有威胁的,因为如果新信息属实,他们就需要做大量的辛苦工作,修改关于现实的地图。他们会本能地避免这种情形的发生,宁可同新的信息较量,也不想吸收它们。他们抗拒现实的动机,固然源于恐惧,但恐惧的基础却是懒惰。他们懒得去做大量的辛苦工作。

+

在每一个人的身体中,都拥有向往神性的本能,都有达到完美境界的欲望,同时也都有懒惰的原罪。无所不在的熵的力量,试图把我们推回到人类进化的初期——那里有我们的幼年,有母亲的子宫,还有荒凉的原始沼泽。

+

邪恶是真实存在的。
所谓邪恶,就是为所欲为、横行霸道式的懒惰。
至少到目前人类进化的这一阶段,邪恶是不可避免的。
熵是一种强大的力量,是人性极恶的体现。

+

任何训诫都不能免除心灵之路上的行者必经的痛苦。你只能自行选择人生道路,忍受生活的艰辛与磨难,最终才能达到上帝的境界。

+ +
+ + + + + + + +
+ + + + + + +
+ +
+ + + +
+ + + + + + + + +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2019/Tmux-Note/index.html b/2019/Tmux-Note/index.html new file mode 100644 index 0000000000..885d30629c --- /dev/null +++ b/2019/Tmux-Note/index.html @@ -0,0 +1,517 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Tmux 使用笔记 | 贾攀的流水账 + + + + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

贾攀的流水账

+ +
+

Panmax's Blog

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+ + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ Tmux 使用笔记 +

+ + +
+ + + + +
+ + +

Tmux 是一个用于在终端窗口中运行多个终端会话的工具,即终端复用软件(terminal multiplexer)。

+

在 Tmux 中可以根据不同的工作任务创建不同的会话,每个会话又可以创建多个窗口来完成不同的工作,每个窗口又可以分割成很多小窗口。这些功能都是非常实用的。

+

在 Mac OS 中安装:

$ brew install tmux

+

开启 oh-my-zsh tmux 插件:

1
2
3
plugins=(
tmux
)
+

开启后提供以下快捷命令:

+
    +
  1. ta <session-name>:接入某个已存在的会话
  2. +
  3. ts <session-name>:新建一个指定名称的会话
  4. +
  5. tl:查看当前所有的 Tmux 会话
  6. +
  7. tksv:用于杀死全部会话
  8. +
  9. tkss <session-name>:用于杀死某个会话
  10. +
+

最简操作流程

    +
  1. 新建会话 tmux new -s my_session。
      +
    • 安装 zsh tmux 插件后可使用 ts my_session
    • +
    +
  2. +
  3. 在 Tmux 窗口运行所需的程序。
  4. +
  5. 按下快捷键 Ctrl+b d 将会话分离。
  6. +
  7. 下次使用时,重新连接到会话 tmux attach-session -t my_session
      +
    • 安装 zsh tmux 插件后可使用 ta my_session
    • +
    +
  8. +
+

常用快捷键:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
# session
Ctrl+b d:分离当前会话。
Ctrl+b s:列出所有会话(切换回话)。
Ctrl+b $:重命名当前会话。

# pane
Ctrl+b %:划分左右两个窗格。
Ctrl+b ":划分上下两个窗格。
Ctrl+b <arrow key>:光标切换到其他窗格。<arrow key>是指向要切换到的窗格的方向键,比如切换到下方窗格,就按方向键↓。
Ctrl+b ;:光标切换到上一个窗格。
Ctrl+b o:光标切换到下一个窗格。
Ctrl+b {:当前窗格左移。
Ctrl+b }:当前窗格右移。
Ctrl+b Ctrl+o:当前窗格上移。
Ctrl+b Alt+o:当前窗格下移。
Ctrl+b x:关闭当前窗格。
Ctrl+b !:将当前窗格拆分为一个独立窗口。
Ctrl+b z:当前窗格全屏显示,再使用一次会变回原来大小。
Ctrl+b Ctrl+<arrow key>:按箭头方向调整窗格大小。
Ctrl+b q:显示窗格编号。

# window
Ctrl+b c:创建一个新窗口,状态栏会显示多个窗口的信息。
Ctrl+b p:切换到上一个窗口(按照状态栏上的顺序)。
Ctrl+b n:切换到下一个窗口。
Ctrl+b <number>:切换到指定编号的窗口,其中的<number>是状态栏上的窗口编号。
Ctrl+b w:从列表中选择窗口。
Ctrl+b ,:窗口重命名。
+

解决滚轮无效的问题

新建 ~/.tmux.conf 配置文件后写入如下内容:

+
1
2
3
4
set-option -g mouse on

bind -n WheelUpPane if-shell -F -t = "#{mouse_any_flag}" "send-keys -M" "if -Ft= '#{pane_in_mode}' 'send-keys -M' 'select-pane -t=; copy-mode -e; send-keys -M'"
bind -n WheelDownPane select-pane -t= \; send-keys -M
+

需杀掉全部会话(tksv),重新启动新的会话后以上配置才能生效。

+

修改历史记录上限

历史记录默认上限为 2000 行,可通过在配置文件中加入如下配置来进行修改:

+

set-option -g history-limit 10000

+

参考链接

http://www.ruanyifeng.com/blog/2019/10/tmux.html

+ +
+ + + + + + + +
+ + + + + + +
+ +
+ + + +
+ + + + + + + + +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2019/Unable-to-create-native-threads/1.jpeg b/2019/Unable-to-create-native-threads/1.jpeg new file mode 100644 index 0000000000..78f6c42033 Binary files /dev/null and b/2019/Unable-to-create-native-threads/1.jpeg differ diff --git a/2019/Unable-to-create-native-threads/1.png b/2019/Unable-to-create-native-threads/1.png new file mode 100644 index 0000000000..5fde2b611a Binary files /dev/null and b/2019/Unable-to-create-native-threads/1.png differ diff --git a/2019/Unable-to-create-native-threads/index.html b/2019/Unable-to-create-native-threads/index.html new file mode 100644 index 0000000000..9d7b873d94 --- /dev/null +++ b/2019/Unable-to-create-native-threads/index.html @@ -0,0 +1,538 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 五分钟彻底解决 OutOfMemoryError: Unable to create native threads | 贾攀的流水账 + + + + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

贾攀的流水账

+ +
+

Panmax's Blog

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+ + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ 五分钟彻底解决 OutOfMemoryError: Unable to create native threads +

+ + +
+ + + + +
+ + +
+

作为 Java 程序员,我们几乎都会碰到 java.lang.OutOfMemoryError 异常。

+
+

+

JVM 在抛出 java.lang.OutOfMemoryError 时,除了会打印出一行描述信息,还会打印堆栈跟踪,因此我们可以通过这些信息来找到导致异常的原因。

+

其中一种异常是 java.lang.OutOfMemoryError: Unable to create native threads,我们通过这篇文章来彻底搞懂它。

+

抛出这个异常的过程大概是这样的:

+
    +
  1. Java 程序向 JVM 请求创建一个新的 Java 线程。
  2. +
  3. JVM 本地代码(Native Code)代理该请求,通过调用操作系统 API 去创建一个操作系统级别的线程 Native Thread。
  4. +
  5. 操作系统尝试创建一个新的 Native Thread,需要同时分配一些内存给该线程,每一个 Native Thread 都有一个线程栈,线程栈的大小由 JVM 参数-Xss决定。
  6. +
  7. 由于各种原因,操作系统创建新的线程可能会失败,下面会详细谈到。
  8. +
  9. JVM 抛出 java.lang.OutOfMemoryError: Unable to create new native thread 错误。
  10. +
+

因此关键在于第四步线程创建失败,JVM 就会抛出 OutOfMemoryError,那具体有哪些因素会导致线程创建失败呢?

+
    +
  1. JVM 内存大小限制
  2. +
  3. ulimit -u 限制
  4. +
  5. 参数 sys.kernel.threads-max 限制
  6. +
  7. 参数 sys.kernel.pid_max 限制
  8. +
+

第一种失败原因简单说一下:Java 创建一个线程需要消耗一定的栈空间,并通过-Xss参数指定。需要注意的是栈空间如果过小,可能会导致 StackOverflowError,尤其是在递归调用的情况下,但是栈空间过大会占用过多内存。

+

同时还要注意,对于一个 32 位 Java 应用来说,用户进程空间是 4GB,内核占用 1GB,那么用户空间就剩下 3GB,因此它能创建的线程数大致可以通过这个公式算出来:

+
1
Max memory(3GB) = [-Xmx] + [-XX:MaxMetaSpaceSize] + number_of_threads * [-Xss]
+

不过对于 64 位的应用,由于虚拟进程空间近乎无限大,因此不会因为线程栈过大而耗尽虚拟地址空间。但是请你注意,64 位的 Java 进程能分配的最大内存数仍然受物理内存大小的限制。

+

下边重点来介绍后边三种失败因素。

+

在搞明白 pid_maxulimit -uthread_max 的区别前,需要先明白进程和线程之间的区别。

+
+

一个最典型的区别是,(同一个进程内的)线程运行时共享内存空间,而进程在独立的内存空间中运行。

+
+

pid_max

pid_max 参数表示系统全局的 PID 号数值的限制,每一个进程都有 ID,ID 的值超过这个数,进程就会创建失败,pid_max 参数可以通过以下命令查看:

+
1
cat /proc/sys/kernel/pid_max
+

默认情况下,执行以上命令返回 32768,这意味着我们可以在系统中同时运行 32768进程,这些进程可以在独立的内存空间中运行。

+

修改方法

echo 65535 > /proc/sys/kernel/pid_max

+

上面只是临时生效,重启机器后会失效

+

永久生效方法:

+

/etc/sysctl.conf 中添加 kernel.pid_max = 65535

+
1
2
vi /etc/sysctl.conf
kernel.pid_max = 65535
+

或者:

+
1
echo "kernel.pid_max = 65535" >> /etc/sysctl.conf
+

threads-max

treamd-max 用来限制操作系统全局的线程数,通过以下命令查看 treamd-max 参数:

+
1
cat /proc/sys/kernel/threads-max
+

上边的命令返回 126406,这意味着我可以在共享内存空间中拥有 126406线程

+

ulimit -u

limit -u 表示当前用户可以运行的最大进程数

+

这个值怎么来的

root 账号下 ulimit -u 得到的值默认是 cat /proc/sys/kernel/threads-max 的值 / 2,即系统线程数的一半。

+

普通账号下 ulimit -u 得到的值默认是 /etc/security/limits.d/20-nproc.conf文件中指定的:

+

+

修改方式

1
ulimit -u 65535
+

ulimit 受到全局限制

ulimit -u 的值受全局的 kernel.pid_max 的值限制。也就是说如果 kernel.pid_max=1024,那么即使你的 ulimit -u 的值是 63203,用户能打开的最大进程数还是 1024。

+ +
+ + + + + + + +
+ + + + + + +
+ +
+ + + +
+ + + + + + + + +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2019/common-use-commands/index.html b/2019/common-use-commands/index.html new file mode 100644 index 0000000000..389a7f15d1 --- /dev/null +++ b/2019/common-use-commands/index.html @@ -0,0 +1,501 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 常用命令记录 | 贾攀的流水账 + + + + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

贾攀的流水账

+ +
+

Panmax's Blog

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+ + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ 常用命令记录 +

+ + +
+ + + + +
+ + +

在性能非常有限的机器上启动一个 MySQL

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
docker run --env TZ='Asia/Shanghai' \
--name daily-goals-mysql \
-v /data/daily-goals-mysql:/var/lib/mysql \
-e MYSQL_ROOT_PASSWORD=xxxx -d \
--restart=always -p 13307:3306 \
--memory=512m mariadb:10.2 \
--character-set-server=utf8mb4 \
--collation-server=utf8mb4_unicode_ci \
--performance_schema=off \
--key_buffer_size=32M \
--query_cache_size=16M --query-cache-limit=32M \
--tmp_table_size=4M \
--innodb_buffer_pool_size=32M --innodb_log_buffer_size=2M \
--max_connections=50 --sort_buffer_size=32M \
--read_buffer_size=2m --read_rnd_buffer_size=2m \
--join_buffer_size=128K \
--thread_stack=196K
+

YouTube 下载

1
2
3
4
5
# 查看所有支持下周的格式

docker run --rm --user $UID:$GID \
-v $PWD:/downloads wernight/youtube-dl \
-F https://www.youtube.com/watch?v=gxj96RCun_k
+
1
2
3
4
5
# 下载指定格式

docker run --rm --user $UID:$GID \
-v $PWD:/downloads wernight/youtube-dl \
-f code https://www.youtube.com/watch?v=gxj96RCun_k
+

ss

1
2
3
docker run -e PASSWORD=xxxxxx -e METHOD=aes-256-cfb \
-p 8443:8388 -d --restart=always \
--name ssserver shadowsocks/shadowsocks-libev
+

七牛

1
2
3
qshell account ak sk panmax

qshell rput bucket name filepath
+

CPU

查看物理CPU个数

+
1
cat /proc/cpuinfo| grep "physical id"| sort| uniq| wc -l
+

查看每个物理CPU中core的个数(即核数)

+
1
cat /proc/cpuinfo| grep "cpu cores"| uniq
+

查看逻辑CPU的个数

+
1
cat /proc/cpuinfo| grep "processor"| wc -l
+

Docker 删除所有 none 镜像

1
docker images|grep none|awk '{print $3 }'|xargs docker rmi
+

Docker 停止所有容器

1
docker stop $(docker ps -aq)
+

Docker 删除所有容器

1
docker rm $(docker ps -aq)
+

Docker 进入交互式容器

1
sudo docker exec -it {{containerName or containerID}} bash
+

TODO…

+
+ + + + + + + +
+ + + + + + +
+ +
+ + + +
+ + + + + + + + +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2019/difference-between-architecture-and-design/0.jpeg b/2019/difference-between-architecture-and-design/0.jpeg new file mode 100644 index 0000000000..3ed532c66d Binary files /dev/null and b/2019/difference-between-architecture-and-design/0.jpeg differ diff --git a/2019/difference-between-architecture-and-design/1.png b/2019/difference-between-architecture-and-design/1.png new file mode 100644 index 0000000000..dd1e1db1ad Binary files /dev/null and b/2019/difference-between-architecture-and-design/1.png differ diff --git a/2019/difference-between-architecture-and-design/index.html b/2019/difference-between-architecture-and-design/index.html new file mode 100644 index 0000000000..acc427ce58 --- /dev/null +++ b/2019/difference-between-architecture-and-design/index.html @@ -0,0 +1,543 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 聊聊软件架构和软件设计 | 贾攀的流水账 + + + + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

贾攀的流水账

+ +
+

Panmax's Blog

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+ + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ 聊聊软件架构和软件设计 +

+ + +
+ + + + +
+ + +

+

很多人并不了解软件架构和软件设计之间的区别。即使对于开发人员来说,对两者的界限也很模糊,他们可能还会把架构模式和设计模式中的内容搞混。作为一名开发人员,我想简述一下这些概念并解释软件设计和软件架构之间的区别。另外,我还会证明为什么软件架构和软件设计对我们来说很重要。

+

软件架构的定义

简单来说,软件架构是将软件特性(如灵活性、可伸缩性、可行性和安全性)转换为满足技术和业务期望的结构化解决方案的过程。这个定义使我们开始去思考可能会影响软件架构设计的各种特性。除了技术需求外,还有其他方面会影响软件架构(如商业需求或运营需求)。

+

软件架构的特点

如上所述,软件特性描述了软件在操作和技术层面上的需求和期望。所以,当老板说我们正在一个快速变化的市场当中竞争,企业需要迅速调整现有的商业模式,这时如果企业的业务需求很紧急,要求在短时间内完成的话,这个软件就应该有「可扩展、模块化和可维护」的特性。作为软件架构师,我们应该将性能(performance)、低容错性(low fault tolerance)、扩展性(scalability)和可用性(reliability)作为我们软件的关键特性。在定义了上边的几个特性后,老板告诉你我们当前预算有限,此时又要将另一个特性考虑进来,也就是「可行性」。

+

这个维基百科中列出了全部的软件特性:https://en.wikipedia.org/wiki/List_of_system_quality_attributes

+

软件架构模式

很多人之前可能听说过「微服务」。微服务是众多软件架构模式之一,其他的架构模式还有分层模式(Layered)、事件驱动模式(Event-Driven)、无服务模式(Serverless)等。我会在后后面介绍几个常见的架构模式。微服务模式在被亚马逊和 Netflix 采纳后收获了很大的影响力。现在,让我们更深入地研究架构模式。

+

这里提醒一句,不要把设计模式(如工厂模式或适配器模式)与架构模式搞混,我们在稍后讨论设计模式。

+

无服务架构

无服务架构指的是依赖第三方服务来管理服务器和后端复杂基础设施的应用解决方案。无服务架构可以分为两类:第一类是「后端即服务(BaaS)」,另一类是「函数即服务(FaaS)」。

+

无服务架构帮助我们节省了大量服务器部署和例行维护任务所花费的时间。最著名的无服务 API 提供商是亚马逊的 AWS Lambda。

+

事件驱动架构

事件驱动架构依赖事件生产者( Event Producers)和事件消费者(Event Consumers)。它的主要思想是将系统各个模块进行解耦,当有某个模块中的一个事件发生后,对这个事件感兴趣的其他模块会被触发。听起来很复杂?我们来举个简单的例子:假如你设计了一个在线购物系统,它包含两个模块:订单模块和供应商模块。如果客户产生了购买行为,订单模块会生成一个 ORDER_PENDING 事件。由于供应商模块对 ORDER_PENDING 事件感兴趣,所以它会监听这个事件,用以触发后续的行为。一旦供应商模块收到这个事件,它会执行一些任务或者触发其他后续事件(比如从某个供货商处订购更多的商品、通知仓库发货等)。

+

需要记住的是,事件生产者并不知道有哪些事件消费者在监听哪些事件。

+

微服务架构

微服务架构已成为近几年最受欢迎的架构。它依赖于开发小而独立的模块化服务,其中每个服务都可以解决特定的问题或执行独特的任务,这些模块通过定义明确的 API 互相通信来实现业务目标。对于微服务架构无需介绍太多,来看看下边这张图:

+

+

软件设计

软件架构负责软件框架和基础设施的选型,软件设计负责代码级别的设计,例如每个模块的作用、类的范围和函数用途等。

+

作为一名开发人员,了解 SOLID 原则和如何通过设计模式来解决常规问题是非常重要的。

+

SOLID 指的是单一职责原则(Single Responsibility)、开闭原则(Open Closed)、里式替换原则(Liskov substitution)、接口隔离原则(Interface Segregation)和依赖反转原则(Dependency Inversion)。

+
    +
  • 单一职责原则意味着每个类只有一个单一的目的和责任。
  • +
  • 开闭原则:一个类应该对扩展开放、对修改关闭。详细表述一下就是,添加一个新的功能应该是,在已有代码基础上扩展代码(新增模块、类、方法等),而非修改已有代码(修改模块、类、方法等)。
  • +
  • 里式替换原则:这个原则指导开发人员任何情况下使用继承都不要破坏应用逻辑。举个例子,如果子类 XyClass 继承自 AbClass,那么 XyClass 不可以改变父类已实现功能的行为。因此你可以放心地使用 XyClass 对象而不是 AbClass 对象,而不用担心破坏应用的逻辑。
  • +
  • 接口隔离原则:简单来说,因为一个类可以实现多个接口,所以应该合理的组织代码,使一个类无需被迫实现与其目的无关的方法。因此,要把接口分好类。
  • +
  • 依赖反转原则:如果你遵循 TDD(测试驱动开发Test-Driven Development)的方式进行应用开发,那么你就会知道将代码解耦对于可测试性和模块化是多么重要。举个例子,如果 Order 类依赖于 User 类,那么 User 对象应该在 Order 类之外进行实例化。
  • +
+

设计模式

工厂模式

工厂模式是 OOP 世界中最常用的设计模式,因为通过这个模式,我们在创建对象时不会对客户端暴露创建逻辑,并且是通过使用一个共同的接口来指向新创建的对象。来看个例子:

+

我们现在有一个接口和三个实现了这个接口的类:

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class Rectangle implements Shape {

@Override
public void draw() {
System.out.println("Inside Rectangle::draw() method.");
}
}

public class Square implements Shape {

@Override
public void draw() {
System.out.println("Inside Square::draw() method.");
}
}

public class Circle implements Shape {

@Override
public void draw() {
System.out.println("Inside Circle::draw() method.");
}
}
+

假如你现在要实例化一个方形 Shape,有两种方式可以实现:

+

第一种:

+
1
Shape shape = new Square();
+

第二种:

+
1
Shape shape = ShapeFactory.getShape("SQUARE");
+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class ShapeFactory {
public static Shape getShape(String shapeType){
if(shapeType == null){
return null;
}
if(shapeType.equalsIgnoreCase("CIRCLE")){
return new Circle();
} else if(shapeType.equalsIgnoreCase("RECTANGLE")){
return new Rectangle();
} else if(shapeType.equalsIgnoreCase("SQUARE")){
return new Square();
}
return null;
}
}
+

我更喜欢第二种方式,有三点原因。首先一个调用者想创建一个对象,只要知道其名称就可以了。其次扩展性高,如果想增加一个新的形状,只要扩展 ShapeFactory 工厂类就可以。最后屏蔽了具体的实现,调用者只用关心 Shape 接口,即使需要传额外参数来进行实例化,调用者也无需去关心。

+

适配器模式

适配器模式是结构设计模式之一。根据这个名字可以判断出,我们可以期望它把类的意外用法转为我们所预期的用法。

+

假如我们的应用要调用了百度的 API,需要在发起请求前需要调用 getBaiduToken() 获取 token。我们在 20 多个不同的地方调用了这个函数。之后百度在发布的新版本中把这个函数改名为了 getAccessToken()

+

现在我们必须在应用代码的所有位置找到并替换这个函数名,或者可以创建一个适配器类:

+
1
2
3
4
5
6
7
8
9
public class BaiduAdapter {

public static String getToken() {
Baidu baidu = new Baidu();
String token = baidu.getBaiduToken;
return token;
}

}
+

应用中的调用改为:

+
1
token = BaiduAdapter.getToken();
+

这种情况下,即使百度修改了函数名,我们也只需修改一行代码,应用程序的其余部分将保持正常工作。

+
1
2
3
4
5
6
7
8
9
public class BaiduAdapter {

public static String getToken() {
Baidu baidu = new Baidu();
String token = baidu.getAccessToken;
return token;
}

}
+

本文没有详细讨论各种设计模式,如果你想了解更多,我推荐 2 本关于设计模式的书:

+
    +
  • 《设计模式》
      +
    • 学习设计模式,不知道 GoF 的《设计模式》估计会被人笑话的。这本书比较晦涩难懂,对于初学者不建议从这本书看起。
    • +
    +
  • +
  • 《Head First 设计模式》
      +
    • 这本书最大的特点就是口语化、场景化。整本书围绕几个人的对话来展开。里面的例子比较脱离实践,但比较容易看懂。
    • +
    +
  • +
+

架构师 vs 程序员

最后再来说说软件架构师和软件开发人员之间的区别。

+

架构师通常是具有丰富经验的 team leader,他们对现有解决方案有很好的了解,这些方案可以帮助他们在计划阶段做出正确的决策。软件开发人员应该去了解更多的软件设计,并对软件架构有足够的了解,以使团队内部的沟通更佳高效。

+ +
+ + + + + + + +
+ + + + + + +
+ +
+ + + +
+ + + + + + + + +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2019/different-of-people/0.jpeg b/2019/different-of-people/0.jpeg new file mode 100644 index 0000000000..2fa2808c16 Binary files /dev/null and b/2019/different-of-people/0.jpeg differ diff --git a/2019/different-of-people/1.png b/2019/different-of-people/1.png new file mode 100644 index 0000000000..1669bdfcfe Binary files /dev/null and b/2019/different-of-people/1.png differ diff --git a/2019/different-of-people/index.html b/2019/different-of-people/index.html new file mode 100644 index 0000000000..ac4accd34c --- /dev/null +++ b/2019/different-of-people/index.html @@ -0,0 +1,531 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 理解人与人大不同 | 贾攀的流水账 + + + + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

贾攀的流水账

+ +
+

Panmax's Blog

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+ + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ 理解人与人大不同 +

+ + +
+ + + + +
+ + +
+

本文是对《原则》一书的第二部分,第 4 章节的概括。

+
+

+

作者认为,由于不同人的大脑构造不同,所以每个人的行为方式(原文中使用的是体验现实的方式)也千差万别。

+

开头处作者讲了一个真实案例,他交给同事鲍勃一项宏大的任务,鲍勃可以任意挑选自己的团队成员,但在事情进行一段时间后他们发现在具体落实方面毫无进展,经过长时间的讨论和研究,发现问题在于鲍勃挑选的每个角色跟他自己的长处、短板相似。

+

作者就此对人们不同的思维方式产生了浓厚的兴趣,开始探寻不同思维方式所带来的不同的力量。

+

我们很多的心理差异实际上是生理差异,大脑就像高矮胖瘦一样存在着的差异,从而影响到我们的心理能力。

+

天性

我们都有过这样的情况:对其他人做出的决策感到愤怒或者沮丧,但在了解每个人的大脑在生理上就存在不同后,就会逐渐明白他们并不是有意识地采取在我们看来低效的做法,他们只不过是依据自己认为正确的做法来做事,而这种做事方式又是由他们大脑的运行方式决定的,人与人之间出现的分歧不是因为沟通不良导致的,而是因为我们不同的思维方式导致了沟通的不良。

+

每个人的天性各不相同,这些天性即可能帮助我们又可能伤害我们,取决于我们如何运用我们的天性。这也是我们为什么经常说,具有创造力的天才和疯子往往只有一步之遥的原因。很多杰出的有创造力的人都曾患有双相障碍,如贝多芬、海明威、柴可夫斯基、丘吉尔。

+

合作

科学研究发现,人脑的构造先天地使人需要并享受社会合作。所以做有意义的工作和进行有意义的社交活动,不仅会让我们的生活更美好,更是我们天生就需要的生理需求。从社会合作中获得有意义的人际关系使我们更快乐、更健康、更有创造力,同时也会让我们的大脑发育得更好。我们的祖先进化出了支持合作功能的大脑,并以此支持狩猎等需要合作的活动,随着群体变得比个体更强大,大脑不断进化出管理更大群体的能力,这一进化使得利他意识、伦理观、良知和尊严意识发展起来。

+

斗争

我们的头脑中永远会存在两股势力间,分别是情绪和理性思考。

+

情绪主要是由潜意识性的杏仁核控制的,而理性思考主要是由意识性的前额皮层控制的。

+

杏仁核是一个小小的杏仁状构造,深深地隐藏在大脑底部,是大脑最强有力的区域之一。尽管你感觉不到它,但它控制着你的行为。作者把人们被情绪控制时的状态称为「杏仁核绑架」。如果你放任自己做出本能反应的话,你就很可能会反应过度,你也可以安慰自己,因为你已经知道,你经历的任何精神痛苦不久后都会自动消失。

+

大部分情况下,「杏仁核绑架」来得快去得也快,杏仁核产生的反应是一阵爆发然后平息,而前额皮层产生的反应更为稳定和持久。

+

我们所面临最大的挑战是让深思熟虑的较高层次的自我管理情绪性的较低层次的自我,做到这一点的最佳途径是有意识地养成习惯。习惯是大脑中最强有力的工具。习惯本质上是一种惯性,一种继续把你一直做的事情做下去(或者继续不做你一直不做的事情)的强烈倾向。研究显示,如果你能坚持某种行为约 18 个月,你就会形成一种几乎要永远做下去的强烈倾向。

+

潜意识

在我们的大脑中有两种潜意识,一种就是我们上边提到的情绪性的潜意识,它们具有危险的动物性,但我们还有一部分潜意识比意识更聪明、反应更快。

+

人们所说的灵感就是来自这部分潜意识,你会发现大部分情况下我们是在放松、不试图刻意去思考的时候会产生创造性突破。这也解释了为什么我们经常在淋浴的时候产生创意。

+

很多人认为只要往我们大脑中(也就是意识里)不断地塞入东西才能让我们进步,这样做可能会适得其反,有时候清理我们的头脑可能是取得进展的最佳途径。

+

左右脑

我们听说过一种说法,有的人是左脑思维者,有的人是右脑思维者。

+

简单来说左右脑的分工如下:

+
    +
  • 左脑按顺序推理,分析细节,并擅长线性分析。
  • +
  • 右脑思考不同类别,识别主题,综合大局。
  • +
+

通常左脑思维者人们称为「明智」的人,右脑思维者被称为「机灵」的人。

+

+

如果我们了解到自己和其他人的思维倾向,认识到这两种思维方式都各有所长,并按照思维方式的不同来对每个人分配他更加擅长的工作,可以产生很好的结果。

+

大自然塑造万物皆是有目的的,每个人都有自己的长处和短处,每个人都在他们的生活中扮演着重要的角色。我们所需要的并不是战胜其他人的勇气,而是坚持做最真实自我的勇气,不必太过在意其他人对你的冀望。

+

各司其职

在我们生活和工作中会遇到各式各样性格的人,有的人内向,有的人外向;有的人喜欢井然有序的生活方式,另一些喜欢灵活随性的方式;一些人理性分析客观事实,考虑所有与具体情况相关的已知、可证明因素,富有逻辑性地决定如何行动,而另一些偏好感觉者关注人与人之间的和谐;一些人可以看到全局,另一些人看到的是细节;一些人关注日常任务,另一些人关注目标及其实现途径。

+

可以把团队中的成员识别为5种类型,创造者、推进者、改进者、贯彻者和变通者。

+
    +
  • 创造者提出新想法、新概念。他们喜欢非结构化、抽象的活动,喜欢创新和不走寻常路。
  • +
  • 推进者传递这些新想法并推进。他们喜欢处理人与人之间的关系。他们非常善于激发工作热情。
  • +
  • 改进者挑战想法。他们分析计划以寻找缺陷,然后以很客观、符合逻辑的方式改进计划。他们喜欢事实和理论,以系统性的方式工作。
  • +
  • 贯彻者也可以叫作执行者。他们确保重要的工作得到执行,目标被实现。他们关注细节和结果。
  • +
  • 变通者是以上4种类型的结合。他们能根据特定需求调整自身,并能从各种各样的视角看待问题。
  • +
+

作者认为在我们的生命历程中,了解人的特性是必要的一步。我们做什么并不重要,只要做的事符合自己的个性和人生理想就够了。经济水平在基本生活线之上人,幸福水平和大众所认为的成功标准之间是没有任何联系的。

+

回到刚开始作者遇到的问题,无论在生活还是工作中,我们和其他人合作的最好方式都是把具有互补性特征的人搭配在一起,这样才能创建最适于完成任务的团队组合。把不同的人组织起来,更好地发挥其长处,弥补其短板,就像指挥交响乐团一样,做得好就很漂亮,做不好就很糟糕。

+

最后作者举了一个团队管理的例子,我觉得很恰当,摘抄下来:

+

在管理其他人方面,我能想到的比方是一个好乐队。乐队指挥是塑造者、引导者,他主要不是“做”(例如他不演奏乐器,尽管他了解很多关于乐器的知识),而是勾勒结果,并确保乐队所有成员一起发力实现目标。指挥要确保每个乐队成员知道自己的长处和短处,以及各自的职责。不是每个人都自己演奏得最好,而是通过合作实现“1+1 > 2”的效果。指挥最吃力不讨好的工作之一是开除总是不能好好演奏或合作的人。最重要的是,指挥要确保演奏效果和他想的一样。他说:“音乐得是这样。”然后加以落实:“贝斯手,撑起整个格局。这里要连接得妙,这里要奏出神韵。”乐队的每个部分也有各自的领导者,如首席小提琴手等,他们也帮助把作曲者和指挥的设想表达出来。

+ +
+ + + + + + + +
+ + + + + + +
+ +
+ + + +
+ + + + + + + + +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2019/dockerfile-add-vs-copy/1.png b/2019/dockerfile-add-vs-copy/1.png new file mode 100644 index 0000000000..5c93ed160f Binary files /dev/null and b/2019/dockerfile-add-vs-copy/1.png differ diff --git a/2019/dockerfile-add-vs-copy/index.html b/2019/dockerfile-add-vs-copy/index.html new file mode 100644 index 0000000000..bf5c975687 --- /dev/null +++ b/2019/dockerfile-add-vs-copy/index.html @@ -0,0 +1,519 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Dockerfile 下 ADD 与 COPY 的区别 | 贾攀的流水账 + + + + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

贾攀的流水账

+ +
+

Panmax's Blog

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+ + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ Dockerfile 下 ADD 与 COPY 的区别 +

+ + +
+ + + + +
+ + +

+

COPYADD 都是 Dockerfile 中的指令,有着类似的作用。它们允许我们将文件从特定位置复制到 Docker 镜像中。

+

COPY

COPY 指令从 <src> 复制新的文件或目录,并将它们添加到 Docker 容器文件系统的 <dest> 的路径下。

+

COPY 有两种格式:

+
    +
  • COPY [--chown=<user>:<group>] <src>... <dest>
  • +
  • COPY [--chown=<user>:<group>] ["<src>",... "<dest>"](包含空格的路径使用这种格式)
  • +
+

ADD

ADD 有两种格式:

+
    +
  • ADD [--chown=<user>:<group>] <src>... <dest>
  • +
  • ADD [--chown=<user>:<group>] ["<src>",... "<dest>"](包含空格的路径使用这种格式)
  • +
+

从 URL 复制的 Dockerfile 最佳实践

通过 URL 进行复制的效率通常很低,最佳实践是使用其他策略来包含所需的远程文件。

+

COPY 只支持基础的复制:将本地文件复制到容器中。

+

而 ADD 有一些额外的功能 :

+
    +
  • ADD 指令可以让你使用 URL 作为 <src> 参数。当遇到 URL 时候,可以通过 URL 下载文件并且复制到 <dest>
  • +
  • ADD 的另一个特性是自动解压文件的能力。如果 <src> 参数是一个可识别压缩格式(tar, gzip, bzip2, …)的本地文件注:无法实现同时下载并解压),就会被解压到指定容器文件系统的路径 <dest> 下。
  • +
+

因此,ADD 的最佳用途是将本地压缩包文件自动提取到镜像中:

+
1
ADD code.tar.gz /app/
+

由于镜像的体积很重要,所以强烈建议不要使用 ADD 从远程 URL 获取文件,我们应该使用 curlwget 来代替。这样我们可以在解压后删除这些不再需要的文件,同时还也可以避免在镜像中生成额外的层。

+

我们应该避免以下操作:

+
1
2
3
4
ADD http://example.com/big.tar.xz /usr/src/things/
RUN tar -xJf /usr/src/things/big.tar.xz -C /usr/src/things \
&& make -C /usr/src/things all \
&& rm -f /usr/src/things/big.tar.xz
+

这个压缩包解压后,rm 命令处于独立的镜像层。

+

我们可以这样做:

+
1
2
3
4
RUN mkdir -p /usr/src/things \
&& curl -SL http://example.com/big.tar.xz \
| tar -xJC /usr/src/things \
&& make -C /usr/src/things all
+

curl 会下载这个压缩包并通过管道传给 tar 命令进行解压,这样也就不会在文件系统中留下这个压缩文件了。

+

对于不需要自动解压的文件或目录,应该始终使用 COPY

+

最后,认准一个原则:总是使用 COPY(除非我们明确需要 ADD)。

+ +
+ + + + + + + +
+ + + + + + +
+ +
+ + + +
+ + + + + + + + +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2019/fast-idea-in-mac/index.html b/2019/fast-idea-in-mac/index.html new file mode 100644 index 0000000000..db7354200b --- /dev/null +++ b/2019/fast-idea-in-mac/index.html @@ -0,0 +1,487 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Idea 启动 SpringBoot 加速 | 贾攀的流水账 + + + + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

贾攀的流水账

+ +
+

Panmax's Blog

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+ + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ Idea 启动 SpringBoot 加速 +

+ + +
+ + + + +
+ + +
1
2
3
4
5
6
7
# 查看自己的 hostname
➜ hostname
jiapandeMacBook-Pro.local

# 将 hostname 加入 hosts
127.0.0.1 localhost jiapandeMacBook-Pro.local
::1 localhost jiapandeMacBook-Pro.local
+

参考:https://amsterdam.luminis.eu/2017/05/10/fixing-slow-startup-time-java-application-running-macos-sierra/

+ +
+ + + + + + + +
+ + + + + + +
+ +
+ + + +
+ + + + + + + + +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2019/fat-nutrition-and-meal/1.jpg b/2019/fat-nutrition-and-meal/1.jpg new file mode 100644 index 0000000000..5b719f9854 Binary files /dev/null and b/2019/fat-nutrition-and-meal/1.jpg differ diff --git a/2019/fat-nutrition-and-meal/1.png b/2019/fat-nutrition-and-meal/1.png new file mode 100644 index 0000000000..ce9eefaa10 Binary files /dev/null and b/2019/fat-nutrition-and-meal/1.png differ diff --git a/2019/fat-nutrition-and-meal/index.html b/2019/fat-nutrition-and-meal/index.html new file mode 100644 index 0000000000..58c03f09a1 --- /dev/null +++ b/2019/fat-nutrition-and-meal/index.html @@ -0,0 +1,525 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 我对肥胖、营养与三餐的看法 | 贾攀的流水账 + + + + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

贾攀的流水账

+ +
+

Panmax's Blog

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+ + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ 我对肥胖、营养与三餐的看法 +

+ + +
+ + + + +
+ + +

+
+

我之前在一年多的时间里,在很少运动的情况下,减掉了 50 多斤的体重(从 90 多公斤到目前稳定在 65 公斤内),同时内脏脂肪也降了下来(我的体脂称告诉我的),今年体检后各项指标也都正常,所以对本文谈到的东西也算有话语权。

+
+
+

本文不会给出任何具体的减肥方法,只是表达我的一些观点,不带有任何异端邪说请放心服用。

+
+

肥胖

肥胖是一种状态,是身体发炎的表现。肥胖是慢性病的主角,就像一股闷火在体内 24 小时不停的烧,烧着烧着就出了大问题。

+

以前人们提到心血管疾病都只是简单的归结为心血管阻塞,为什么阻塞,以前把罪嫁祸于胆固醇。

+

但是如果你的血管是健康的,它是没有理由被任何东西阻塞的,血管内壁是很光滑的组织,只有发炎的时候才会变得粗糙,才会粘附东西,不过粘附的也不一定是胆固醇。

+

胆固醇并没有罪,所以还是请大家多吃鸡蛋,不要害怕吃鸡蛋。

+

香烟在 2016 年被世界卫生组织认为是健康最大的杀手,但是 2016 年的下半年,世卫组织改口称肥胖才是人类最大的杀手,香烟已经退居到了第二位,所以最害怕的人其实是既肥胖又抽烟的人,这种绝对就是在自杀。

+

世界卫生组织关于肥胖的说明:

+

+

发炎时我们第一个想到的就是去吃消炎药,这是不对的。最好的方法是减肥,体重降了炎症也就降了,慢性病就会渐渐的消失。

+

结合上边的香烟有害健康,可以得出减肥、戒烟可以治百病

+

减肥吃减肥药是件最愚蠢的事,如果尝试过吃药减肥的人,都会感到很痛苦,但凡减肥药,都会有一种叫做安非他命的成分,安非他命是一种中枢神经兴奋剂,吃完会心跳加快、没有食欲。也早就已经被证明是一种无效的手段。

+

热量 != 营养

你认为你吃了一顿很丰盛的饭,你觉得很营养吗?其实并不是,大部分都是吃了很多的热量,热量不等于营养。

+

每克蛋白质、脂肪、淀粉中的热量分别为4卡、9卡、4卡,这个叫做热量。维他命 C 是营养,可是它并没有热量。

+

拿我本人来说,自从瘦下来后对冷的敏感度就提高了,这也只是说明我的热量小了,不代表我营养不良,没什么大不了的,多穿几件衣服,睡觉的时候盖厚点就可以了。

+

肥胖的状态就是热量过剩,还可能伴随营养不良。脂肪是没有营养的,但是我们都知道它的热量也是最多的。

+

但是脂肪是个很好的东西,不要害怕它,减肥就是要燃烧脂肪。我们要做的是热量尽量少,营养尽量多。

+

现代人都知道病是吃出来的,吃出病来后又在想我要吃些什么才能把病治好?

+

答案并不是要吃什么,而是不吃什么。

+

每天一定要吃三餐吗?

人类吃三餐的历史并不长,其实现在还有很多地方保持一日两餐的习惯。

+

三餐是工业化后的产物,餐饮业越来越发达、便利店越来越多,让我们吃东西越来越方便,方便的后果就是吃东西吃过剩、吃太多。

+

来看一个英文单词:breakfast。

+

我们都知道它是早餐的意思,但是它是两个单词的组合,break 和 fast。

+

break 的意思是 打破、阻断、破坏。

+

fast 大多数人只知道它有快的意思,但实际上它是一个医学专有名词,叫做「禁食」

+

breakfast 这个单词已经告诉我们了,早餐是破坏禁食状态,不吃才是对的,常态应该是不吃。但是很多人就会跳起来反驳我,不吃我会饿啊。别急,听我继续说。

+

现在的上班族,大多是 8 点多吃早餐,不到 12 点又要去吃午餐,甚至有的是 10 点吃早餐,12 点又要去吃,为什么?

+

这是因为我们认为吃饭的时间到了。大家都知道时间到了,就要去吃啊。

+

这种想法是不对的,正确的应该是饿了才去吃。

+

什么叫饿?饿是可以训练的,具体如何做不在本文的讨论范围内,不然又会到具体的方法上,又会被认为是异端邪说。

+

好了,今天就聊这些,本文的意图只是给大家提供一些新的视角来思考关于健康的问题。

+ +
+ + + + + + + +
+ + + + + + +
+ +
+ + + +
+ + + + + + + + +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2019/fix-docker-install-MySQL-python/index.html b/2019/fix-docker-install-MySQL-python/index.html new file mode 100644 index 0000000000..1fe9663c2e --- /dev/null +++ b/2019/fix-docker-install-MySQL-python/index.html @@ -0,0 +1,491 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 修复 Docker 安装 MySQL-python 失败的问题 | 贾攀的流水账 + + + + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

贾攀的流水账

+ +
+

Panmax's Blog

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+ + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ 修复 Docker 安装 MySQL-python 失败的问题 +

+ + +
+ + + + +
+ + +

之前开发的一个 Python 项目今天在编译 Docker 镜像时无法通过了,使用的基础镜像是 python:2.7,报错原因是在执行 pip install MySQL-python==1.2.5 时出了问题,详细报错如下:

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
ERROR: Command errored out with exit status 1:
command: /usr/local/bin/python -u -c 'import sys, setuptools, tokenize; sys.argv[0] = '"'"'/tmp/pip-install-Od1Eam/MySQL-python/setup.py'"'"'; __file__='"'"'/tmp/pip-install-Od1Eam/MySQL-python/setup.py'"'"';f=getattr(tokenize, '"'"'open'"'"', open)(__file__);code=f.read().replace('"'"'\r\n'"'"', '"'"'\n'"'"');f.close();exec(compile(code, __file__, '"'"'exec'"'"'))' bdist_wheel -d /tmp/pip-wheel-wCwIa3 --python-tag cp27
cwd: /tmp/pip-install-Od1Eam/MySQL-python/
Complete output (38 lines):
running bdist_wheel
running build
running build_py
creating build
creating build/lib.linux-x86_64-2.7
copying _mysql_exceptions.py -> build/lib.linux-x86_64-2.7
creating build/lib.linux-x86_64-2.7/MySQLdb
copying MySQLdb/__init__.py -> build/lib.linux-x86_64-2.7/MySQLdb
copying MySQLdb/converters.py -> build/lib.linux-x86_64-2.7/MySQLdb
copying MySQLdb/connections.py -> build/lib.linux-x86_64-2.7/MySQLdb
copying MySQLdb/cursors.py -> build/lib.linux-x86_64-2.7/MySQLdb
copying MySQLdb/release.py -> build/lib.linux-x86_64-2.7/MySQLdb
copying MySQLdb/times.py -> build/lib.linux-x86_64-2.7/MySQLdb
creating build/lib.linux-x86_64-2.7/MySQLdb/constants
copying MySQLdb/constants/__init__.py -> build/lib.linux-x86_64-2.7/MySQLdb/constants
copying MySQLdb/constants/CR.py -> build/lib.linux-x86_64-2.7/MySQLdb/constants
copying MySQLdb/constants/FIELD_TYPE.py -> build/lib.linux-x86_64-2.7/MySQLdb/constants
copying MySQLdb/constants/ER.py -> build/lib.linux-x86_64-2.7/MySQLdb/constants
copying MySQLdb/constants/FLAG.py -> build/lib.linux-x86_64-2.7/MySQLdb/constants
copying MySQLdb/constants/REFRESH.py -> build/lib.linux-x86_64-2.7/MySQLdb/constants
copying MySQLdb/constants/CLIENT.py -> build/lib.linux-x86_64-2.7/MySQLdb/constants
running build_ext
building '_mysql' extension
creating build/temp.linux-x86_64-2.7
gcc -pthread -fno-strict-aliasing -g -O2 -DNDEBUG -g -fwrapv -O3 -Wall -Wstrict-prototypes -fPIC -Dversion_info=(1,2,5,'final',1) -D__version__=1.2.5 -I/usr/include/mariadb -I/usr/include/mariadb/mysql -I/usr/local/include/python2.7 -c _mysql.c -o build/temp.linux-x86_64-2.7/_mysql.o
In file included from _mysql.c:44:
/usr/include/mariadb/my_config.h:3:2: warning: #warning This file should not be included by clients, include only <mysql.h> [-Wcpp]
#warning This file should not be included by clients, include only <mysql.h>
^~~~~~~
In file included from _mysql.c:46:
/usr/include/mariadb/mysql.h:440:3: warning: function declaration isn’t a prototype [-Wstrict-prototypes]
MYSQL_CLIENT_PLUGIN_HEADER
^~~~~~~~~~~~~~~~~~~~~~~~~~
_mysql.c: In function ‘_mysql_ConnectionObject_ping’:
_mysql.c:2005:41: error: ‘MYSQL’ {aka ‘struct st_mysql’} has no member named ‘reconnect’
if ( reconnect != -1 ) self->connection.reconnect = reconnect;
^
error: command 'gcc' failed with exit status 1
----------------------------------------
ERROR: Failed building wheel for MySQL-python
+

错误原因是 MariaDB 10.2、10.3 的 MySQL 版本没有定义 reconnect,需要自己来声明,只需在 pip install 前插入如下命令即可:

+
1
RUN sed '/st_mysql_options options;/a unsigned int reconnect;' /usr/include/mysql/mysql.h -i.bkp
+

非 Docker 环境执行一下 RUN 之后的命令就可以了。

+

参考:https://github.com/DefectDojo/django-DefectDojo/issues/407

+ +
+ + + + + + + +
+ + + + + + +
+ +
+ + + +
+ + + + + + + + +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2019/flag-2019/index.html b/2019/flag-2019/index.html new file mode 100644 index 0000000000..79562b9f04 --- /dev/null +++ b/2019/flag-2019/index.html @@ -0,0 +1,507 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 2019 Flags | 贾攀的流水账 + + + + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

贾攀的流水账

+ +
+

Panmax's Blog

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+ + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ 2019 Flags +

+ + +
+ + + + +
+ + +
    +
  • 至少阅读 3 本计算机相关的砖头书
      +
    • [ ] 深入理解计算机系统
    • +
    • [ ] 代码大全
    • +
    • [ ] 程序员修炼之道 - 从小工到专家(二刷)
    • +
    • [ ] UNINX 编程艺术
    • +
    • [ ] 算法
    • +
    • [ ] 计算机网络:自顶向下方法
    • +
    + +
  • +
  • 每月至少读一本非计算机类的书
  • +
  • 熟练掌握 Go 语言,并用它完成一个大中型项目
  • +
  • 阅读 & 解析 Spring、Eureka 源码
  • +
  • 学习 Team Leader
  • +
+ +
+ + + + + + + +
+ + + + + + +
+ +
+ + + +
+ + + + + + + + +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2019/go-docker-reload/0.png b/2019/go-docker-reload/0.png new file mode 100644 index 0000000000..4ff82a10dd Binary files /dev/null and b/2019/go-docker-reload/0.png differ diff --git a/2019/go-docker-reload/1.png b/2019/go-docker-reload/1.png new file mode 100644 index 0000000000..da70a0b262 Binary files /dev/null and b/2019/go-docker-reload/1.png differ diff --git a/2019/go-docker-reload/2.png b/2019/go-docker-reload/2.png new file mode 100644 index 0000000000..78371f4669 Binary files /dev/null and b/2019/go-docker-reload/2.png differ diff --git a/2019/go-docker-reload/3.png b/2019/go-docker-reload/3.png new file mode 100644 index 0000000000..f8a82463b6 Binary files /dev/null and b/2019/go-docker-reload/3.png differ diff --git a/2019/go-docker-reload/4.png b/2019/go-docker-reload/4.png new file mode 100644 index 0000000000..24a65f3d44 Binary files /dev/null and b/2019/go-docker-reload/4.png differ diff --git a/2019/go-docker-reload/5.png b/2019/go-docker-reload/5.png new file mode 100644 index 0000000000..0b8f84250b Binary files /dev/null and b/2019/go-docker-reload/5.png differ diff --git a/2019/go-docker-reload/index.html b/2019/go-docker-reload/index.html new file mode 100644 index 0000000000..dd882b7d75 --- /dev/null +++ b/2019/go-docker-reload/index.html @@ -0,0 +1,566 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Docker 部署 Go 服务并实现热加载 | 贾攀的流水账 + + + + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

贾攀的流水账

+ +
+

Panmax's Blog

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+ + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ Docker 部署 Go 服务并实现热加载 +

+ + +
+ + + + +
+ + +

Docker 足够轻量、也非常易用,并且可以确保我们所有的运行环境保持一致。

+

在这篇文章中,我将通过创建 Docker 容器来部署一个 Go API 服务。当我对源码进行修改时,这个 Go 服务也会立即重新加载。

+

通过这个方式我们就不需要再在开发过程中多次重新编译 Docker 镜像了。

+

+

创建 Go 模块

官方在 Go 的 1.13 版本中介绍了模块的概念。这意味着我们不再需要把整个工程放在 Go 的工作空间下了。

+

开始前,我创建一个新的目录 go-docker 来放置所有文件。

+

然后初始化一个 Git 仓库并创建 Go 模块。

+
1
2
3
git init
git remote add origin git@github.com:Panmax/go-docker.git
go mod init github.com/Panmax/go-docker
+

你会看到在项目目录下出现了一个 go.mod 文件。这个文件将存有这个模块下所有的依赖,类似于 Node 开发中用到的 package.json 或 Python 中的 requirements.txt

+

构建 API

模块设置好了,现在来构建一个简单的 API 服务。

+

我准备在构建这个 API 服务时使用 gorilla/mux 路由包。我也可以只用 Go 中提供的标准模块来实现路由,但我想确保模块依赖可以按照预期工作,并且利用 mux 可以支持我们构建更加复杂的应用。

+
1
go get -u github.com/gorilla/mux
+

执行这个命令后,你会看到它被作为依赖写入了 go.mod 文件。

+
1
2
3
4
5
### module github.com/Panmax/go-docker

go 1.13

require github.com/gorilla/mux v1.7.3 // indirect
+

接下来,创建这个 Go 项目的主文件 commands/runserver.go

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
package main

import (
"fmt"
"github.com/gorilla/mux"
"net/http"
)

func main() {
r := mux.NewRouter()

r.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "Hello, World!")
})

fmt.Println("Server listening!")
http.ListenAndServe(":80", r)
}
+

这个 API 只是简单返回一条消息:「Hello World!」

+

在把这个程序放进 Docker 容器前我们最好先来测试一下。通过 go run 命令来运行这个服务。

+
1
2
go run commands/runserver.go
Server listening!
+

+

API 服务可以正常工作。

+

配置 Docker

我们开始为这个项目构建 Docker 镜像。Docker 镜像包含一组用来告诉 Docker 需要提供什么环境的指令。

+
1
2
3
4
5
6
7
8
9
FROM golang:latest

WORKDIR /app

COPY ./ /app

RUN go mod download

ENTRYPOINT go run commands/runserver.go
+

使用 golang:latest 镜像作为这个自定义镜像的基础镜像。这样就可以免去 Go 开发环境的配置。

+

将整个项目拷贝到了镜像的 /app 目录下,然后通过 go mod download 下载依赖。

+

最后,我们告诉 Docker 执行 go run commands/runserver.go 命令来启动服务。

+

执行以下命令来构建这个镜像:

+
1
docker build -t go-docker-image .
+

现在我已经构建好了 Docker 镜像,接下来我们实际启动一下这个 Docker。

+
1
2
docker run go-docker-image
Server listening!
+

服务已经监听在了 Docker 容器中,但是当我通过浏览器中打开 localhost 时却发现无法访问。

+

+

出现这个情况的原因是,虽然程序在 Docker 容器内监听了 80 端口的传入请求,但是它并没有在宿主机的 80 端口上进行监听。因此我们给 localhsot 发送一个 GET 请求,它是找不到正在运行的服务的。

+

我用一张逻辑图来表述一下这个问题:

+

+

为了解决这个问题,我们需要把容器内的 80 端口映射到主机的 80 端口。

+
1
docker run -p 80:80 go-docker-image
+

端口映射后的逻辑图如下:

+

+

现在再来访问 localhost,就可以看到「Hello World!」显示在了页面上。

+

修改源码

我们来对这个 API 做一点调整:

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
package main

import (
"fmt"
"github.com/gorilla/mux"
"net/http"
)

func main() {
r := mux.NewRouter()

r.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "Hello, World!\n世界,你好!")
})

fmt.Println("Server listening!")
http.ListenAndServe(":80", r)
}
+

我在这个 API 的返回结果中新加了一行消息,我们再来启动一个新的 Docker 容器。

+
1
docker run -p 80:80 go-docker-image
+

但是如果我现在访问 localhost,看到的仍然是旧消息。

+

+

这是因为 Docker 镜像没有变化。为了使变更生效,我们必须重新构建这个镜像。

+
1
2
docker build -t go-docker-image .
docker run -p 80:80 go-docker-image
+

现在就可以看到更新后的消息了。

+

+

配置热加载

每次对代码修改后,重新构建 Docker 镜像会花费很长时间,我们来让这个系统更好用一点。

+

我要使用的是 Compile Daemon 包。如果有任何 Go 源码发生了变更,这个包会重新编译并重启我们的 Go 程序。

+
1
2
3
4
5
6
7
8
9
10
11
FROM golang:latest

WORKDIR /app

COPY ./ /app

RUN go mod download

RUN go get github.com/githubnemo/CompileDaemon

ENTRYPOINT CompileDaemon --build="go build commands/runserver.go" --command=./runserver
+

我修改了 Dockerfile 来下载 CompileDaemon 包。

+

之后修改了 ENTRYPOINT 后面的命令来运行 CompileDaemon 程序,同时为它指定了项目编译和服务启动命令。每次有文件变化后,以上命令就会被执行。

+

重新编译这个镜像:

+
1
docker build -t go-docker-image .
+

启动 Docker 时,我添加了 -v ~/Projects/go-docker:/app 参数。这样就可以把我本机的 go-docker 目录挂载到 Docker 容器内的 /app 目录下了。

+

当我修改了本机 go-docker 目录内的文件时,容器 /app 目录下的文件也会变化。

+

最终的启动命令如下。

+
1
docker run -v ~/Projects/go-docker:/app -p 80:80 go-docker-image
+

容器运行过程中,尝试修改源码,你会看到更改自动生效了。

+
1
2
3
4
5
6
7
8
9
10
2019/11/20 11:59:53 Running build command!
2019/11/20 11:59:53 Build ok.
2019/11/20 11:59:53 Hard stopping the current process..
2019/11/20 11:59:53 Restarting the given command.
2019/11/20 11:59:53 stdout: Server listening!
2019/11/20 12:00:24 Running build command!
2019/11/20 12:00:25 Build ok.
2019/11/20 12:00:25 Hard stopping the current process..
2019/11/20 12:00:25 Restarting the given command.
2019/11/20 12:00:25 stdout: Server listening!
+

使用 Docker Compose

每次运行容器时,我都要输入很长的启动命令:docker run -v ~/Projects/go-docker:/app -p 80:80 go-docker-image。在这个项目中倒没有太大问题,毕竟我才只有一个容器要启动。

+

但假设我有一个需要启动很多容器的项目,执行多个 docker run 命令会非常麻烦。

+

解决方案是使用 Docker Compose。利用这个工具,我们可以指定运行 docker-compose up 命令时要启动哪些容器。

+

为了配置它,我们需要创建一个 docker-compose.yml 文件:

+
1
2
3
4
5
6
7
8
version: "3"
services:
go-docker-image:
build: ./
ports:
- '80:80'
volumes:
- ./:/app
+

这里,我声明了要创建一个名为 go-docker-image 的镜像。这个镜像使用当前目录下的 Dockerfile 来构建。同时我配置了端口映射和目录挂载。

+

执行 docker-compose up 来启动 docker-compose.yml 中指定的容器。

+

现在,我有了一个运行在 Docker 内的 API 服务,与此同时,当代码变化时这个服务也会自动重新加载。

+

可以在这里查看项目源码:https://github.com/Panmax/go-docker

+ +
+ + + + + + + +
+ + + + + + +
+ +
+ + + +
+ + + + + + + + +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2019/how-to-check-if-a-list-is-empty-in-python/1.jpg b/2019/how-to-check-if-a-list-is-empty-in-python/1.jpg new file mode 100644 index 0000000000..59b32763f3 Binary files /dev/null and b/2019/how-to-check-if-a-list-is-empty-in-python/1.jpg differ diff --git a/2019/how-to-check-if-a-list-is-empty-in-python/index.html b/2019/how-to-check-if-a-list-is-empty-in-python/index.html new file mode 100644 index 0000000000..8711b51c37 --- /dev/null +++ b/2019/how-to-check-if-a-list-is-empty-in-python/index.html @@ -0,0 +1,522 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 如何在 Python 中判断列表是否为空 | 贾攀的流水账 + + + + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

贾攀的流水账

+ +
+

Panmax's Blog

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+ + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ 如何在 Python 中判断列表是否为空 +

+ + +
+ + + + +
+ + +
+

在判断列表是否为空时,你更喜欢哪种方式?决定因素是什么?

+
+

+

在 Python 中有很多检查列表是否是空的方式,在讨论解决方案前,先说一下不同方法涉及到的不同因素。

+

我们可以把判断表达式分为两个阵营:

+
    +
  1. 对空列表的显式比较
  2. +
  3. 对空列表的隐式求值
  4. +
+

这是什么意思?

+

显式比较

我们从显式比较开始说起,无论我们使用列表符号 [] 还是声明空列表的函数 list(),遵循的策略是查看待检查列表是否与空列表完全相等。

+
1
2
3
4
# 都是用来创建空列表
a = []
b = list()
print(a == b) # True
+

另外,我们可以使用 len() 函数返回列表中的元素个数。

+
1
2
3
a = []
if len(a) == 0:
print("The list is empty")
+

隐式求值

和显式比较相反,隐式求值遵循的策略是:将空列表求值为布尔值的 False,将有元素填充的列表求值为布尔值的 True

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
a = []
b = [1]
if a:
print("Evaluated True")
else:
print("Evaluated False")
if b:
print("Evaluated True")
else:
print("Evaluated False")


# 输出
Evaluated False
Evaluated True
+

那么,显式比较和隐式求值有什么区别呢?

+

很多人习惯于使用显式比较的方式。但是如果你遵循鸭子类型的设计风格,那么会更加偏向于使用的是隐式方法。

+

什么是鸭子类型

「鸭子类型」这个词来自以下短语:

+
+

当看到一只鸟走起来像鸭子、游泳起来像鸭子、叫起来也像鸭子,那么这只鸟就可以被称为鸭子。

+
+

从功能上讲,这是对对象实际数据类型压力较小的一种确认。在鸭子类型中,关注点在于对象的行为,能做什么(比如,可迭代 iterable);而不是关注对象所属的类型。鸭子类型在动态语言中经常使用,非常灵活。

+

鸭子类型优先考虑便利性而非安全性,从而可以使用更灵活的代码来适应更广泛的用途,它不会像传统方式那么严格。

+

我们应该使用哪种方式?

当我们越了解隐式求值,就越倾向于使用这种方式,因为我们知道空列表将被求值为 False

+
1
2
a = []
print(bool(a)) # False
+

这使得我们可以合并那些很长的检查表达式,如:

+
1
2
3
4
5
6
# 之前
if isinstance(a, list) and len(a) > 0:
print("Processing list...")
# 之后
if a:
print("Processing list...")
+

当然,最终的选择还取决于本次判断的意图:

+
    +
  • 如果你检查空列表是为了对其进行迭代,那么隐式求值是更合适的方法。
  • +
  • 如果你检查空列表是为了在之后调用列表中的方法,那么可以考虑使用显式比较来同时验证数据类型。
  • +
+ +
+ + + + + + + +
+ + + + + + +
+ +
+ + + +
+ + + + + + + + +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2019/how-to-improve-programming-skills/1.jpeg b/2019/how-to-improve-programming-skills/1.jpeg new file mode 100644 index 0000000000..819eea1fe6 Binary files /dev/null and b/2019/how-to-improve-programming-skills/1.jpeg differ diff --git a/2019/how-to-improve-programming-skills/2.jpeg b/2019/how-to-improve-programming-skills/2.jpeg new file mode 100644 index 0000000000..4fac70d2d4 Binary files /dev/null and b/2019/how-to-improve-programming-skills/2.jpeg differ diff --git a/2019/how-to-improve-programming-skills/3.jpeg b/2019/how-to-improve-programming-skills/3.jpeg new file mode 100644 index 0000000000..af643d7270 Binary files /dev/null and b/2019/how-to-improve-programming-skills/3.jpeg differ diff --git a/2019/how-to-improve-programming-skills/4.jpeg b/2019/how-to-improve-programming-skills/4.jpeg new file mode 100644 index 0000000000..9e3afafb83 Binary files /dev/null and b/2019/how-to-improve-programming-skills/4.jpeg differ diff --git a/2019/how-to-improve-programming-skills/index.html b/2019/how-to-improve-programming-skills/index.html new file mode 100644 index 0000000000..0e2f8158c1 --- /dev/null +++ b/2019/how-to-improve-programming-skills/index.html @@ -0,0 +1,512 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 如何提升编程能力 | 贾攀的流水账 + + + + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

贾攀的流水账

+ +
+

Panmax's Blog

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+ + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ 如何提升编程能力 +

+ + +
+ + + + +
+ + +
+

编程是一种技能,可以让我们不断提升和学习新知识。

+
+

+

编程是一门永远学不完的手艺。我们无法掌握所有与编程相关的主题,因为这会涉及太多的内容。如果想要自己不断进步,需要保持开放的思维,不断获取新的知识,并接受无法掌握全部知识的事实,让自己每天都有进步就够了。

+

可以通过以下三种方式实现这一目标。

+

日常编码

编码是一项与其他技术一样的技能。想要把它做好,需要大量的练习和努力。没有人会在一觉醒来后就突然变得擅长编码。所有优秀的工程师都夜以继日地工作,以完善他们的编码技能。无论你在做什么项目,用的什么编程语言,都要养成每天编写代码的习惯 —— 重要的是每天都要写代码

+

+

不要只是写代码,尝试阅读其他程序员的代码,与其他程序员讨论代码,并尝试寻找高手来 review 你的代码。编程是一门技术精湛的手艺,不能仅仅通过学习语法规则就能精通这门手艺,只有不断的练习和反思,才能取得好成绩。

+

学习多种编程语言

大学课程中引入多种编程语言是有原因的,编码知识通过语言进行传播。例如,熟悉 Java 语言的面向对象编程使你更容易理解 Go 语言中的概念,因为一些相同的编码概念适用于这两种语言。

+

+

当我们从多种语言中学习到不同的概念时,编程才开始真正地有趣起来。我从 Go 中学到结构体,从 Python 中学到了函数式编程,从 Java 中学到了面向对象编程。将多种语言的特性结合起来无疑有助于我巩固整体思维格局,并使我在编程方面做得更好。不要局限在一个小角落,经常尝试和探索未知的事物,哪怕觉得自己什么都不知道也没关系,毕竟吸收新的信息是我们学习的唯一方式。

+
+

人最害怕的不是自己什么都不会,而是自己不知道自己不会。

+
+

教导和帮助其他程序员

听说过门徒效应吗?这是一种通过教别人来学习的有趣方式。门徒效应是一种现象,在这种现象中,教授或准备将知识传授给他人可以帮助一个人学习这些知识。

+

教授一门课程意味着你必须从不同的角度来掌握它,因为你不知道学生已经掌握了多少。因此,你需要假设学生对该主题了解不多,同时意味着你必须从最基础的知识开始教学。而教授基础知识的唯一方法是你要彻底搞懂基础知识。

+

+

通过教学来学习可以借鉴小黄鸭调试法。有证据表明,教一个无生命的物体可以提高对所教知识的理解和掌握。

+

我们可以从小事开始,试着每天帮助一个人:在 GitHub 上挑选一个 issue 并解决它。为了尽可能多地学习和帮助他人,也可以在 SegmentFault 或 StackOverflow 上回答问题。

+

最后

尽管编程很难掌握,但它非常有趣。问问自己:如果你真的想掌握编程,是否愿意付出额外的努力?我想你已经知道答案了。

+ +
+ + + + + + + +
+ + + + + + +
+ +
+ + + +
+ + + + + + + + +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2019/httpie-introduce/1.jpg b/2019/httpie-introduce/1.jpg new file mode 100644 index 0000000000..168b95d72c Binary files /dev/null and b/2019/httpie-introduce/1.jpg differ diff --git a/2019/httpie-introduce/index.html b/2019/httpie-introduce/index.html new file mode 100644 index 0000000000..326909b1b4 --- /dev/null +++ b/2019/httpie-introduce/index.html @@ -0,0 +1,558 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + HTTPie介绍——一个轻量级HTTP客户端 | 贾攀的流水账 + + + + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

贾攀的流水账

+ +
+

Panmax's Blog

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+ + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ HTTPie介绍——一个轻量级HTTP客户端 +

+ + +
+ + + + +
+ + +
+

HTTPie 是一个用于与 HTTP 服务器进行交互的命令行客户端。

+
+

+

概览

HTTPie(发音为 H-T-T-派)是一个基于命令行的 HTTP 客户端,可以提供更加人类友好的命令行交互,HTTPie 可用于测试、调试以及与 HTTP 服务器进行交互。

+

HTTPie 提供了一个 http 命令,这个命令可以使用简单自然的语法发送任意 HTTP 请求,并以精美的彩色输出作为响应结果。

+

在这篇文章中,我们将学习如何使用此工具访问 REST 服务。

+

功能

作为一个现代化命令行工具,HTTPie 提供了如下功能:

+
    +
  • 简单、直观的 HTTP 命令语法
  • +
  • 漂亮的格式化输出
  • +
  • 天然的 JSON 支持
  • +
  • 表单和文件上传
  • +
  • 支持自定义 HTTP 头
  • +
  • 主流操作系统支持 —— Linux、macOS、Windows
  • +
  • 通过插件扩展额外功能
  • +
+

在后边的文章中,你将看到这些功能的介绍。

+

安装

可以通过多种方式来安装 HTTPie。

+

macOS

1
brew install httpie
+

Linux(Ubuntu)

1
apt-get install httpie
+

Windows

1
2
pip install --upgrade pip setuptools
pip install --upgrade httpie
+

或者

+
1
easy_install httpie
+

使用

现在 HTTPie 已经安装在了本地电脑上,可以来调用各种 HTTP 接口。

+

后边的文章中,我将会使用下边三个网站来演示相关功能:

+ +

调用 http

HTTPie 提供 http 命令来访问 HTTP 服务器。以下是 http 命令最基本的用法,返回了 HTTP 响应头和其他服务器信息。

+
1
2
3
4
5
6
7
8
9
10
11
12
➜ http httpie.org

HTTP/1.1 301 Moved Permanently
CF-RAY: 543ebdd4cad6eb79-LAX
Cache-Control: max-age=3600
Connection: keep-alive
Date: Thu, 12 Dec 2019 09:41:15 GMT
Expires: Thu, 12 Dec 2019 10:41:15 GMT
Location: https://httpie.org/
Server: cloudflare
Transfer-Encoding: chunked
Vary: Accept-Encoding
+

获取数据

最常见的 HTTP 操作是从服务器检索信息,通常通过 HTTP GET 方法来实现。HTTP GET 请求的查询参数是可选的。

+

下边是一个 HTTPie 的 GET 方法示例(无查询参数):

+
1
http GET http://httpbin.org/get
+

但是,不带查询参数的 GET 请求很少见。可以通过在原始请求后边追加 param==value 的方式来添加参数。

+

下边的示例演示了如何在 GET 请求中携带参数。我们来获取 userId 为 1 的所有帖子。

+
1
➜ http https://jsonplaceholder.typicode.com/posts userId==1
+

下边是多个参数的例子:

+
1
2
3
4
5
6
7
8
9
10
11
12
13
➜ http https://jsonplaceholder.typicode.com/posts userId==1 id==9

HTTP/1.1 200 OK
// 忽略响应头

[
{
"body": "consectetur animi nesciunt iure dolore\nenim quia ad\nveniam autem ut quam aut nobis\net est aut quod aut provident voluptas autem voluptas",
"id": 9,
"title": "nesciunt iure omnis dolorem tempora et accusantium",
"userId": 1
}
]
+

在 HTTP 请求头中携带信息是很常见的做法,在 HTTPie 中我们可以使用 Header:Value 格式添加 HTTP 请求头,如下所示:

+
1
http example.org X-Foo:Bar Sample:Value
+

发布和更新数据

HTTP 的 POST 方法通常用于在服务器上创建资源,下边的示例演示了如何使用内联方式提供 JSON 数据并发送 POST 请求,注意:非字符串类型参数的格式为 Param:=Value

+
1
2
3
4
5
6
7
8
9
10
11
➜ http POST https://jsonplaceholder.typicode.com/posts title=foo body=bar userId:=9

HTTP/1.1 201 Created
// 忽略响应头

{
"body": "bar",
"id": 101,
"title": "foo",
"userId": 9
}
+

HTTPie 允许我们将 JSON 数据存入文件中,并在命令行中指定这个文件的路径。

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
➜ cat data.txt
{
"title": "foo",
"body": "bar",
"userId": 9
}

➜ http POST https://jsonplaceholder.typicode.com/posts data=@data.txt

HTTP/1.1 201 Created
// 忽略响应头

{
"data": "{\n \"title\": \"foo\",\n \"body\": \"bar\",\n \"userId\": 9\n}\n",
"id": 101
}
+

HTTP PUT 方法通常用于更新服务器中已存在的资源,用法和 POST 类似:

+
1
➜ http PUT https://jsonplaceholder.typicode.com/posts/10002 data=@data.txt
+

删除数据

HTTP DELETE 方法用于删除 HTTP 服务器中的资源,示例如下:

+
1
➜ http DELETE https://jsonplaceholder.typicode.com/posts/1
+

通过 HTTPie 进行认证

上边的示例中我们演示了 HTTPie 的核心用法,在这些示例中,我们假设资源都是可以在不需要任何身份认证的情况下就能够访问的。但在实际场景中很少有这种情况,大多数服务都有安全防护,并强制要求它的用户在访问资源前进行身份认证。

+

现代化 HTTP 客户端程序为多种认证模式提供了很好的支持,HTTPie 也不例外,支持主流如:Basic、摘要、密码等认证类型。

+

使用 Basic 认证访问资源

HTTP Basic 认证是 HTTP 协议中的身份验证方案。在 Basic 认证中,HTTP Authorization 请求头设置为 Basic,用户名和密码以明文形式提供。Basic 认证总是需要配合其他安全机制,如:HTTPS。

+

以下示例演示如何访问一个要求用户通过 Basic 认证来进行身份验证的资源:

+
1
2
3
4
5
6
➜ http --default-scheme=https https://httpbin.org/basic-auth/username/password

HTTP/1.1 401 UNAUTHORIZED
Access-Control-Allow-Credentials: true
Access-Control-Allow-Origin: *
// 其他响应头
+

可以看到,请求在未提供用户名和密码的情况下,服务器的响应状态码为 401 UNAUTHORIZED。HTTPie 通过以 -a username:password 的方式提供 Basic 认证所需要的用户名密码:

+
1
2
3
4
5
6
7
8
9
10
11
➜ http --default-scheme https https://httpbin.org/basic-auth/username/password -a username:password
HTTP/1.1 200 OK
Access-Control-Allow-Credentials: true
Access-Control-Allow-Origin: *
Connection: keep-alive
// 其他响应头

{
"authenticated": true,
"user": "username"
}
+

使用摘要认证访问资源

Basic 认证的主要问题是它将用户名和密码以明文的方式发送至服务器。摘要认证略有不同,在摘要认证而非明文模式中,它采用基于哈希的方法与服务器传递凭据。

+

以下是摘要认证的流程:

+
    +
  1. 客户端请求一个需要认证的页面,但是不提供用户名和密码。
  2. +
  3. 服务器返回 401 Unauthorized 响应代码,并提供认证域(realm),以及一个随机生成的、只使用一次的数值,称为密码随机数 nonce。
  4. +
  5. 客户端以上一步中得到的随机数(nonce)、用户名、密码和 realm 的哈希值作为响应
  6. +
  7. 服务器利用这些信息对客户端进行身份验证,如果身份验证成功,则返回客户端所请求的资源
  8. +
+

HTTPie 使用 -A 摘要标志 并通过 -a 参数提供相应的用户名和密码即可进行摘要认证,如下所示:

+
1
2
3
4
5
6
7
8
9
10
11
➜ http --default-scheme https -A digest -a aa:bb https://httpbin.org/digest-auth/auth/aa/bb
HTTP/1.1 200 OK
Access-Control-Allow-Credentials: true
Access-Control-Allow-Origin: *
Connection: keep-alive
// 其他响应头

{
"authenticated": true,
"user": "aa"
}
+

插件方面,HTTPie 还支持其他身份验证机制,如:jwt-auth、OAuth 等。

+

总结

HTTPie 是一个轻量但强大的工具,可以轻松与 HTTP 服务器通信。通过 http 命令并配合合理参数调用各种 HTTP 方法的能力使其成为 RESTful 和微服务生态的理想选择。

+ +
+ + + + + + + +
+ + + + + + +
+ +
+ + + +
+ + + + + + + + +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2019/install-node-and-npm-on-linux/index.html b/2019/install-node-and-npm-on-linux/index.html new file mode 100644 index 0000000000..dac6fa8742 --- /dev/null +++ b/2019/install-node-and-npm-on-linux/index.html @@ -0,0 +1,518 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Linux 安装 node 和 npm | 贾攀的流水账 + + + + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

贾攀的流水账

+ +
+

Panmax's Blog

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+ + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ Linux 安装 node 和 npm +

+ + +
+ + + + +
+ + +

网上介绍 Node 如何安装的文章数不胜数,但我还是决定自己写一篇记录一下,最主要的原因是网上的文章比较混乱,有的建议通过包管理工具安装,还有的让一步步编译源码来安装。

+

通过包管理工具安装的通常版本不会太新,通过源码安装的方式非常麻烦,还需要提前安装 gcc 之类的,只有极少部分良心博主介绍了通过二进制文件直接安装的方式,但操作上都不是特别规范。

+

网上已有的文章还有一个很严重的问题,就是没有考虑国内的网络环境,不管从 Node 官方下载源码包还是二进制包,都巨慢无比,所以我把已经下载好的包放在 CDN 上供自己和大家之后使用。同时我还提供了其他常用软件的安装包,如 Nginx,Java,Neo4j 等等,后边有机会列个清单出来,并准备长期维护更新版本。

+
+

下边进入正题:

+
+

我推荐以下操作在 /opt 目录下进行

+
+

下载压缩包

wget http://developer.jpanj.com/node-v10.15.3-linux-x64.tar.xz

+

解压为 tar 包

xz -d node-v10.15.3-linux-x64.tar.xz

+

解压

tar -xvf node-v10.15.3-linux-x64.tar

+

当前目录下软链一个 node 目录出来

+

这样做的好处是,未来升级版本非常方便,只需要更新这个软链就行

+
+

ln -s ./node-v10.15.3-linux-x64 ./node

+

通过软链接,将可执行程序放入系统环境变量的路径中

    +
  • 查看当前系统中都有哪些环境变量路径
  • +
+
1
2
# echo $PATH
/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/root/bin
+

可以看到我的列表中有:

+
    +
  • /usr/local/bin
  • +
  • /usr/bin
  • +
+

大家约定成俗逻辑是:

+
    +
  • /usr/bin下面的都是系统预装的可执行程序,会随着系统升级而改变。
  • +
  • /usr/local/bin 目录是给用户放置自己的可执行程序的地方
  • +
+

所以我推荐将软链放在 /usr/local/bin 目录下:

+
1
2
3
ln -s /opt/node/bin/node /usr/local/bin/node
ln -s /opt/node/bin/npm /usr/local/bin/npm
ln -s /opt/node/bin/npx /usr/local/bin/npx
+

检查是否安装成功

1
2
3
4
[root@dc8 ~]# node -v
v10.15.3
[root@dc8 ~]# npm -v
6.4.1
+

Done

+
+ + + + + + + +
+ + + + + + +
+ +
+ + + +
+ + + + + + + + +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2019/install-requests-offline/index.html b/2019/install-requests-offline/index.html new file mode 100644 index 0000000000..be43ee0d00 --- /dev/null +++ b/2019/install-requests-offline/index.html @@ -0,0 +1,509 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 离线安装 Python requests 包 | 贾攀的流水账 + + + + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

贾攀的流水账

+ +
+

Panmax's Blog

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+ + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ 离线安装 Python requests 包 +

+ + +
+ + + + +
+ + +
+

requests 是一个简单优雅的 Python HTTP 库,相较于 Python 标准库中的 urlliburllib2requests 更加的便于理解使用。

+
+

背景介绍

由于某地区热点事件持续升温,我们的客户想要通过我们系统的搜索功能导出一批数据,目前我们的搜索结果是不支持导出的,并且搜索功能也是通过调用几个子服务后对数据进行了合并,所以无法直接通过 ElasticSearch 来捞数据。

+

我们在评估需求后,预计编写这个统计程序大概需要 1 天的时间,但是客户认为事态紧急,当天就要结果,我们本着顾客就是上帝的原则,又进行了一番讨论,结论是可以写一个类似爬虫的工具,来「爬取」我们自己的搜索接口来拿到这些数据。

+

Python 来实现最合适不过,而且我对编写爬虫也比较熟悉,所以就采用了最简单粗暴的方法:用 requests 包作为一个 HTTP Client 来收发请求,但是客户现场是个离线环境,之前我们也没有安装过 requests,所以才有了本文:在离线环境中安装 requests

+

正文

资源准备

为方便后期使用,我将所有用到的文件打包在了一起,可直接解压使用,无需从网上东奔西走寻找资源。

+

压缩包内涉及到的文件如下:

+
1
2
3
4
5
6
7
setuptools-41.1.0.post1.tar
pip-19.2.2.tar.gz
certifi-2019.9.11-py2.py3-none-any.whl
chardet-3.0.4-py2.py3-none-any.whl
idna-2.8-py2.py3-none-any.whl
urllib3-1.25.7-py2.py3-none-any.whl
requests-2.22.0.tar.gz
+

打包资源下载链接:http://developer.jpanj.com/requests-offline.tar.gz

+

安装

解压 requests-offline.tar.gz 后进入 requests-offline 目录开始安装。

+

安装 setuptools

+

setuptools 能帮助我们更好的创建和分发 Python 的包,尤其是具有复杂依赖关系的包。

+
+
1
2
3
tar -zxvf setuptools-41.1.0.post1.tar.gz
cd setuptools-41.1.0.post1/
python setup.py install
+

安装 pip

+

pip 是 Python 官方推荐的包管理工具。

+
+
1
2
3
tar -zxvf pip-19.2.2.tar.gz
cd pip-19.2.2/
python setup.py install
+

安装 requests 所需的其他依赖

1
2
3
4
5
6
7
8
# CA 认证模块
pip install certifi-2019.9.11-py2.py3-none-any.whl
# 字符编码检测模块
pip install chardet-3.0.4-py2.py3-none-any.whl
# 域名解析模块
pip install idna-2.8-py2.py3-none-any.whl
# 线程安全的 HTTP 库
pip install urllib3-1.25.7-py2.py3-none-any.whl
+

安装 requests

1
2
3
tar -zxvf requests-2.22.0.tar.gz
cd requests-2.22.0/
python setup.py install
+

测试是否成功成功

1
2
➜ python
>>> import requests
+

后记

客户是下午两点半提出的需求,内部讨论好实现方案后三点半,我在准备好安装 requests 所需资源后直接奔赴现场,而不是在公司编码完再过去,因为我相信只要有我们的人到达现场客户就可以踏下心来了,同时我也想挑战一下自己,所以本次点亮了一个技能点:现场使用 vi 进行 coding 和 debug。

+ +
+ + + + + + + +
+ + + + + + +
+ +
+ + + +
+ + + + + + + + +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2019/java-8-stringjoiner/index.html b/2019/java-8-stringjoiner/index.html new file mode 100644 index 0000000000..1d8e177fd1 --- /dev/null +++ b/2019/java-8-stringjoiner/index.html @@ -0,0 +1,503 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Java 8 StringJoiner 示例 | 贾攀的流水账 + + + + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

贾攀的流水账

+ +
+

Panmax's Blog

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+ + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ Java 8 StringJoiner 示例 +

+ + +
+ + + + +
+ + +

在 Java8 中,java.util 包中引入了一个新类 StringJoiner。利用这个类,我们可以使用指定分隔符连接多个字符串,还可以为最终字符串添加前缀和后缀。

+

下边介绍一些 StringJoiner 类的示例。

+

示例1:通过指定分隔符连接字符串

在这个例子中,我们使用 StringJoiner 连接多个字符串,在创建 StringJoiner 实例时我们将分隔符指定为连字符(-)。

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import java.util.StringJoiner; 

public class Example {
public static void main(String[] args) {
// 传递连字符(-)作为分隔符
StringJoiner mystring = new StringJoiner("-");

// 通过 add() 方法连接多个字符串
mystring.add("张三");
mystring.add("李四");
mystring.add("王五");
mystring.add("赵六");

System.out.println(mystring);
}
}
+

输出:

+
1
张三-李四-王五-赵六
+

示例2:为输出字符串添加前缀和后缀

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import java.util.StringJoiner;  

public class Example {
public static void main(String[] args) {
/* 传递逗号(,)作为连字符
* 左括号为前缀右括号为后缀
*/
StringJoiner mystring = new StringJoiner(",", "(", ")");

mystring.add("张三");
mystring.add("李四");
mystring.add("王五");
mystring.add("赵六");

System.out.println(mystring);
}
}
+

输出:

+
1
(张三,李四,王五,赵六)
+

示例3:合并两个 StringJoiner 对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
import java.util.StringJoiner;

public class Example {
public static void main(String[] args) {
StringJoiner mystring = new StringJoiner(",", "(", ")");

mystring.add("张三");
mystring.add("李四");
mystring.add("王五");
mystring.add("赵六");

System.out.println("First String: " + mystring);

StringJoiner myanotherstring = new StringJoiner("-", "pre", "suff");

myanotherstring.add("小张");
myanotherstring.add("小李");
myanotherstring.add("小王");
myanotherstring.add("小赵");

System.out.println("Second String: " + myanotherstring);

/* 合并两个字符串要注意的是
* 输出字符串将具有第一个字符串的分隔符前缀和后缀
*/
StringJoiner mergedString = mystring.merge(myanotherstring);
System.out.println(mergedString);
}
}
+

输出:

+
1
2
3
First String: (张三,李四,王五,赵六)
Second String: pre小张-小李-小王-小赵suff
(张三,李四,王五,赵六,小张-小李-小王-小赵)
+

在上边的例子中我们学习了 StringJoiner 类的 add()merge() 方法,再来看看这个类的其他方法。

+

setEmptyValue(),length() 和 toString() 方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
import java.util.StringJoiner;  

public class Example {
public static void main(String[] args) {

StringJoiner mystring = new StringJoiner(",");

/* 使用 setEmptyValue() 方法可以设置 StringJoiner 实例
* 的默认值如果 StringJoiner 为空并且
* 我们打印它的值就会显示此默认值
*/
mystring.setEmptyValue("这是个默认字符串");

/* 我们还没有向 StringJoiner 添加任何字符串
* 所以这应该显示 StringJoiner 的默认值
*/
System.out.println("Default String: " + mystring);


mystring.add("苹果");
mystring.add("香蕉");
mystring.add("橘子");
mystring.add("猕猴桃");
mystring.add("葡萄");
System.out.println(mystring);

/* StringJoiner 类的 length() 方法返回
* 字符串的长度(StringJoiner 实例中的字符数)
*/
int length = mystring.length();
System.out.println("Length of the StringJoiner: " + length);

/* toString() 方法用于将 StringJoiner
* 实例转换为字符串
*/
String s = mystring.toString();
System.out.println(s);
}
}
+

输出:

+
1
2
3
4
Default String: 这是个默认字符串
苹果,香蕉,橘子,猕猴桃,葡萄
Length of the StringJoiner: 15
苹果,香蕉,橘子,猕猴桃,葡萄
+

参考:

Java 8 – StringJoiner JavaDoc

+ +
+ + + + + + + +
+ + + + + + +
+ +
+ + + +
+ + + + + + + + +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2019/nginx-reverse-proxy-and-ssl-template/index.html b/2019/nginx-reverse-proxy-and-ssl-template/index.html new file mode 100644 index 0000000000..4e832be3f6 --- /dev/null +++ b/2019/nginx-reverse-proxy-and-ssl-template/index.html @@ -0,0 +1,486 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + nginx 配置反向代理 + ssl 模板 | 贾攀的流水账 + + + + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

贾攀的流水账

+ +
+

Panmax's Blog

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+ + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ nginx 配置反向代理 + ssl 模板 +

+ + +
+ + + + +
+ + +
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
server {
listen 80;
server_name abc.com;

location ^~ /.well-known/acme-challenge/ {
alias /xxx/xxx/;
try_files $uri =404;
}

location / { // 强制 https 重定向
rewrite ^(.*)$ https://$host$1 permanent;
}


}

server {
listen 443 ssl;
server_name abc.com;

location / {
proxy_pass http://127.0.0.1:8000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header REMOTE-HOST $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}

ssl on;
ssl_certificate /root/ssl/chained.pem;
ssl_certificate_key /root/ssl/domain.key;
}

// 静态网站
server {
listen 443 ssl;
server_name xxx.com;

root /www/xxx;
index index.html;
error_page 404 /404.html;

ssl on;
ssl_certificate /root/ssl/chained.pem;
ssl_certificate_key /root/ssl/domain.key;
}
+ +
+ + + + + + + +
+ + + + + + +
+ +
+ + + +
+ + + + + + + + +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2019/raid-and-hdfs/index.html b/2019/raid-and-hdfs/index.html new file mode 100644 index 0000000000..32ea007f7b --- /dev/null +++ b/2019/raid-and-hdfs/index.html @@ -0,0 +1,573 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + RAID 介绍,为什么不推荐为 HDFS 配置 RAID? | 贾攀的流水账 + + + + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

贾攀的流水账

+ +
+

Panmax's Blog

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+ + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ RAID 介绍,为什么不推荐为 HDFS 配置 RAID? +

+ + +
+ + + + +
+ + +

RAID

开始前先来思考一个问题,如果一个文件的大小超过了一块磁盘的大小,该如何存储?

+

独立硬盘冗余阵列(RAID, Redundant Array of Independent Disks),简称磁盘阵列,利用虚拟化存储技术把多个磁盘组合起来,成为一个或多个磁盘阵列组,目的为提升性能或数据冗余,或是两者同时提升。

+

简单来说,RAID 把多个磁盘组合成为一个逻辑磁盘,因此,操作系统只会把它当作一个实体磁盘。

+

常见 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 0RAID 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 块磁盘比其他磁盘更容易损坏,需要频繁更换,所以 RAID 3 很少在实践中使用。

+

RAID 5

+

相比 RAID 3RAID 5 是使用更多的方案。RAID 5RAID 3 很相似,但是校验数据不是写入第 N 块磁盘,而是螺旋式地写入所有磁盘中。这样校验数据的修改也被平均到所有磁盘上,避免 RAID 3 频繁写坏一块磁盘的情况。

+

RAID 5 至少需要三块磁盘,RAID 5 不是对存储的数据进行备份,而是把数据和相对应的奇偶校验信息存储到组成 RAID 5 的各个磁盘上,并且奇偶校验信息和相对应的数据分别存储于不同的磁盘上。当 RAID 5 的一个磁盘数据发生损坏后,可以利用剩下的数据和相应的奇偶校验信息去恢复被损坏的数据。RAID 5 可以理解为是 RAID 0RAID 1 的折衷方案。

+

RAID 6

+

如果数据需要很高的可靠性,在出现同时损坏两块磁盘的情况下(或者运维管理水平比较落后,坏了一块磁盘但是迟迟没有更换,导致又坏了一块磁盘),仍然需要修复数据,这时候可以使用 RAID 6

+

RAID 5 相比 RAID 6 增加第二个独立的奇偶校验信息块。两个独立的奇偶系统使用不同的算法,数据的可靠性非常高,任意两块磁盘同时失效时不会影响数据完整性。

+

各种 RAID 技术比较

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
RAID类型访问速度数据可靠性磁盘利用率目的
RAID 0很快很低100%追求最大容量、速度
RAID 1很慢很高50%追求最大安全性
RAID 10中等很高50%总和 RAID 0/1 优点,理论速度较快
RAID 5较快很高(N-1)/N追求最大容量、最小预算
RAID 6较快较 RAID 5 高(N-2)/N同 RAID 5,但更安全
+

HDFS

RAID 可以看作是一种垂直伸缩,一台计算机集成更多的磁盘实现数据更大规模、更安全可靠的存储以及更快的访问速度。而 HDFS 则是水平伸缩,通过添加更多的服务器实现数据更大、更快、更安全存储与访问。

+

Hadoop 分布式文件系统 HDFS 的设计目标是管理数以千计的服务器、数以万计的磁盘,将这么大规模的服务器计算资源当作一个单一的存储系统进行管理,对应用程序提供数以 PB 计的存储容量,让应用程序像使用普通文件系统一样存储大规模的文件数据。

+

为什么不推荐为 HDFS 配置 RAID?

HDFS 已经为同一个文件保留了多个副本,如果磁盘发生故障 HDFS 可以将其恢复。HDFS 同样可以一次从多个节点(DataNode)读取数据,如果使用 RAID 1,将浪费更多的存储空间,如果使用 RAID 0,产生失败的可能性会提升 N 倍(N = 磁盘数量)。使用 RAID 5/6 的话,读写速度将受到影响,也更昂贵。

+

不过由于 NameNodeHDFS 中容易出现单点故障,因此需要更可靠的硬件配置,所以建议在 NameNode 上使用 RAID

+

其他说明

在一些 Master 处理节点上,如:Hive 的 MetaStore,也推荐使用 RAID。同时建议为所有的系统盘配置 RAID,你不会希望仅仅因为系统盘故障而导致节点故障。

+

对于 ElasticSearch,其本身也提供了很好的 HA 机制,同样无需使用 RAID

+

最后

回到开头的那个问题,我的回答是:

+
    +
  • 单机时代:RAID
  • +
  • 分布式时代:分布式文件系统
  • +
+ +
+ + + + + + + +
+ + + + + + +
+ +
+ + + +
+ + + + + + + + +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2019/raid-and-hdfs/raid0.png b/2019/raid-and-hdfs/raid0.png new file mode 100644 index 0000000000..e1ff8a05b3 Binary files /dev/null and b/2019/raid-and-hdfs/raid0.png differ diff --git a/2019/raid-and-hdfs/raid1.png b/2019/raid-and-hdfs/raid1.png new file mode 100644 index 0000000000..f62e78e791 Binary files /dev/null and b/2019/raid-and-hdfs/raid1.png differ diff --git a/2019/raid-and-hdfs/raid10.png b/2019/raid-and-hdfs/raid10.png new file mode 100644 index 0000000000..6e9c12d9ad Binary files /dev/null and b/2019/raid-and-hdfs/raid10.png differ diff --git a/2019/raid-and-hdfs/raid3.png b/2019/raid-and-hdfs/raid3.png new file mode 100644 index 0000000000..8ddd91e2d2 Binary files /dev/null and b/2019/raid-and-hdfs/raid3.png differ diff --git a/2019/raid-and-hdfs/raid5.png b/2019/raid-and-hdfs/raid5.png new file mode 100644 index 0000000000..05be8a91c3 Binary files /dev/null and b/2019/raid-and-hdfs/raid5.png differ diff --git a/2019/raid-and-hdfs/raid6.png b/2019/raid-and-hdfs/raid6.png new file mode 100644 index 0000000000..72666d87c6 Binary files /dev/null and b/2019/raid-and-hdfs/raid6.png differ diff --git a/2019/real-world-go-interface/1.jpeg b/2019/real-world-go-interface/1.jpeg new file mode 100644 index 0000000000..c48f294810 Binary files /dev/null and b/2019/real-world-go-interface/1.jpeg differ diff --git a/2019/real-world-go-interface/index.html b/2019/real-world-go-interface/index.html new file mode 100644 index 0000000000..97c812024d --- /dev/null +++ b/2019/real-world-go-interface/index.html @@ -0,0 +1,514 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 用一个现实世界中的例子介绍 Go 接口 | 贾攀的流水账 + + + + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

贾攀的流水账

+ +
+

Panmax's Blog

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+ + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ 用一个现实世界中的例子介绍 Go 接口 +

+ + +
+ + + + +
+ + +

+

假设我现在要用 Go 编写一个 Web 应用。在这个应用里,我要实现给用户发送消息的功能。我可以通过邮件或短信等方式来发送这条消息,这是一个完美的接口使用场景。

+

在这个虚构的 Web 应用中,先来创建如下 main.go 文件:

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
package main

import "fmt"

type User struct {
Name string
Email string
}

type UserNotifier interface {
SendMessage(user *User, message string) error
}

func main() {
user := &User{"Panmax", "panmax@email.com"}

fmt.Printf("Welcome %s\n", user.Name)
}
+

这里的 User 结构体代表一个用户。

+

可以看到我创建了只有一个函数 SendMessage()UserNotifier 接口。

+

为了实现这个接口,我需要创建一个结构体来实现 SendMessage() 函数。

+
1
2
3
4
5
6
7
type EmailNotifier struct {
}

func (notifier EmailNotifier) SendMessage(user *User, message string) error {
_, err := fmt.Printf("Sending email to %s with content %s\n", user.Name, message)
return err
}
+

正如你看到的,我创建了一个新的 EmailNotifier 结构体。然后我给这个结构体实现了 SendMessage() 方法。在这个例子中,EmailNotifier 只是简单打印了一条消息。在现实世界中你可能需要调用发送邮件的 API,比如 Mailgun

+

到此,UserNotifier 接口已经实现了,就是这么简单。

+

下一步要做的是使用 UserNotifier 接口为用户发送一份邮件。

+
1
2
3
4
5
6
7
8
func main() {
user := User{"Panmax", "panmax@email.com"}
fmt.Printf("Welcome %s\n", user.Name)

var userNotifier UserNotifier
userNotifier = EmailNotifier{}
userNotifier.SendMessage(&user, "Interfaces all the way!")
}
+

运行这个程序,EmailNotifierSendMessage 方法被正确调用了。

+
1
2
3
4
go build -o main main.go
./main
Welcome Panmax
Sending email to Panmax with content Interfaces all the way!
+

下边我们来实现发送短信的接口。

+
1
2
3
4
5
6
7
type SmsNotifier struct {
}

func (notifier SmsNotifier) SendMessage(user *User, message string) error {
_, err := fmt.Printf("Sending SMS to %s with content %s\n", user.Name, message)
return err
}
+

我们可以把 Notifier 放进用户结构体中,这样每个用户都有一个属于自己的 Notifier,是不是很酷。

+
1
2
3
4
5
type User struct {
Name string
Email string
Notifier UserNotifier
}
+

然后,我们向 User 结构体中添加一个便捷方法 notify(),这个方法使用 UserNotifier 接口给用户发送消息。

+
1
2
3
func (user *User) notify(message string) error {
return user.Notifier.SendMessage(user, message)
}
+

最后,我在 main() 函数中创建两个用户,分别调用了它们的 notify() 方法。

+
1
2
3
4
5
6
7
func main() {
user1 := User{"Dirk", "dirk@email.com", EmailNotifier{}}
user2 := User{"Justin", "bieber@email.com", SmsNotifier{}}

user1.notify("Welcome Email user!")
user2.notify("Welcome SMS user!")
}
+

最终结果正是我们所预期的:

+
1
2
3
4
go build -o main main.go
./main
Sending email to Panmax with content Welcome Email user!
Sending SMS to Panmax with content Welcome SMS user!
+

结语

本文介绍了 Go 接口是如何工作的,同时用一个现实中简单的例子进行了演示。

+

希望对你有所帮助。

+ +
+ + + + + + + +
+ + + + + + +
+ +
+ + + +
+ + + + + + + + +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2019/serverless-introduce/1.png b/2019/serverless-introduce/1.png new file mode 100644 index 0000000000..65550a0677 Binary files /dev/null and b/2019/serverless-introduce/1.png differ diff --git a/2019/serverless-introduce/2.png b/2019/serverless-introduce/2.png new file mode 100644 index 0000000000..bc5456a5e3 Binary files /dev/null and b/2019/serverless-introduce/2.png differ diff --git a/2019/serverless-introduce/index.html b/2019/serverless-introduce/index.html new file mode 100644 index 0000000000..d9cdc5b322 --- /dev/null +++ b/2019/serverless-introduce/index.html @@ -0,0 +1,544 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Serverless 入门 | 贾攀的流水账 + + + + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

贾攀的流水账

+ +
+

Panmax's Blog

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+ + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ Serverless 入门 +

+ + +
+ + + + +
+ + +

+
+

云技术已经彻底改变了我们管理应用程序的方式,尽管很多公司早已不再使用物理服务器,但他们仍然从服务器的角度来看待他们的系统。

+
+

如果我们试图把服务器的概念忘掉,并开始把基于云的应用程序视为工作流、分布式逻辑和外部管理的数据存储,会是什么情况?

+

本文文我们一起探讨下 Serverless。

+

和其它软件开发趋势一样,Serverless 并没有一个清晰的概念,它可以用在两种不同但又有些相似的领域:

+
    +
  • Serverless 最初是用来描述那些结合第三方、云托管来管理服务器端逻辑和状态的应用。通常是一些「富客户端」应用,比如单页 web 应用或者手机 APP,它们可以使用第三方提供的庞大生态系统来进行云端存储(比如:国内的 LeanCloud,国外的 Parse、Firbase)。这些类型的服务之前被称为「后端即服务」或「BaaS」。
  • +
  • Serverless 还可以表示另一种情况,开发人员仍然编写服务器端应用代码,但是与传统架构不同,这个应用运行在无状态的容器中,这些容器是事件触发、短暂(可能只调用一次)并且由第三方来管理的。这种做法通常被理解为「函数即服务」或「FaaS」。AWS Lambda 是目前提供「函数即服务」的实现之一。
  • +
+

尽管有 Serverless 这个名字,但实际并不是在没有服务器的情况下运行代码。之所以使用「无服务器计算(serverless computing)」这个名称,是因为拥有系统的企业或个人不必为运行后端应用而采购、租用、配置服务器或虚拟机。

+

Serverless 有以下优势:

+
    +
  • 无服务器管理(无需管理任何形式的服务器)
  • +
  • 按执行付费(不为空闲时间买单)
  • +
  • 自动伸缩(根据需求伸缩)
  • +
  • 函数作为应用的逻辑单元
  • +
+

Serverless 模式鼓励将开发重点放在定义明确的业务逻辑单元上,而无需考虑如何部署、扩容或其它一些过早优化。因此开发的重点也应该是单个功能或模块,而不是一个具有大范围功能的服务。Serverless 将开发人员从部署的麻烦中解放出来,使得他们能够专注于按照逻辑封装应用。

+

一个典型的例子是将图片上传到文件存储,此事件调用一个 Serverless 函数,这个函数创建图片的缩略图然后把该缩略图存入文件存储中,并将缩略图位置记录在 NoSQL 数据库中。数据写入 NoSQL 数据库的事件可能还会触发其他函数。这个缩略图创建函数只需按需运行,唯一的成本是调用该函数的次数。

+

和其他技术一样,Serverless 并不完美。它的缺点是应用监控和调试将会变得困难,只能依靠于服务产生的日志记录。同时,在有服务间调用事件时,可能会出现供应商锁定。并且现有的 IDE 对 Serverless 函数支持也不够友好。

+

简单 HTTP 服务示例

+

Serverless 框架 —— 可以构建由微服务组成的应用,这些微服务在响应事件时运行,并且可以自动扩容、只在运行期间收费。

+
+

下边的例子将演示如何实现一个简单的 HTTP GET 端点,调用它时会返回当前的时间。内部函数名为 currentTime,HTTP 端点为 ping

+

快速上手 Serverless

    +
  • 通过 npm 安装 serverless 程序
  • +
+
1
npm install -g serverless
+ +

我们需要新建 handler.jsserverless.yml 文件来描述和部署我们的 severless 函数。

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// handler.js

'use strict';

module.exports.endpoint = (event, context, callback) => {
const response = {
statusCode: 200,
body: JSON.stringify({
message: `Hello, the current time is ${new Date().toTimeString()}.`,
}),
};

callback(null, response);
};
+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// serverless.yml

service: serverless-simple-http-endpoint

frameworkVersion: ">=1.1.0 <2.0.0"

provider:
name: aws
runtime: nodejs8.10

functions:
currentTime:
handler: handler.endpoint
events:
- http:
path: ping
method: get
+

本地函数调用

在命令行中执行

+
1
serverless invoke local --function currentTime
+

返回结果如下:

+
1
2
3
4
{
"statusCode": 200,
"body": "{\"message\":\"Hello, the current time is 21:46:18 GMT+0800 (CST).\"}"
}
+

部署

部署应用只需执行

+
1
serverless deploy
+

在安全凭证配置正确的情况下会看到类似下边的结果:

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
Serverless: Packaging service...
Serverless: Excluding development dependencies...
Serverless: Uploading CloudFormation file to S3...
Serverless: Uploading artifacts...
Serverless: Uploading service serverless-simple-http-endpoint.zip file to S3 (331 B)...
Serverless: Validating template...
Serverless: Updating Stack...
Serverless: Checking Stack update progress...
..............................
Serverless: Stack update finished...
Service Information
service: serverless-simple-http-endpoint
stage: dev
region: us-east-1
stack: serverless-simple-http-endpoint-dev
resources: 11
api keys:
None
endpoints:
GET - https://qnye7m4dwf.execute-api.us-east-1.amazonaws.com/dev/ping
functions:
currentTime: serverless-simple-http-endpoint-dev-currentTime
layers:
None
+

使用

现在,我们可以直接调用 AWS Lambda 服务,并且可以同时获取执行日志:

+
1
2
3
4
5
6
7
8
9
10
serverless invoke --function currentTime --log

{
"statusCode": 200,
"body": "{\"message\":\"Hello, the current time is 14:03:57 GMT+0000 (UTC).\"}"
}
--------------------------------------------------------------------
START RequestId: 002cbcce-fda6-4d84-98a2-2fb19d325812 Version: $LATEST
END RequestId: 002cbcce-fda6-4d84-98a2-2fb19d325812
REPORT RequestId: 002cbcce-fda6-4d84-98a2-2fb19d325812 Duration: 0.61 ms Billed Duration: 100 ms Memory Size: 1024 MB Max Memory Used: 59 MB
+

或者使用如 curl 等工具发送一个 HTTP 请求来查看结果:

+
1
2
3
curl https://qnye7m4dwf.execute-api.us-east-1.amazonaws.com/dev/ping

{"message":"Hello, the current time is 14:03:49 GMT+0000 (UTC)."}
+

甚至可以直接用浏览器访问:

+

+ +
+ + + + + + + +
+ + + + + + +
+ +
+ + + +
+ + + + + + + + +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2019/singleton-design-pattern-in-java/1.jpeg b/2019/singleton-design-pattern-in-java/1.jpeg new file mode 100644 index 0000000000..e28b553607 Binary files /dev/null and b/2019/singleton-design-pattern-in-java/1.jpeg differ diff --git a/2019/singleton-design-pattern-in-java/index.html b/2019/singleton-design-pattern-in-java/index.html new file mode 100644 index 0000000000..3591dec610 --- /dev/null +++ b/2019/singleton-design-pattern-in-java/index.html @@ -0,0 +1,591 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Java 单例模式完整指南 | 贾攀的流水账 + + + + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

贾攀的流水账

+ +
+

Panmax's Blog

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+ + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ Java 单例模式完整指南 +

+ + +
+ + + + +
+ + +
+

设计模式一直流行于程序员之间,本文讨论被许多人认为最简单但最有争议的设计模式 —— 单例模式

+
+

+

设计模式概述

在软件工程中,设计模式描述了如何解决重复出现的设计问题,以设计出灵活、可复用的面向对象的应用程序。设计模式一共有 23 种,可以将它们分为三个不同的类别 —— 创建型、结构型和行为型。

+

创建型设计模式

创建型设计模式是处理对象创建机制的模式,试图以适合的方式创建对象。

+

对象创建的基本形式可能会导致设计问题或增加设计的复杂性。创建型设计模式通过某种方式控制对象的创建来解决此问题。

+

结构型设计模式

结构型设计模式处理类和对象的组成。这类模式使我们将对象和类组装为更大的结构,同时保持结构的高效和灵活。

+

行为型设计模式

行为型设计模式讨论对象的通信以及它们之间如何交互。

+

单例设计模式

我们对设计模式和其类型进行了概述,接下来我们重点介绍单例设计模式。

+

单例模式提供了控制程序中允许创建的实例数量的能力,同时确保程序中有一个单例的全局访问点。

+

优点

    +
  • 对单个实例的访问控制
  • +
  • 减少对象数量
  • +
  • 允许完善操作和表示
  • +
+

缺点

    +
  • 被很多程序员视为反模式
  • +
  • 在可能不需要单例的情况下被误用
  • +
+

实现

单例设计模式可以通过多种方式实现。每一种都有其自身的优点和局限性,我们可以通过以下几种方式实现单例模式:

+
    +
  • 预先初始化(Eager Initialization)
  • +
  • 静态块预初始化
  • +
  • 延迟初始化(Lazy Initialization)
  • +
  • 线程安全的延迟初始化
  • +
  • 双重检查的延迟初始化
  • +
  • 单个实例的枚举
  • +
+

实现单例

本节我们将讨论实现单例模式的各种方法。

+

预先初始化(Eager Initialization)

    +
  • 用预先初始化方法,对象在创建之前就已被初始化
  • +
  • 由于预先初始化,所以可能会出现程序并不需要的情况下初始化
  • +
  • 如果单例类很简单并且不需要太多资源,这个方法会很有用。
  • +
+
1
2
3
4
5
6
7
8
9
10
11
12
public class EagerInitialization {

private static EagerInitialization INSTANCE = new EagerInitialization();

private EagerInitialization() {
}

public static EagerInitialization getInstance() {
return INSTANCE;
}

}
+

静态块预初始化

    +
  • 在前面讨论的预先初始化方法中,没有提供任何异常处理逻辑
  • +
  • 在此实现中,对象是在静态块中创建的,因此在对象初始化时可以进行异常处理
  • +
  • 这种方法和预先初始化有同样的问题:即使程序可能不使用对象,对象也会被提前创建出来
  • +
+

延迟初始化(Lazy Initialization)

    +
  • 按需创建对象
  • +
  • 与预先初始化不同,延迟初始化会在需要时创建对象
  • +
  • 此实现不是线程安全的
  • +
+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import java.util.Objects;

public class LazyInit {

private static LazyInit INSTANCE = null;

private LazyInit() {
}

public static LazyInit getInstance() {
if (null == INSTANCE) {
synchronized (LazyInit.class) {
INSTANCE = new LazyInit();
}
}
return INSTANCE;
}
}
+

线程安全的延迟初始化

    +
  • 添加了用以处理多线程的同步方案
  • +
  • 因为每次调用都需要进行方法级同步,而过度的同步会降低性能
  • +
+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import java.util.Objects;

public class LazyInitialization {

private static LazyInitialization INSTANCE = null;

private LazyInitialization() {
}

public synchronized static LazyInitialization getInstance() {
if (null == INSTANCE) {
INSTANCE = new LazyInitialization();
}
return INSTANCE;
}
}
+

双重检查的延迟初始化

    +
  • 解决方法级同步的问题
  • +
  • 为对象可为空性执行双重检查
  • +
  • 尽管这种方法似乎可以完美的工作,但是在多线程场景下不太适用。
  • +
+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import java.util.Objects;

public class DoubleCheckSingleton {

private static DoubleCheckSingleton INSTANCE;

private DoubleCheckSingleton(){}

public static DoubleCheckSingleton getInstance() {
if(null == INSTANCE){
synchronized (DoubleCheckSingleton.class) {
if(null == INSTANCE){
INSTANCE = new DoubleCheckSingleton();
}
}
}
return INSTANCE;
}

}
+

说明一下为什么这种方法在多线程常见下可能存在问题:

+

INSTANCE = new DoubleCheckSingleton(); 这句代码,实际上可以分解成以下三个步骤:

+
    +
  1. 分配内存空间
  2. +
  3. 初始化对象
  4. +
  5. 将对象指向刚分配的内存空间
  6. +
+

但是有些编译器为了性能的原因,可能会将第二步和第三步进行重排序,顺序就成了:

+
    +
  1. 分配内存空间
  2. +
  3. 将对象指向刚分配的内存空间
  4. +
  5. 初始化对象
  6. +
+

在多线程中就会出现第二个线程判断对象不为空,但此时对象还未初始化的情况。

+

正确的双重检查

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import java.util.Objects;

public class DoubleCheckSingleton {

private volatile static DoubleCheckSingleton INSTANCE;

private DoubleCheckSingleton(){}

public static DoubleCheckSingleton getInstance() {
if(null == INSTANCE){
synchronized (DoubleCheckSingleton.class) {
if(null == INSTANCE){
INSTANCE = new DoubleCheckSingleton();
}
}
}
return INSTANCE;
}

}
+

为了解决上述问题,需要加入关键字 volatile。使用了 volatile 关键字后,重排序被禁止,所有的写(write)操作都将发生在读(read)操作之前。

+

单个实例的枚举

    +
  • 使用 Java 枚举类型创建单例
  • +
  • 此方法为处理反射、序列化和多线程场景提供了本地支持
  • +
  • 枚举类型有些不灵活
  • +
+
1
2
3
public enum Singleton {
INSTANCE;
}
+

保护单例

使单例不受反射访问的影响

在所有的单例实现中(枚举方法除外),我们通过提供私有构造函数来确保单例。但是,可以通过反射来访问私有构造函数,反射是在运行时检查或修改类的运行时行为的过程。

让我们演示如何通过反射访问单例:

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;

public class SingletonWithReflection {


public static void main(String[] args) {

EagerInitializedSingleton firstSingletonInstance = EagerInitializedSingleton.getInstance();
EagerInitializedSingleton secondSingletonInstance = null;

try{
Class<EagerInitializedSingleton> clazz = EagerInitializedSingleton.class;
Constructor<EagerInitializedSingleton> constructor = clazz.getDeclaredConstructor();
constructor.setAccessible(true);
secondSingletonInstance = constructor.newInstance();
}
catch(NoSuchMethodException | InvocationTargetException | IllegalAccessException | InstantiationException e){
e.printStackTrace();
}

System.out.println("Instance 1 hashcode: "+firstSingletonInstance.hashCode());
System.out.println("Instance 2 hashcode: "+secondSingletonInstance.hashCode());

}
}
+

上边代码输出如下:

+
1
2
Instance 1 hashcode: 21049288
Instance 2 hashcode: 24354066
+

解决:

如果单例对象已经初始化,则可以通过禁止对构造函数的访问来防止通过反射访问单例类。如果在对象初始化之后调用构造函数,可以通过抛出异常的方式来实现。

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
public final class EagerInitializedSingleton {

private static final EagerInitializedSingleton INSTANCE = new EagerInitializedSingleton();

private EagerInitializedSingleton(){
if(Objects.nonNull(INSTANCE)){
throw new RuntimeException("This class can only be access through getInstance()");
}
}

public static EagerInitializedSingleton getInstance(){
return INSTANCE;
}
}
+

使单例在序列化时安全

在分布式应用程序中,有时我们会序列化一个对象,以将对象状态保存在持久化存储中,并用以之后的检索。保存对象状态的过程称为序列化,而检索操作称为反序列化

+

如果单例没有被正确实现,那么可能出现一个单例对象有两个实例的情况。

+

让我们看看如何出现这种情况:

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;

public class SingletonWithSerialization {

public static void main(String[] args) {

EagerInitializedSingleton firstSingletonInstance = EagerInitializedSingleton.getInstance();
EagerInitializedSingleton secondSingletonInstance = null;

ObjectOutputStream outputStream = null;
ObjectInputStream inputStream = null;
try{
// 将对象状态保存到文件中
outputStream = new ObjectOutputStream(new FileOutputStream("FirstSingletonInstance.ser"));
outputStream.writeObject(firstSingletonInstance);
outputStream.close();

// 从文件中检索对象状态
inputStream = new ObjectInputStream(new FileInputStream("FirstSingletonInstance.ser"));
secondSingletonInstance = (EagerInitializedSingleton) inputStream.readObject();
inputStream.close();
}
catch(Exception e){
e.printStackTrace();
}

System.out.println("FirstSingletonInstance hashcode: "+firstSingletonInstance.hashCode());
System.out.println("SecondSingletonInstance hashcode: "+secondSingletonInstance.hashCode());
}
}
+

以上代码输出如下:

+
1
2
FirstSingletonInstance hashcode: 23090923
SecondSingletonInstance hashcode: 19586392
+

这说明现在有两个单例实例。

+
+

注意,单例类必须实现 Serializable 接口才能序列化实例。

+
+

为了避免序列化产生多个实例,我们可以在单例类中实现 readResolve() 方法。这个方法将会替换从流中读取的对象。实现代码如下:

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import java.io.Serializable;
import java.util.Objects;

public class EagerInitializedSingleton implements Serializable {

private static final long serialVersionUID = 1L;

private static final EagerInitializedSingleton INSTANCE = new EagerInitializedSingleton();

private EagerInitializedSingleton(){
if(Objects.nonNull(INSTANCE)){
throw new RuntimeException("This class can only be access through getInstance()");
}
}

public static EagerInitializedSingleton getInstance(){
return INSTANCE;
}

protected Object readResolve(){
return getInstance();
}
}
+

再次执行 SingletonWithSerialization 输出如下:

+
1
2
FirstSingletonInstance hashcode: 24336889
SecondSingletonInstance hashcode: 24336889
+

Java API 中的单例示例

Java API 中有很多类是用单例设计模式设计的:

+
1
2
3
java.lang.Runtime#getRuntime()
java.awt.Desktop#getDesktop()
java.lang.System#getSecurityManager()
+

结论

单例模式是最重要和最常用的设计模式之一。尽管很多人批评它是一种反模式,并且有很多实现时的注意事项,但在实际生活中有很多使用这种模式的示例。本文尝试介绍了常见的单例设计和与之相关的缺陷。

+ +
+ + + + + + + +
+ + + + + + +
+ +
+ + + +
+ + + + + + + + +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2019/ssh-honeypot/1.jpg b/2019/ssh-honeypot/1.jpg new file mode 100644 index 0000000000..15b9863c29 Binary files /dev/null and b/2019/ssh-honeypot/1.jpg differ diff --git a/2019/ssh-honeypot/index.html b/2019/ssh-honeypot/index.html new file mode 100644 index 0000000000..701e1ea0d9 --- /dev/null +++ b/2019/ssh-honeypot/index.html @@ -0,0 +1,524 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 通过蜜罐技术自制一个暴力破解字典库 | 贾攀的流水账 + + + + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

贾攀的流水账

+ +
+

Panmax's Blog

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+ + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ 通过蜜罐技术自制一个暴力破解字典库 +

+ + +
+ + + + +
+ + +

+
+

蜜罐指具有缺陷的,用于吸引网络的计算机病毒侵占以便用于病毒的研究和破解的计算机。

+
+

互联网就像一个黑暗丛林,当你拥有一个面向公网的服务器时,永远不知道会有多少双眼睛在盯着你。

+

基于这个信条,我相信我的几台 vps 经常受到各种 ssh 的暴力破解的骚扰,为了安全考虑我也早就把默认 ssh 端口号 22 改为了某个随机值(有些是运营商强行修改)。

+

前段时间突发奇想,是否可以监听下 22 端口,感受下在这个黑暗丛林中来自各方的打击?继而又想到,既然来感受打击,为何不把这些打击详细记录一下,假以时日,我是不是就可以得到一个「丛林常用爆破密码库」了?

+

部署

说干就干,选择了我其中一个坐落于米国的服务器来部署蜜罐程序。修改 ssh 默认端口号的步骤就不再介绍了,直奔主题。

+

为了方便,我通过 Docker 来部署,只需 1 行命令:

+
1
docker run -itd --name ssh-honeypot -p 22:22 txt3rob/docker-ssh-honey
+

这里用 Docker 启动了一个 ssh 蜜罐镜像,然后把蜜罐的 22 端口映射到本地 22 端口,验证一下 22 端口的开放情况:

+
1
2
3
# lsof -i:22
COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME
docker-pr 20832 root 4u IPv6 22035968 0t0 TCP *:ssh (LISTEN)
+

后边就坐等蜜罐来收集登录信息吧。

+

程序默认把登录日志输出到 Docker 容器的控制台中,日志格式如下:

+
1
2
3
4
5
6
7
[Thu Nov  7 17:59:00 2019] 187.189.55.192 root 123456
[Thu Nov 7 17:59:00 2019] 187.189.55.192 root password
[Thu Nov 7 17:59:00 2019] 187.189.55.192 root default
[Thu Nov 7 17:59:01 2019] 187.189.55.192 root root
[Thu Nov 7 17:59:01 2019] 187.189.55.192 root 000000
[Thu Nov 7 17:59:01 2019] 187.189.55.192 root 111111
[Thu Nov 7 17:59:01 2019] 187.189.55.192 root password
+

我们可以通过管道(|) + 重定向(>)的方式把结果导出出来:

+
1
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 的行)。

+

拿到数据后需要再进行一下简单的清洗:

去掉行首行尾的空格

1
2
sed -i 's/^[ \t]*//g' ssh_password.log
sed -i 's/[ \t]*$//g' ssh_password.log

+

去除空行

+
1
sed -i '/^$/d' ssh_password.log
+

去掉数据结尾的 ^M

+
1
dos2unix -f ssh_password.log
+

收网

运行三周后,我一共收到了 1340万+ 的用户名密码(看到结果后有些震惊),去重后(cat ssh_password.log | awk '{print $2$3}' | uniq -c | wc -l)也有 1290万,之后我对这些数据进行了统计。

+

统计常用用户名的 top5:

1
2
3
4
5
6
# 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
admin:7179731
root:6236949
test:1062
guest:814
mysql:799
+

统计常用密码的 top5:

1
2
3
4
5
6
# 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
admin:727668
12345:726670
1234:726205
password:725527
default:724872
+

统计常用用户名密码组合的 top5:

1
2
3
4
5
6
# 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
admin_admin:478249
admin_1234:478020
admin_pfsense:477780
admin_12345:477733
admin_admin1234:477222
+

数据很有意思,也确实是我们最常用的那些用户名密码。

+

关于安全

简单通过修改 ssh 端口号的方式也不是最为稳妥的方法,真正安全的做法应该是:

+
    +
  1. 配置 ssh 密钥
  2. +
  3. 禁用密码登录
  4. +
  5. 配置 ssh ip 登录白名单
  6. +
+

最后

我把我的蜜罐中采到的蜂蜜分享出来,下载链接:http://developer.jpanj.com/ssh_password.log

+

大家一起来享用这份喜悦吧。

+ +
+ + + + + + + +
+ + + + + + +
+ +
+ + + +
+ + + + + + + + +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2019/stop-using-post-increment-in-your-loops/1.jpg b/2019/stop-using-post-increment-in-your-loops/1.jpg new file mode 100644 index 0000000000..327e6df457 Binary files /dev/null and b/2019/stop-using-post-increment-in-your-loops/1.jpg differ diff --git a/2019/stop-using-post-increment-in-your-loops/index.html b/2019/stop-using-post-increment-in-your-loops/index.html new file mode 100644 index 0000000000..466c960204 --- /dev/null +++ b/2019/stop-using-post-increment-in-your-loops/index.html @@ -0,0 +1,509 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 停止在你的循环中使用 i++ | 贾攀的流水账 + + + + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

贾攀的流水账

+ +
+

Panmax's Blog

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+ + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ 停止在你的循环中使用 i++ +

+ + +
+ + + + +
+ + +
+

为什么 ++i 通常比 i++ 更好?

+
+

+

介绍

如果你之前写过 for 循环,那么你一定使用过 i++ 来增加你的循环变量。

+

然而,你是否考虑过为什么要选择这种做法呢?

+

我们在执行完 i++ 后,i 的值会比它先前大 1,这是我们想要的结果。与此同时还有很多方法可以做到,比如:++i 甚至 i = i + 1

+

接下来,我会对比介绍两种实现变量加 1 的方法:++ii++,并解释为什么大多数情况下 ++i 可能好于 i++

+

后递增(i++)

i++ 方法(或者叫后递增)是最常见的使用方式。

+

在伪代码中,后递增操作符对变量 i 的操作大致如下:

+
1
2
3
int j = i;
i = i + 1;
return j;
+

由于后递增需要返回 i 的原值而不是返回 i + 1 后的增量值,所以需要将 i 的旧值进行存储。

+

这意味着 i++ 需要额外的内存来存储这个值,但这是不必要的。因为在大多数情况下,我们并不会使用 i 的旧值,而是直接将其丢弃。

+

前递增(++i)

++i 方法(或者叫前递增)比较少见,通常是使用 CC++ 的老程序员在用。

+

在伪代码中,前递增操作符对变量 i 的操作大致如下:

+
1
2
i = i + 1;
return i;
+

需要注意的是,在前递增中,我们不必保存 i 的旧值,我们只需简单的对它加 1 并返回。这与 for 循环中的经典用例更加匹配:正如上文所说,我们很少需要 i 的旧值。

+

说明

看过后递增和前递增之间的区别后,你可能会想到:由于 i 的旧值在后递增中未被使用,因此在编译阶段,编译器将会优化掉这一行,使两个操作符等价。

+

对于基本类型来说(如整形)确实如此。

+

但是对于复杂类型,例如(在 C++ 中)用户自定义类型或带有 + 操作重载的迭代器,编译器就无法对此进行优化了。

+

所以如果说在你用不到所要递增变量旧值的情况下,使用前递增运算符要好过(或等价于)后递增。

+ +
+ + + + + + + +
+ + + + + + +
+ +
+ + + +
+ + + + + + + + +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2019/story-https/1.png b/2019/story-https/1.png new file mode 100644 index 0000000000..2677d6de6f Binary files /dev/null and b/2019/story-https/1.png differ diff --git a/2019/story-https/1.tiff b/2019/story-https/1.tiff new file mode 100644 index 0000000000..cc0250efe0 Binary files /dev/null and b/2019/story-https/1.tiff differ diff --git a/2019/story-https/2.png b/2019/story-https/2.png new file mode 100644 index 0000000000..9cb7d5ed90 Binary files /dev/null and b/2019/story-https/2.png differ diff --git a/2019/story-https/2.tiff b/2019/story-https/2.tiff new file mode 100644 index 0000000000..4cda1a1dba Binary files /dev/null and b/2019/story-https/2.tiff differ diff --git a/2019/story-https/3.png b/2019/story-https/3.png new file mode 100644 index 0000000000..c7b3be57d0 Binary files /dev/null and b/2019/story-https/3.png differ diff --git a/2019/story-https/3.tiff b/2019/story-https/3.tiff new file mode 100644 index 0000000000..e613258a8f Binary files /dev/null and b/2019/story-https/3.tiff differ diff --git a/2019/story-https/4.png b/2019/story-https/4.png new file mode 100644 index 0000000000..7611eae9e9 Binary files /dev/null and b/2019/story-https/4.png differ diff --git a/2019/story-https/4.tiff b/2019/story-https/4.tiff new file mode 100644 index 0000000000..a385df896c Binary files /dev/null and b/2019/story-https/4.tiff differ diff --git a/2019/story-https/5.png b/2019/story-https/5.png new file mode 100644 index 0000000000..234c029927 Binary files /dev/null and b/2019/story-https/5.png differ diff --git a/2019/story-https/5.tiff b/2019/story-https/5.tiff new file mode 100644 index 0000000000..eb3f77ec4f Binary files /dev/null and b/2019/story-https/5.tiff differ diff --git a/2019/story-https/6.png b/2019/story-https/6.png new file mode 100644 index 0000000000..079e708ad7 Binary files /dev/null and b/2019/story-https/6.png differ diff --git a/2019/story-https/index.html b/2019/story-https/index.html new file mode 100644 index 0000000000..3ce9e2461a --- /dev/null +++ b/2019/story-https/index.html @@ -0,0 +1,556 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 大p故事会002:关于 https 那些事 | 贾攀的流水账 + + + + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

贾攀的流水账

+ +
+

Panmax's Blog

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+ + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ 大p故事会002:关于 https 那些事 +

+ + +
+ + + + +
+ + +

大p在刚刚开始追求小h的时候,正值风华正茂,所以比较喜欢写写书信什么的来交流彼此。由于大p的脸皮比较薄,每次写完信后,并不是把信直接交给小h,而且大部分传信的时间是在上课的时候,所以大p会把信交个离她比较近的一个同学,再由这个同学交给离她更近的同学,最后一步步的传到小h手中,小h写完回信后再使用这个过程传过来。

+

+

使用这种方式虽然浪漫,但有几个问题让大p和小h很困扰:

+
    +
  1. 每次传递过程中都会有好奇心比较旺盛的同学先打开信看一遍然后才继续往下传
  2. +
  3. 遇到爱搞恶作剧的同学还会修改信的内容,比如有一次我告诉小h,今晚9点操场见,但是这个同学改成了今晚7点操场见,结果导致小h提前两小时到了操场,因为这事俩人差点分手
  4. +
  5. 甚至有更讨厌的同学,冒充大p给小h写信
  6. +
+
+

以上三中情况对应的是 HTTP 协议传输时存在的风险:
窃听风险:第三方节点可以获知通信内容
篡改风险:第三方节点可以修改通信内容
冒充风险:第三方节点可以冒充他人身份参与通信

+
+

大p想到了一个对策,把信放在一个带有密码锁的盒子里,这样就由之前的直接传递小纸条改为了传递有密码锁的盒子。示意图如下:

+

+

现在对策有了,带密码锁的盒子有了,信也写好了,但另一个问题来了,密码怎么告诉小h呢,最简单的办法肯定是大p直接把密码告诉小h,但他们两个平时单独约会的时间非常少,每次独处时大p都把心思用在其他地方了,把给密码这件事忘的一干二净。另一个方法是把写有密码的信还通过之前的方法传给小h,但是这样的话中间那个传信的同学就有可能打开信看到密码,把信放在密码盒中就没有任何意义了。

+

因为锁子的密码安全传递问题解决不了,大p暂时否定了这个方案。

+
+

以上介绍的是对称加密算法,带有密码锁的盒子和密码分别对应的算法和密钥,常见的对称加密算法有AES、DES、3DES、RC5、RC6

+
+

因为担心信中的内容被其他人看到,两个人之间的信件交流就越来越少了,这让大p很苦恼。有一天大p在学校小卖部里看到了一种新的密码锁,这种密码锁神奇之处在于它需要配合一对密码来使用,由一个密码锁上的锁头必须由另一个密码才能解开,反之亦然。

+

大p立刻来了精神,买了把这样的锁回去,并且生成了两对密码。大p和小h协商好,给每对密码中的每个密码分别起个名字:公钥、私钥,公钥表示这个密码能够随意分发,让任何人得到:可以直接把写有公钥的纸条传给对方,甚至把公钥直接写在黑板上都没有问题,但是私钥只能自己知道,甚至连对方都不能告诉。

+

也就是说此时他们两个每人有一个属于自己的私钥。这样只需要每次写完信后,用对方的的公钥把盒子锁上,对方拿到后用自己的私钥解开盒子取出信件,写完回信后再用另一方的公钥锁上盒子即可。

+

假如

+
    +
  • 大p的公钥是abc,私钥是cba;
  • +
  • 小h的公钥是123,私钥是321,流程示意图如下:
  • +
+

+
+

以上介绍的是非对称加密也叫公钥加密,这套密码算法包含配对的密钥对,分为加密密钥和解密密钥。发送者用加密密钥进行加密,接收者用解密密钥进行解密。加密密钥是公开的,任何人都可以获取,因此加密密钥又称为公钥(public key),解密密钥不能公开,只能自己使用,因此它又称为私钥(private key),常见的公钥加密算法有 RSA

+
+

于是两个人又开始频繁的给对方写信了,但是在慢慢使用中,他们两个都人发现了一个问题,这个密码锁的加密和解密的效率很低,简直就是写信5分钟,加/解密2小时。

+

后来,大p想到一个方法,他们可以结合两种密码锁,先通过非对称密码锁把之前对称密码锁的密码传给对方,两个人后边直接用对称密码锁来加密解密就可以了。

+

为了后边不再出什么漏洞,大p决定对这种方案进行了严格的推敲,推敲过程中他突然意识到一种可怕的情况,虽然他们两个都持有对方的公钥,但他们自己并不知道自己拿到的是不是真的就是对方的公钥,假如中间传信的人里有一个既邪恶又聪明的同学小x,他可能就会想到一种破解方法:

+
    +
  • 小x手中有两对他自己生成好的密钥:
      +
    • 第一对:公钥xyz,私钥zyx
    • +
    • 第二对:公钥456,私钥654
    • +
    +
  • +
+

当大p和小h想要获取对方的公钥时,小x拿到大p的公钥abc后记下来,但是小x却告诉小h:大p的公钥是 xyz(这是小x的公钥),反过来也是,小h的公钥也被小x拿到并且掉了包,打p拿到的也是小x生成的公钥456。

+

当大p写完信后用他认为是小h的公钥加密时,实际用的是小x的公钥,小x只需拿到加密的信后用自己的私钥解开看一看,可能再改改信的内容,然后再用小h的公钥把信加密后交给小h,反过来同理。

+

+

因为大p是个阴谋论者,所以他相信这样的事情一定是存在的,所以之前所有的加密方案瞬间都因为这种可能有中间人攻击的存在而崩塌了。

+

由于现在市面上的密码锁只有这两种,而且大p还在读高中,所以造一种可以防中间人攻击的新型加密锁对他来说难度太大了(真实情况是大p毕业参加工作后依然造不出来),大p决定找到一种方案可以让他和小h拿到的一定是对方的公钥,而不是中间人的。大p想到,既然我们可能会收到中间人的攻击,那么我们能不能也找个可靠的「中间人」来解决这个问题呢。

+

找班长来做这个「中间人」最合适不过了,为了防止再出其他幺蛾子,大p和班长进行了面基,班长也有一对自己的密钥,大p让班长当面把公钥给了大p,此时大p可以确定他拿到的班长的公钥一定就是班长的公钥(这个实际是根证书预装进操作系统或浏览器的过程)。

+

小h为了让大p(也包括其他追求者)拿到不被篡改的公钥,需要把自己的公钥交给班长,她虽然和班长没有进行面交,但班长经过一系列严格而且复杂的检查确认了这个公钥确确实实是小h的,然后班长会把小h的基本信息(比如姓名、学号)和小h的公钥放在一起,然后对以上内容做一次散列计算后得到一个信息摘要(也叫指纹),这个指纹可以保证只要班长拿到的小h的基本信息或小h的公钥有任何修改,再次散列计算后得到的指纹一定不同。

+
+

消息摘要(message digest)函数是一种用于判断数据完整性的算法,也称为散列函数或哈希函数,函数返回的值叫散列值,散列值又称为消息摘要或者指纹(fingerprint)。这种算法是一个不可逆的算法,因此你没法通过消息摘要反向推倒出消息是什么,所以它也称为单向散列函数。常用的散列算法有MD5、SHA。

+
+

再之后班长使用自己的私钥把之前计算出来的信息摘要进行了签名(实际就是用私钥对这个值进行了加密),加密后的值我们叫它数字签名,最后把数字签名和原始信息一起打包,生成了最终的数字证书,也就是说数字证书中有两块内容,一块是小h的公钥+小h基本信息组成的明文,另外一块是把明文部分进行散列计算后的值再次通过私钥加密后得到的数字签名。

+

+

之后班长把带有自己签名的证书(数字证书)交给了小h。大p找小h索要公钥时,小h只需要把这个数字证书交给他就行了,大p需要用相同的散列算法将明文部分进行计算得到一个散列值a,并且因为大p确定自己手中拿的班长的公钥是可信的,于是大p用班长的公钥对证书中的数字签名进行解密得到得到班长计算出的散列值b,散列值a和散列值b进行比对,如果相同就可以确定明文部分是没有被篡改过的,也就是说此时大p可以相信自己拿到的小h的公钥一定是可靠的了。

+

这也印证了一句名言:“一切计算机问题都可以通过添加中间层解决”。

+
+

上边的部分班长就是认证机构(CA),CA 把用户的姓名、组织、邮箱地址等个人信息收集起来,加上公钥,由 CA 提供数字签名生成公钥证书(Public-Key Certificate)PKC

+
+

至此,一套比较完善的数据传输方案就完成了。HTTPS(SSL/TLS)就是在这样一套流程基础之上建立起来的。

+

https 简化流程图如下:

+

+ +
+ + + + + + + +
+ + + + + + +
+ +
+ + + +
+ + + + + + + + +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2019/story-sync-async-blocking/index.html b/2019/story-sync-async-blocking/index.html new file mode 100644 index 0000000000..dde03ed70c --- /dev/null +++ b/2019/story-sync-async-blocking/index.html @@ -0,0 +1,515 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 大p故事会001:同步、异步、阻塞、非阻塞那些事 | 贾攀的流水账 + + + + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

贾攀的流水账

+ +
+

Panmax's Blog

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+ + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ 大p故事会001:同步、异步、阻塞、非阻塞那些事 +

+ + +
+ + + + +
+ + +

高中生大p就读于一所很一般的学校,大p所在的班里有一个来自s市的女孩小h。

+

小h娇小活泼可爱而且很个性,很受同学们的欢迎,小h不知道的是大p也已经关注她很久了,虽然两个人都是寄宿生,但他们两个都偷偷带了手机到学校来,大p从其他同学那里问来了小h的手机号码,大p鼓起勇气拨通了小h的电话,还没等小h接听,大p就退缩了,挂了电话。那时的手机对学生来说还很新鲜,小h 看到这个未接电话,不知道是谁打来的,处于好玩就给这个陌生号码回了过来,但大p 却没有勇气接听,任由手机振动着。当时很流行彩铃,大p 也不例外地设置了彩铃,用的是张震岳的《思念是一种病》,这首歌刚好是当时小h最喜欢的音乐,于是后来小h在想听这首歌的时候就会给这个号码打电话。

+

高二那年下了一场暴雪,导致全市所有设施瘫痪,于是学校放假了,刚好那天学校期中考试,也是在这一天大p告诉了小h那个手机号是他的。晚上回家后大p假装很不在乎地发短信问了小h这次考的怎么样,小h很客套的应付了几句,但大p这天异常兴奋,他隐约感受到如果再不做点什么可能就会永远错过了,于是他寻找各种话题和小h聊天,不知不觉从晚上10点聊到了早上5点…

+

这之后小h也明白了大p的用意,大p在1个月后向小h表白了,但遭到了小h的拒绝,小h告诉大p学业要紧,等高中毕业后再考虑。

+

转眼间高二下学期快要结束了,这一天小h把大p叫到一个没有人的地方,跟大p说:「我答应你之前那件事了,你也答应我,等我回来好吗?」,大p不知道她在说什么,他只听到小h说她同意了,现在的大p 不管什么事情都会答应的,后来大p才知道,小h 是要回s市读高三,然后要在s市参加高考。

+

高三,小h去了s市,他们两个每天通过手机短信的方式进行交流(当然是偷偷的),刚开始的时候,大p总是心心念地等着短信回复,每次给小h发过去短信后就茶不思饭不想题也看不进去,只能两眼直勾勾地盯着手机等着短信回来,因为手机是静音所以要想第一时间知道短信到了只能盯着看屏幕有没有亮,这样过了一段时间,大p的成绩一落千丈,因为他有太多的时间花在了等着短信回复上。

+

大p认为这样下去也不是办法于是调整了一下自己的心态,每次发完短信后不再一直盯着手机等回复了,而是用这个时间去看书、做题,每过一段时间就查看一下有没有短信过来,虽然大p的成绩慢慢爬了上来,但在这种状态下大p的心里还是需要一直惦记着手机,因为他不知道短信什么时候过来所以要时不时的去看一眼。

+

于是大p就想有没有什么办法可以让自己不用频繁去看手机,又能在第一时间知道有短信来了呢,大p想了三天三夜想到了一个主意:用手机的振动功能,打开短信的振动提醒,可以把手机放在裤兜里,每次短信来了可以立刻感受到而且不会被老师发现。大p 像平常一样给小h发了条短信,为了验证这个振动功能的有效性,大p这次还是盯着手机等回复,直到短信回来手机震了,大p确信了方案的可行性。

+

此后,大p的学习效率更高了,也可以安心的听课做题了,大p 只需要在手机振动的时候去看短信就行。大p很开心,就这样,高三的时光一闪而过,他们两个约定好要考同一所大学。

+

预知后事如何,请听下回分解。

+

上边大p用到了4种方案来处理短信这件事:

    +
  • 同步阻塞(傻傻的等短信过来)
  • +
  • 同步非阻塞(大p写会作业,检查下手机有没有新短信,这样交替轮询)
  • +
  • 异步阻塞(手机开启振动模式,但大p 还是盯着看。用这种方案的大p很傻,所以大p只尝试了一次)
  • +
  • 异步非阻塞(大p只管学习,手机一震大p就知道短信来了)
  • +
+
+

阻塞非阻塞都是相对于大p来说的,取决于大p等待短信时的状态。全心投入等短信达到的大p是阻塞的,可以抽出时间来做其他事情的大p是非阻塞的。

+
+
+

而同步和异步是对手机来说的,同步需要让大p自己去检查有没有新短信达到,而异步(也就是手机开启振动模式)可以主动告诉大p有短信来了。

+
+ +
+ + + + + + + +
+ + + + + + +
+ +
+ + + +
+ + + + + + + + +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2019/telegram-bot-send-taily-message/10.png b/2019/telegram-bot-send-taily-message/10.png new file mode 100644 index 0000000000..80c682915c Binary files /dev/null and b/2019/telegram-bot-send-taily-message/10.png differ diff --git a/2019/telegram-bot-send-taily-message/6.png b/2019/telegram-bot-send-taily-message/6.png new file mode 100644 index 0000000000..33d6e9a182 Binary files /dev/null and b/2019/telegram-bot-send-taily-message/6.png differ diff --git a/2019/telegram-bot-send-taily-message/7.jpg b/2019/telegram-bot-send-taily-message/7.jpg new file mode 100644 index 0000000000..424ae44582 Binary files /dev/null and b/2019/telegram-bot-send-taily-message/7.jpg differ diff --git a/2019/telegram-bot-send-taily-message/8.png b/2019/telegram-bot-send-taily-message/8.png new file mode 100644 index 0000000000..dd6ade8e52 Binary files /dev/null and b/2019/telegram-bot-send-taily-message/8.png differ diff --git a/2019/telegram-bot-send-taily-message/9.png b/2019/telegram-bot-send-taily-message/9.png new file mode 100644 index 0000000000..0cc84fa9d0 Binary files /dev/null and b/2019/telegram-bot-send-taily-message/9.png differ diff --git a/2019/telegram-bot-send-taily-message/head.png b/2019/telegram-bot-send-taily-message/head.png new file mode 100644 index 0000000000..2504b9fa01 Binary files /dev/null and b/2019/telegram-bot-send-taily-message/head.png differ diff --git a/2019/telegram-bot-send-taily-message/index.html b/2019/telegram-bot-send-taily-message/index.html new file mode 100644 index 0000000000..b2d954e0e8 --- /dev/null +++ b/2019/telegram-bot-send-taily-message/index.html @@ -0,0 +1,563 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 不到 40 行代码实现 Telegram 自动发消息机器人 | 贾攀的流水账 + + + + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

贾攀的流水账

+ +
+

Panmax's Blog

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+ + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ 不到 40 行代码实现 Telegram 自动发消息机器人 +

+ + +
+ + + + +
+ + +

+

创建一个 Telegram 机器人,定时发送消息,并部署到 AWS Lambda。

+
+

AWS Lambda 是一项计算服务,可使你无需预配置或管理服务器即可运行代码。

+
+

我们要做什么?

    +
  • 创建一个 Telegram 机器人
  • +
  • 自动发送日常信息
  • +
  • 把它部署到 AWS Lambda
  • +
+

需要准备什么?

    +
  • 一个 Telegram 帐号
  • +
  • Python 3.6
  • +
  • Node.js
  • +
  • 一个 AWS 帐号
  • +
+

AWS Lambda 可以在一定配额内免费使用,所以需要避免发送大量请求。

+

AWS Lambda 定价方案如下:

+

+

创建机器人

待办清单上的第一件事是创建一个机器人,遵循 Telegram 官方说明:

+
    +
  • 在 Teletram 中搜索用户 @BotFather
  • +
  • 发送命令 /newbot 并为你的机器人指定 nameusername
  • +
  • 拿到 token 并记录在一个安全的地方,后边会用到。
  • +
+

现在机器人准备好了,开始编写代码。

+

准备部署设施

有很多部署 Lambda 的方法,我准备使用 serverless 框架,所以我们先来安装它:

+
1
$ npm install serverless --global
+

Serverless 的文档中提供了一些范例,还支持生成模板,像下边这样:

+
1
$ serverless create --template aws-python3 --path scheduled_telegram_bot
+

执行这个命令后,会创建出一个 scheduled_telegram_bot 目录,并已经生成了 3 个文件:
.gitignoreserverless.ymlhandler.py

+

serverless.yml 文件用来描述:部署什么、何时运行、如何运行。 handler.py 文件包含将要运行的代码,所以我们先来编写它。

+

编写代码

我们将使用是一个封装好的包来调用 Telegram 的 API:python-teletram-bot,创建一个新的文件 requirements.txt 写入:

+
1
python-telegram-bot==12.2.0
+

我们需要在程序中导入这个库,不过后边我们会遇到一个问题:由于 python-telegram-bot 不是 AWS Lambda 所提供的标准库,因此我们在部署时需要同时包含这个包中的文件。

+

所以我们后边会在本地安装这个包的所有内容。

+
1
pip install requirements.txt --target=.
+

现在让我们来定义一个发送消息的函数,打开 handler.py 修改内容如下:

+
1
2
3
4
5
6
7
8
9
10
import telegram
import os

TOKEN = os.environ['TELEGRAM_TOKEN']
CHAT_ID = 000000 # Change this


def send_message(event, context):
bot = telegram.Bot(token=TOKEN)
bot.sendMessage(chat_id=CHAT_ID, text='Hey there!')
+

你需要把 CHAT_ID 修改为你想让机器人互动的群组、频道或者会话的 ID。获取 ID 的方法如下,我以频道为例:

+

首先创建自己的频道,将机器人拉入频道并设置为管理员,随意在频道内发送一个消息。

+

添加 GetIDsBot,并将上边发的那条消息转发给这个机器人,它会返回这个频道相关信息:

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
👤 You
├ id: xxxxxxx
├ is_bot: false
├ first_name: xxxxx
├ username: xxxxxx
├ language_code: zh-hans (-)
└ created: ~ 9/2017 (?)

💬 Origin chat
├ id: -1001156324531
├ title: Panmax Channel
└ type: channel

📃 Message
├ message_id: 82
└ forward_date: Fri, 15 Nov 2019 15:38:41 GMT
+

这样可以得到我所在这个频道的 ID 为 -1001156324531

+

部署定义

现在我们来定义如何运行我们的代码。

+

编辑 serverless.yml

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
service: scheduled-telegarm-bot

frameworkVersion: ">=1.2.0 <2.0.0"

provider:
name: aws
runtime: python3.6
environment:
TELEGRAM_TOKEN: ${env:TELEGRAM_TOKEN}

functions:
cron:
handler: handler.send_message
events:
- schedule: cron(*/2 * * * ? *)
+

这里我们告诉了 AWS 我们所需要的运行环境,并且让它从我们的环境变量中获取 Telegram token,这样我们就不需要把 token 硬编码到代码中了。

+

最后我们还定义了一个定时器,声明每两分钟触发一次这个函数。当然,定时器有很多选项,通过这个文档可以了解更多配置方式,比如每小时或者每周一发送消息。

+

汇总

我们已经准备好了所有需要的东西。

+

好吧,准确来说是几乎所有的东西。我们还需要获取 AWS 的凭证,然后和 token 一样,在部署前设置为环境变量,获取凭证步骤如下:

+

通过 AWS 的控制台:

+

进入 我的安全凭证 - 用户 - 添加用户

+

+

设置用户名并选择编程访问

+

+

下一步:选择直接附加现有策略 - AdministratorAccess

+

+

下一步会来到添加标签页,直接点击下一步,确认信息无误后点击 创建用户,将 访问密钥 ID私有访问密钥 拷贝并存放起来。

+

现在,让我们把 AWS 凭证和 Telegram token 导出到环境变量。打开终端,输入:

+
1
2
3
$ export AWS_ACCESS_KEY_ID=[your key goes here]
$ export AWS_SECRET_ACCESS_KEY=[your key goes here]
$ export TELEGRAM_TOKEN=[your token goes here]
+

在本地安装 Python 的依赖包(这也是 AWS Lambda 所需要的):

+
1
$ pip3 install -r requirements.txt -t .
+

最后将所有的东西部署到 AWS:

+
1
$ serverless deploy
+

如果前边配置没有问题,会看到如下输出:

+
1
2
3
4
5
6
7
8
9
10
Serverless: Packaging service...
Serverless: Excluding development dependencies...
Serverless: Uploading CloudFormation file to S3...
Serverless: Uploading artifacts...
Serverless: Uploading service scheduled-telegarm-bot.zip file to S3 (5.64 MB)...
Serverless: Validating template...
Serverless: Updating Stack...
Serverless: Checking Stack update progress...
.........
Serverless: Stack update finished...
+

完成!机器人会在每 2 分钟给我们发送一次消息。

+

+

参考

AWS Python Scheduled Cron Example:https://github.com/serverless/examples/tree/master/aws-python-scheduled-cron

+ +
+ + + + + + + +
+ + + + + + +
+ +
+ + + +
+ + + + + + + + +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2019/top-5-spring-annotation/1.jpeg b/2019/top-5-spring-annotation/1.jpeg new file mode 100644 index 0000000000..6642f0e489 Binary files /dev/null and b/2019/top-5-spring-annotation/1.jpeg differ diff --git a/2019/top-5-spring-annotation/index.html b/2019/top-5-spring-annotation/index.html new file mode 100644 index 0000000000..45646cb24b --- /dev/null +++ b/2019/top-5-spring-annotation/index.html @@ -0,0 +1,519 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Spring 中最常用的 5 个注解 | 贾攀的流水账 + + + + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

贾攀的流水账

+ +
+

Panmax's Blog

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+ + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ Spring 中最常用的 5 个注解 +

+ + +
+ + + + +
+ + +
+

Java 中注解的引入改变了 Java 开发人员配置应用的方式。注解在 Java 的 1.5 版本中引入进来,它使开发人员能够在代码中维护配置而不必依赖于外部配置文件。

+
+

+

注解是可以添加到 Java 类、方法、变量、参数或者包中的一种语法元数据。

+

Spring 框架推荐开发人员通过使用它提供的大量内置注解来配置应用。在这篇文章中,我们重点介绍 Spring Core 框架中最常用的几个注解。

+

1. @Autowired 注解

这个注解用于声明类中的依赖项。基于这个注解,Spring DI 框架可以注入对应的依赖。@Autowired 可以用在构造函数、属性和 setter 方法上。它是 JSR-330(Java 依赖注入)@Inject 注解的替代方法。

+

属性注入

下面的代码演示了如何将属性作为依赖项注入:

+
1
2
3
4
5
6
7
8
import org.springframework.beans.factory.annotation.Autowired;

public class UserController {

@Autowired
private UserRepository userRepository;

}
+

Setter 方法注入

也可以通过 setter 方法完成依赖注入,如下所示:

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import org.springframework.beans.factory.annotation.Autowired;

public class UserController {

private UserRepository userRepository;

public UserRepository getUserRepository() {
return userRepository;
}

@Autowired
public void setUserRepository(UserRepository userRepository) {
this.userRepository = userRepository;
}


}
+

构造函数注入

还可以在构造函数上使用:

+
1
2
3
4
5
6
7
8
9
10
11
12
import org.springframework.beans.factory.annotation.Autowired;

public class UserController {

private UserRepository userRepository;

@Autowired
public UserController(UserRepository userRepository) {
this.userRepository = userRepository;
}

}
+

2. @Bean 注解

这个注解应用在方法上,并生成由 Spring 管理的 bean。Spring 配置类通常包含 bean 声明。通常,应用的 POJO 部分被声明为 Spring 组件,并且 Spring 提供的组件扫描机制会在 Spring IoC 容器中自动创建 bean。但是,当 POJO 的代码不可用并且我们需要创建 Sprint 管理的 bean 时,@bean 注解就会非常有用。

+

以下代码演示了 @Bean 注解的用法:

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class SampleConfiguration {

@Bean
public User user(){
return new User();
}

@Bean(name = "admin", initMethod = "", destroyMethod = "")
public Admin admin(){
return new Admin();
}
}
+

此外,这个注解还提供给了一些属性用以管理 bean 配置,例如:名称、初始化方法、销毁方法等。

+

3. @Value 注解

Spring 的 @Value 注解非常有用,可以方便地使用 Spring 表达式语言(Spring Expression Language,SpEL)提供默认值或控制变量的值。

+

默认值

在下边的示例中,name 变量配置了 @Value 注解。如果实例化 User 类时没有为 name 提供值,就会使用 @Value 配置的默认值 default-user

+
1
2
3
4
5
6
7
import org.springframework.beans.factory.annotation.Value;

public class User {

@Value("default-user")
private String name;
}
+

从环境中读取属性

下面的示例演示如何从环境中读取一个值并赋给变量:

+
1
2
3
4
5
public class User {

@Value("${NAME}")
private String name;
}
+

使用 Spring 表达式语言

下边的示例演示如何使用 Spring 表达式语言来获取值并赋值给变量。注意这里使用了 # 来替代之前使用的 $

+
1
2
3
4
5
public class User {

@Value("#{systemProperties['user.name']}")
private String name;
}
+

4. @Profile 注解

在开发的生命周期中,应用会经历多个环境和阶段。比如,devtestuat(预发布)、industry(生产)等。根据不同的环境和阶段,需要有不同的配置。在这种情况下,@Profile 注解非常方便,它使开发人员可以灵活地控制应该激活的组件。

+
1
2
3
4
5
6
7
8
@Profile("dev")
public class DevDataSource {
}

@Profile("test")
public class QADataSource {

}
+

在上边的示例中,我们提供了两个配置,一个用于 dev 环境,另一个用于 test 环境。根据环境类型,我们可以提供不同的配置,Spring 将会确保加载与之对应的配置。

+

5. @Import 注解

@Import 注解使我们可以将一个或多个组件的配置导入到另一个配置类中。

+
1
2
3
4
5
6
7
8
9
@Configuration
@Import(SampleConfiguration.class)
public class AnotherConfiguration {

@Bean
public Product product(){
return new Product();
}
}
+

在上边的配置类中,我们导入了另一个配置类中定义的配置。

+

总结

本文我们演示了在 Spring 应用开发中使用最频繁的一些注解。尽管有大量的 Spring 注解,但那些注解在大多数 Spring 应用中用到的不多。

+ +
+ + + + + + + +
+ + + + + + +
+ +
+ + + +
+ + + + + + + + +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2019/web-benchmark-for-python-java-golang/1.png b/2019/web-benchmark-for-python-java-golang/1.png new file mode 100644 index 0000000000..cb064a186f Binary files /dev/null and b/2019/web-benchmark-for-python-java-golang/1.png differ diff --git a/2019/web-benchmark-for-python-java-golang/index.html b/2019/web-benchmark-for-python-java-golang/index.html new file mode 100644 index 0000000000..d226c1eee2 --- /dev/null +++ b/2019/web-benchmark-for-python-java-golang/index.html @@ -0,0 +1,534 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Python、Java、GoLang 基于 Web 的性能测试 | 贾攀的流水账 + + + + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

贾攀的流水账

+ +
+

Panmax's Blog

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+ + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ Python、Java、GoLang 基于 Web 的性能测试 +

+ + +
+ + + + +
+ + +

最近一段时间学习了一下 Go 这门语言,其中提到最多的就是 GoLang 的高性能 & 高并发,所以本着没有对比就没有伤害的原则,我准备将其与另外两个我所掌握的语言(Python、Java)进行一个简单的性能对比。

+

测试环境

我的 MacBook Pro,12个逻辑CPU + 16G内存

+

测试工具

https://github.com/wg/wrk

+

wrk -t8 -c100 -d30s --latency http://www.baidu.com

+

模拟8线程、100个并发,持续30秒的性能测试

+

实现

+

以下程序完整源码已放在 GitHub:https://github.com/Panmax/web-benchmark

+
+

Python

框架:Flask
容器:Gunicorn
运行环境:Docker

+

核心代码:

1
2
3
4
5
6
7
8
9
10
11
# -*- coding: utf-8 -*-

from flask import Flask
app = Flask(__name__)

@app.route("/")
def hello():
return "Hello Python!"

if __name__ == '__main__':
app.run()
+

Dockerfile

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
FROM ubuntu:14.04

ADD sources.list /etc/apt/sources.list
ADD pip.conf ~/.pip/pip.conf

# Update OS
# RUN sed -i 's/# \(.*multiverse$\)/\1/g' /etc/apt/sources.list
RUN apt-get update
RUN apt-get -y upgrade

# Install Python
RUN apt-get install -y python-dev python-pip

# Add requirements.txt
ADD requirements.txt /webapp/requirements.txt

# Install gunicorn Python web server
RUN pip install gunicorn==19.6.0
# Install app requirements
RUN pip install -r /webapp/requirements.txt

# Create app directory
ADD . /webapp

# Set the default directory for our environment
ENV HOME /webapp
WORKDIR /webapp

# Expose port 5000 for gunicorn
EXPOSE 5000

ENTRYPOINT ["gunicorn", "-w", "24", "wsgi:app", "-b", "0.0.0.0:5000", "-n", "docker-flask", "--timeout", "45", "--max-requests", "10000"]
+

这里设置 24 个 worker,因为我的机器有 12 个逻辑CPU

+

启动命令

1
2
docker build -t panmax/docker-flask-benchmark .
docker run -d --name docker-flask-benchmark --restart=always -p 8081:5000 panmax/docker-flask-benchmark
+

Java

框架:SpringBoot

+

容器采用 SpringBoot 的默认 tomcat 容器,不进行其他修改。

+

核心代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
package com.jpanj.benchmark;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@SpringBootApplication
@RestController
public class BenchmarkApplication {

public static void main(String[] args) {
SpringApplication.run(BenchmarkApplication.class, args);
}

@GetMapping
public String hello() {
return "Hello Java!";
}

}
+

配置文件

1
2
server:
port: 8082
+

启动命令

1
2
3
4
./gradlew build -xtest
cd build/libs

java -jar benchmark-0.0.1-SNAPSHOT.jar
+

GoLang

框架:Gin

+

不需要配置任何容器

+

核心代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package main

import (
"github.com/gin-gonic/gin"
"net/http"
)

func main() {
router := gin.Default()
router.GET("", func(c *gin.Context) {

c.String(http.StatusOK, "Hello GoLang!")
})
router.Run(":8083")
}
+

启动命令

1
2
go build .
./gin-benchmark
+

go build 可以直接编译出一个可以执行文件,这个二进制文件可以直接放在其他机器上无需安装任何环境就可以运行起来,甚至可以在 Mac 上编译 Linux / Windows 的可执行文件,在 Linux 上编译 Mac / Windows 的可执行文件,这个特性非常爽。

+
+

通过浏览器可以验证以上使用 3 种语言开发的简单 Web 程序已经启起来了:

+

+

接下来我们逐个进行性能测试:

Python

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
➜ wrk -t8 -c100 -d30s --latency http://127.0.0.1:8081/
Running 30s test @ http://127.0.0.1:8081/
8 threads and 100 connections
Thread Stats Avg Stdev Max +/- Stdev
Latency 21.45ms 12.75ms 237.13ms 85.43%
Req/Sec 332.10 111.19 640.00 71.40%
Latency Distribution
50% 19.20ms
75% 26.01ms
90% 33.23ms
99% 90.03ms
15917 requests in 30.08s, 2.63MB read
Socket errors: connect 0, read 560, write 0, timeout 0
Requests/sec: 529.21
Transfer/sec: 89.41KB
+

Java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
➜ wrk -t8 -c100 -d30s --latency http://127.0.0.1:8082/
Running 30s test @ http://127.0.0.1:8082/
8 threads and 100 connections
Thread Stats Avg Stdev Max +/- Stdev
Latency 8.22ms 26.99ms 438.58ms 93.31%
Req/Sec 6.93k 3.00k 16.38k 50.11%
Latency Distribution
50% 1.26ms
75% 2.09ms
90% 12.81ms
99% 132.29ms
1631200 requests in 30.06s, 194.75MB read
Requests/sec: 54256.97
Transfer/sec: 6.48MB
+

GoLang

1
2
3
4
5
6
7
8
9
10
11
12
13
14
➜ wrk -t8 -c100 -d30s --latency http://127.0.0.1:8083/
Running 30s test @ http://127.0.0.1:8083/
8 threads and 100 connections
Thread Stats Avg Stdev Max +/- Stdev
Latency 1.80ms 1.95ms 24.69ms 85.85%
Req/Sec 8.68k 786.97 11.03k 65.72%
Latency Distribution
50% 1.36ms
75% 2.68ms
90% 4.32ms
99% 8.42ms
2078830 requests in 30.10s, 257.73MB read
Requests/sec: 69064.35
Transfer/sec: 8.56MB
+
+

可以看到,在每秒请求数量(Requests/sec),也就是并发能力方面,测试结果为:

+
    +
  • Python: 529.21
  • +
  • Java: 54256.97
  • +
  • GoLang: 69064.35
  • +
+

线程平均延迟(Thread Stats - Avg - Latency)的测试结果为:

+
    +
  • Python: 21.45ms
  • +
  • Java: 8.22ms
  • +
  • GoLang: 1.80ms
  • +
+

可以看出,Go 在性能方面甩出 Python 几十条街是没有问题的,比 Java 的性能确实也好很多。

+
+

最后说明一下,这个测试可能存在不严谨性,但是我所采用的部署方案是大部分公司或者程序员最常使用的方式,也能在一定程度上说明问题。

+
+ +
+ + + + + + + +
+ + + + + + +
+ +
+ + + +
+ + + + + + + + +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2019/when-to-use-event-sourcing/1.jpg b/2019/when-to-use-event-sourcing/1.jpg new file mode 100644 index 0000000000..2499a31947 Binary files /dev/null and b/2019/when-to-use-event-sourcing/1.jpg differ diff --git a/2019/when-to-use-event-sourcing/2.png b/2019/when-to-use-event-sourcing/2.png new file mode 100644 index 0000000000..65552db7f2 Binary files /dev/null and b/2019/when-to-use-event-sourcing/2.png differ diff --git a/2019/when-to-use-event-sourcing/index.html b/2019/when-to-use-event-sourcing/index.html new file mode 100644 index 0000000000..ca5bc1005f --- /dev/null +++ b/2019/when-to-use-event-sourcing/index.html @@ -0,0 +1,521 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 何时使用事件溯源 | 贾攀的流水账 + + + + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

贾攀的流水账

+ +
+

Panmax's Blog

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+ + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ 何时使用事件溯源 +

+ + +
+ + + + +
+ + +
+

事件溯源也许很不错,但这确实增加了系统的复杂性。

+
+

+

什么是事件溯源

大多数 Web 应用将系统状态存放在数据库中。

+

假设让你来做一个在线购物网站的数据库设计,按照传统的数据库设计方案将会有 usersproductsorders 表 —— 用来代表系统的状态

+

再假设你完成了前期的编码工作并发布上线了这个在线购物网站,几周后你的老板想知道用户平均更新电子邮箱的次数。

+

在这种传统的数据库设计中,当用户更新电子邮箱时,执行的查询语句大致如下:

+
1
UPDATE users SET email='newemail@mail.com' WHERE id=1;
+

问题在于,我们并没有在数据库中存储修改电子邮件的事件日志。

+

你可以创建一个额外的列 event_log 并在每次用户更新电子邮箱后记录一次 user changed email。但这样仍存在一些问题:

+
    +
  • 需要额外的开发工作才能支持这个特性
  • +
  • 使得数据库设计更加复杂
  • +
  • 只有在实现这个功能后才能生成这些事件,无法对之前产生过的事件进行追溯
  • +
+

这是让事件溯源派上用场的绝佳场景。

+

根据事件溯源设计,你不需要存储系统状态。取而代之的是存储事件

+

比如:当用户注册时,一个 UserCreated 事件被存储。之后当用户更新电子邮箱时,一个 UserChangedEmail 事件被存储。

+

事件溯源系统中的用例事件

+
事件溯源系统中的用例事件
+ +

为什么使用事件溯源

代表我们的思考方式

在现实世界里,人们思考的是事件:当其他人询问你今天过得怎么样时,你会告诉他们一些发生过的有趣事件,不大可能描述你在某一时刻的确切状态。

+

同样,一个领域专家在描述业务流程时谈论的也是一系列事件。通过事件溯源让我们在系统中对其建模变得更加容易。

+

容易生成报告

想知道用户修改了多少次电子邮箱?通过事件溯源,你已经将这些数据详尽记录在案了。

+

想知道一个商品在购物车中被用户移除了多少次?只需简单的对 EventRemovedFromCart 事件进行计算就可以了。

+

通过事件溯源,你可以对你的数据进行全方位的洞察,同时使生成的报告可以追溯。

+

你拥有了可靠的审计日志

你可以生成审计日志用来准确记录系统如何进入了某个状态。

+

比如,考虑一下你的银行账户,事件溯源生成了交易事件的日志,这样就可以清楚的说明为什么你每个月的工资都不够花了。

+

为什么不使用事件溯源

听起来不错,但事件溯源有什么要注意的呢?

+

事件溯源增加了系统额外的复杂性,更多的复杂性意味着难以让新进入的开发人员上手,花更多的时间添加新功能并且也让系统更难以维护。

+

如果你要构建的是一个规模较小的系统,不需要安全审计日志,此时使用事件溯源方法带来的麻烦可能比它的价值更大。

+ +
+ + + + + + + +
+ + + + + + +
+ +
+ + + +
+ + + + + + + + +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2019/why-ddd-is-best-practice-desgin-for-micro-service/1.webp b/2019/why-ddd-is-best-practice-desgin-for-micro-service/1.webp new file mode 100644 index 0000000000..0c640e28f4 Binary files /dev/null and b/2019/why-ddd-is-best-practice-desgin-for-micro-service/1.webp differ diff --git a/2019/why-ddd-is-best-practice-desgin-for-micro-service/2.webp b/2019/why-ddd-is-best-practice-desgin-for-micro-service/2.webp new file mode 100644 index 0000000000..3b9f196cd6 Binary files /dev/null and b/2019/why-ddd-is-best-practice-desgin-for-micro-service/2.webp differ diff --git a/2019/why-ddd-is-best-practice-desgin-for-micro-service/3.webp b/2019/why-ddd-is-best-practice-desgin-for-micro-service/3.webp new file mode 100644 index 0000000000..a8911944f8 Binary files /dev/null and b/2019/why-ddd-is-best-practice-desgin-for-micro-service/3.webp differ diff --git a/2019/why-ddd-is-best-practice-desgin-for-micro-service/4.webp b/2019/why-ddd-is-best-practice-desgin-for-micro-service/4.webp new file mode 100644 index 0000000000..f92fbb910c Binary files /dev/null and b/2019/why-ddd-is-best-practice-desgin-for-micro-service/4.webp differ diff --git a/2019/why-ddd-is-best-practice-desgin-for-micro-service/5.webp b/2019/why-ddd-is-best-practice-desgin-for-micro-service/5.webp new file mode 100644 index 0000000000..c383b7a652 Binary files /dev/null and b/2019/why-ddd-is-best-practice-desgin-for-micro-service/5.webp differ diff --git a/2019/why-ddd-is-best-practice-desgin-for-micro-service/6.webp b/2019/why-ddd-is-best-practice-desgin-for-micro-service/6.webp new file mode 100644 index 0000000000..e632778f2e Binary files /dev/null and b/2019/why-ddd-is-best-practice-desgin-for-micro-service/6.webp differ diff --git a/2019/why-ddd-is-best-practice-desgin-for-micro-service/index.html b/2019/why-ddd-is-best-practice-desgin-for-micro-service/index.html new file mode 100644 index 0000000000..f28a538e7b --- /dev/null +++ b/2019/why-ddd-is-best-practice-desgin-for-micro-service/index.html @@ -0,0 +1,520 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 为什么 DDD 是设计微服务的最佳实践 | 贾攀的流水账 + + + + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

贾攀的流水账

+ +
+

Panmax's Blog

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+ + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ 为什么 DDD 是设计微服务的最佳实践 +

+ + +
+ + + + +
+ + +

在本人的前一篇文章《不要把微服务做成小单体》中,现在很多的微服务开发团队在设计和实现微服务的时候觉得只要把原来的单体拆小,就是微服务了。但是这不一定是正确的微服务,可能只是一个拆小的小单体。这篇文章让我们从这个话题继续,先看看为什么拆出来的是小单体。

+

设计微服务的路径依赖困境

在微服务架构诞生之前,几乎所有的软件系统都是采用单体架构来构建的,因此大部分软件开发者喜欢的开发路径就是单体架构模式。在这样的背景下,根据经济学和心理学的路径依赖法则,当这些开发者基于新的技术想要把原来的大单体拆分成多个部分时,就必然会习惯性地采用自己最擅长的单体架构来设计每个部分。

+

+
+

路径依赖法则:是指人类社会中的技术演进或制度变迁均有类似于物理学中的惯性,即一旦进入某一路径(无论是「好」还是「好」)就可能对这种路径产生依赖。一旦人们做了某种选择,就好比走上了一条不归之路,惯性的力量会使这一选择不断自我强化,并让你轻易走不出去。第一个使「路径依赖」理论声名远播的是道格拉斯・诺斯,由于用「路径依赖」理论成功地阐释了经济制度的演进,道格拉斯・诺斯于 1993 年获得诺贝尔经济学奖。「路径依赖」理论被总结出来之后,人们把它广泛应用在选择和习惯的各个方面。在一定程度上,人们的一切选择都会受到路径依赖的可怕影响,人们过去做出的选择决定了他们现在可能的选择,人们关于习惯的一切理论都可以用「路径依赖」来解释。

+
+

在现实中我们经常看到这个法则随处都会发生,微信刚出来的时候很多人说这不就是手机上的 QQ 吗,朋友圈刚出来的时候他们又会说这不就是抄袭微博吗。很多时候当你兴致冲冲给朋友介绍一个新的东西时,朋友一句话就能让你万念俱灰:这不就是 XXX 吗?之所以这样,是因为人类在接触到新知识新概念的时候,都会下意识的使用以前知道的概念进行套用,这样的思维方式是人类从小到大学习新事物的时候使用的模式,它已经固化成我们大脑操作系统的一部分了。

+

理解了这个法则,我们就可以很容易的明白,已经在单体架构下开发了多年的软件工程师,当被要求要使用微服务架构来进行设计和开发的时候,本能的反应方式肯定是:这不就是把原来的单体做小了吗?但是这样做出来的「微服务」真的能够给我们带来微服务架构的那些好处吗?真的能提高一个企业的数字化响应力吗?

+

不断变化的软件需求和经常被视为效率低下的软件开发一直都是这个行业里最难解决的顽疾,从瀑布到敏捷,都是在尝试找到一个解决这个顽疾的方法,领域驱动设计(Domain Driven Design)也是其中一个药方,而且随着十多年的不断实践,我们发现这个药方有它自己的独特之处,下面我们先来介绍一下这个药方。

+

DDD 简史

+

领域驱动设计这个概念出现在 2003 年,那个时候的软件还处在从 CS 到 BS 转换的时期,敏捷宣言也才发表 2 年。但是 Eric Evans 做为在企业级应用工作多年的技术顾问,敏锐的发现了在软件开发业界内(尤其是企业级应用)开始涌现的一股思潮,他把这股思潮称为领域驱动设计,同时还出版了一本书,在书中分享了自己在设计软件项目时采用的建模方法,并为设计决策者提供了一个框架。

+

但是从那以后 DDD 并没有和敏捷一样变得更加流行,如果要问原因,我觉得一方面是这套方法里面有很多的新名词新概念,比如说聚合,限界上下文,值对象等等,要理解这些抽象概念本身就比较困难,所以学习和应用 DDD 的曲线是非常陡峭的。另一方面,做为当时唯一的「官方教材」《领域驱动设计》,阅读这本书是一个非常痛苦的过程,在内容组织上经常会出现跳跃,所以很多人都是刚读了几页就放下了。

+

虽然入门门槛有些高,但是对于喜欢智力挑战的软件工程师们来说,这就是一个难度稍为有一点高的玩具,所以在小范围群体内,逐渐有一批人开始能够掌控这个玩具,并且可以用它来指导设计能够控制业务复杂性的软件应用出来了。虽然那时候大部分的软件应用都是单体的,但是使用 DDD 依然可以设计出来容易维护而且快速响应需求变化的单体应用出来。

+

+

到了 2013 年,随着各种分布式的基础设施逐渐成熟,而 SOA 架构应用在实践中又不是那么顺利,Martin Fowler 和 James Lewis 把当时出现的一种新型分布式架构风潮总结成微服务架构。然后微服务这股风就呼呼的吹了起来,这时候软件工程师们发现一个问题,就是虽然知道微服务架构的应用具有什么特征,但是如何把原来的大单体拆分成微服务是完全不知道怎么做了。然后熟悉 DDD 方法的工程师发现,由于 DDD 可以有效的从业务视角对软件系统进行拆解,并且 DDD 特别契合微服务的一个特征:围绕业务能力构建。所以用 DDD 拆分出来的微服务是比较合理的而且能够实现高内聚低耦合,这样接着微服务 DDD 迎来了它的第二春。

+

下面让我们站在软件工程这个大视角看看 DDD 究竟是在做什么。

+

DDD 思辨

从计算机发明以来,人类用过表达世界变化的词有:电子化,信息化,数字化。这些词里面都有一个「化」字,代表着转变,而这些转变就是人类在逐渐的把原来在物理世界中的一个个概念一个个工作,迁移到虚拟的计算机世界。但是在转变的过程中,由于两个世界的底层逻辑以及底层语言不一致,就必须要有一个翻译和设计的过程。这个翻译过程从软件诞生的第一天起就天然存在,而由于有了这个翻译过程,业务和开发之间才总是想两个对立的阶级一样,觉得对方是难以沟通的。

+

+

于是乎有些软件工程界的大牛就开始思考,能不能有一种方式来减轻这个翻译过程呢。然后就发明了面向对象语言,开始尝试让计算机世界有物理世界的对象概念。面向对象还不够,这就有了 DDD,DDD 定义了一些基本概念,然后尝试让业务和开发都能够理解这些概念名词,然后让领域专家使用这些概念名词来描述业务,而由于使用了规定的概念名词,开发就可以很好的理解领域业务,并能够按照领域业务设计的方式进行软件实现。这就是 DDD 的初衷:让业务架构绑定系统架构。

+

+

用 DDD 走出设计微服务拆分困境

上面介绍了使用 DDD 可以做到绑定业务架构和系统架构,这种绑定对于微服务来说有什么关系呢。所谓的微服务拆分困难,其实根本原因是不知道边界在什么地方。而使用 DDD 对业务分析的时候,首先会使用聚合这个概念把关联性强的业务概念划分在一个边界下,并限定聚合和聚合之间只能通过聚合根来访问,这是第一层边界。然后在聚合基础之上根据业务相关性,业务变化频率,组织结构等等约束条件来定义限界上下文,这是第二层边界。有了这两层边界作为约束和限制,微服务的边界也就清晰了,拆分微服务也就不再困难了。

+

+

而且基于 DDD 设计的模型中具有边界的最小原子是聚合,聚合和聚合之间由于只通过聚合根进行关联,所以当需要把一个聚合根从一个限界上下文移动到另外一个限界上下文的时候,非常低的移动成本可以很容易地对微服务进行重构,这样我们就不需要再纠结应不应该这样拆分微服务?拆出的微服务太少了以后要再拆分这样的问题了。

+

所以,经过理论的严密推理和大量实践项目的验证,ThoughtWorks 认为 DDD 是当前软件工程业界设计微服务的最佳实践。虽然学习和使用 DDD 的成本有点高,但是如果中国的企业想在软件开发这个能力上从冷兵器时代进入热兵器时代,就应该尝试一下 DDD 了解一下先进的软件工程方法。

+
+

本文转自:https://www.jianshu.com/p/e1b32a5ee91c 并修改了几处错别字

+
+ +
+ + + + + + + +
+ + + + + + +
+ +
+ + + +
+ + + + + + + + +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2020/Backend-Developer-RoadMap/1.png b/2020/Backend-Developer-RoadMap/1.png new file mode 100644 index 0000000000..e8c69ad196 Binary files /dev/null and b/2020/Backend-Developer-RoadMap/1.png differ diff --git a/2020/Backend-Developer-RoadMap/index.html b/2020/Backend-Developer-RoadMap/index.html new file mode 100644 index 0000000000..3d5dd62818 --- /dev/null +++ b/2020/Backend-Developer-RoadMap/index.html @@ -0,0 +1,489 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 后端开发学习路径 | 贾攀的流水账 + + + + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

贾攀的流水账

+ +
+

Panmax's Blog

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+ + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ 后端开发学习路径 +

+ + +
+ + + + +
+ + +

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
互联网
互联网如何工作?
什么是 HTTP?
浏览器如何工作?
DNS 如何工作?
什么是域名?
什么是主机?

前端基础知识
HTML
CSS
JavaScript

操作系统和通用技能
终端的使用
操作系统工作原理
进程管理
内存管理
进程间通信
I/O 管理
POSIX 基础
stdin
stdout
stderr
pipes
基础网络知识
线程和并发
基本命令
grep
awk
lsof
curl
wget
tail
head
less
find
ssh
kill

开发语言
Java
Python
Go
JavaScritp
Ruby
Rust
C#
PHP(虽然是最好的语言)

版本管理
Git 的基本用法
仓库托管服务
GitHub
GitLab
Bitbucket

关系型数据库
PostgreSQL
MySQL
MariaDB
MS SQL
Oracle

NoSQL Database
HBase
MongoDB
CouchDB
RethinkDB
DynamoDB

数据库周边
ORM框架
ACID 特性
BASE 特性
Basic Availability:基本可用。
Soft-state:软状态。
Eventual Consistency:最终一致性
事务
N+1问题
数据库范式
索引是什么、工作原理
数据副本
分片策略
CAP 定理

API设计
REST
Json API
SOAP 协议
认证
基于 Cookies
OAuth
Basic 认证
Token 认证
JWT
OpenID
SAML
Open API 规范 和 Swagger

缓存
CDN
Server 端缓存
Redis
Memcached
Client 端缓存

Web 安全知识
Hash 算法
何为MD5,为什么不要使用MD5来加密?
SHA 家族
SCrypt
BCrypt
HTTPS
CORS
SSL/TLS
内容安全政策

测试
集成测试
单元测试
功能测试

CI / CD

设计模式和开发原则
SOLID
KISS
YAGNI
DRY
GOF 设计模式
领域驱动设计
测试驱动设计

架构模式
单体架构
微服务架构
Service Mesh
SOA
CQRS 和事件源
Serverless

搜索引擎
ElasticSearch
Solr

消息中介
RabbitMQ
Kafka

容器化与虚拟化
Docker
rkt
LXC

GraphQL
Apollo
Relay Modern

图数据库
Neo4j

WebSocket

Web 服务器
Nginx
Apache
Caddy
MS IIS

大规模建设
容错策略
降级
限流
负载转移
断路器
迁移策略
水平与垂直伸缩
可观察性建设
8 条谬论
网络是稳定的
网络传输的延迟是零
网络的带宽是无穷大
网络是安全的
网络的拓扑不会改变
只有一个系统管理员
传输数据的成本是零
整个网络是同构的

标记说明
个人推荐
替代方案
不紧急,用到再学
不推荐

持续学习
+ +
+ + + + + + + +
+ + + + + + +
+ +
+ + + +
+ + + + + + + + +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2020/CPU-performance-mind-map/1.png b/2020/CPU-performance-mind-map/1.png new file mode 100644 index 0000000000..63c76e696a Binary files /dev/null and b/2020/CPU-performance-mind-map/1.png differ diff --git a/2020/CPU-performance-mind-map/index.html b/2020/CPU-performance-mind-map/index.html new file mode 100644 index 0000000000..4968840d55 --- /dev/null +++ b/2020/CPU-performance-mind-map/index.html @@ -0,0 +1,489 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + CPU 性能指标工具脑图 | 贾攀的流水账 + + + + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

贾攀的流水账

+ +
+

Panmax's Blog

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+ + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ CPU 性能指标工具脑图 +

+ + +
+ + + + +
+ + +

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
CPU 使用率
用户 CPU 使用率
user - 用户态 CPU 使用率
nice - 低优先级用户态 CPU 使用率
系统 CPU 使用率
软中断和硬中断 CPU 使用率
其他
steal - 虚拟化环境中会用到的窃取 CPU 利用率:被其他虚拟机占用的 CPU 时间百分比
guest - 客户 CPU 使用率:运行客户虚拟机的 CPU 时间百分比

上下文切换
自愿上下文切换变多了,说明进程都在等待资源,有可能发生了 I/O 等其他问题
非自愿上下文切换变多了,说明进程都在被强制调度,也就是都在争抢 CPU,说明 CPU 的确成了瓶颈;
中断次数变多了,说明 CPU 被中断处理程序占用,还需要通过查看 /proc/interrupts 文件来分析具体的中断类型。

平均负载
如果 1 分钟、5 分钟、15 分钟的三个值基本相同,或者相差不大,那就说明系统负载很平稳。
如果 1 分钟的值远小于 15 分钟的值,就说明系统最近 1 分钟的负载在减少,而过去 15 分钟内却有很大的负载。
如果 1 分钟的值远大于 15 分钟的值,就说明最近 1 分钟的负载在增加
这种增加有可能只是临时性的,也有可能还会持续增加下去,所以就需要持续观察。
一旦 1 分钟的平均负载接近或超过了 CPU 的个数,就意味着系统正在发生过载的问题,这时就得分析调查是哪里导致的问题,并要想办法优化了。

CPU 缓存命中率

工具
平均负载
uptime
top
系统整体 CPU 使用率
vmstat
mpstat
运行 mpstat 查看 CPU 使用率的变化情况:
# -P ALL 表示监控所有CPU,后面数字5表示间隔5秒后输出一组数据
$ mpstat -P ALL 5
top
sar
/proc/stat
其他性能工具的数据来源
进程 CPU 使用率
top
pidstat
# 间隔5秒后输出一组数据
$ pidstat -u 5 1
# 每隔1秒输出1组数据(需要 Ctrl+C 才结束)
# -w参数表示输出进程切换指标,而-u参数则表示输出CPU使用指标
$ pidstat -w -u 1
pidstat 默认显示进程的指标数据,加上 -t 参数后,才会输出线程的指标。
# 每隔1秒输出一组数据(需要 Ctrl+C 才结束)
# -wt 参数表示输出线程的上下文切换指标
$ pidstat -wt 1
ps
htop
atop
系统上下文切换
vmstat
# 每隔5秒输出1组数据
# vmstat 5
cs(context switch)是每秒上下文切换的次数。
in(interrupt)则是每秒中断的次数。
r(Running or Runnable)是就绪队列的长度,也就是正在运行和等待 CPU 的进程数。
b(Blocked)则是处于不可中断睡眠状态的进程数。
进程上下文切换
pidstat
给它加上 -w 选项,你就可以查看每个进程上下文切换的情况了
$ pidstat -w 5 1
cswch ,表示每秒自愿上下文切换(voluntary context switches)的次数
nvcswch ,表示每秒非自愿上下文切换(non voluntary context switches)的次数
软中断
top
/proc/softirqs
mpstat
网络
dstat
sar
# -n DEV 表示显示网络收发的报告,间隔1秒输出一组数据
$ sar -n DEV 1
tcpdump
# -i eth0 只抓取eth0网卡,-n不解析协议名和主机名
# tcp port 80表示只抓取tcp协议并且端口号为80的网络帧
$ tcpdump -i eth0 -n tcp port 80
I/O
dstat
dstat 的好处是,可以同时查看 CPU 和 I/O 这两种资源的使用情况,便于对比分析。
# 间隔1秒输出10组数据
$ dstat 1 10
ipstat
# -d 展示 I/O 统计数据,-p 指定进程号,间隔 1 秒输出 3 组数据
$ pidstat -d -p 4344 1 3
sar
CPU 个数
/proc/cpuinfo
lscpu
事件剖析
perf
perf top:类似于 top,它能够实时显示占用 CPU 时钟最多的函数或者指令,因此可以用来查找热点函数
第一行包含三个数据,分别是采样数(Samples)、事件类型(event)和事件总数量(Event count)。
再往下看是一个表格式样的数据,每一行包含四列,分别是:
Overhead ,是该符号的性能事件在所有采样中的比例,用百分比来表示。
Shared ,是该函数或指令所在的动态共享对象(Dynamic Shared Object),如内核、进程名、动态链接库名、内核模块名等。
Object ,是动态共享对象的类型。比如 [.] 表示用户空间的可执行程序、或者动态链接库,而 [k] 则表示内核空间。
Symbol 是符号名,也就是函数名。当函数名未知时,用十六进制的地址来表示。
perf record 和 perf report
$ perf record # 按Ctrl+C终止采样
§ $ perf report # 展示类似于perf top的报告
# -g开启调用关系分析,-p指进程号21515
$ perf top -g -p 21515
execsnoop
+ +
+ + + + + + + +
+ + + + + + +
+ +
+ + + +
+ + + + + + + + +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2020/Critical-Conversation-mind-map/0.png b/2020/Critical-Conversation-mind-map/0.png new file mode 100644 index 0000000000..249addbf80 Binary files /dev/null and b/2020/Critical-Conversation-mind-map/0.png differ diff --git a/2020/Critical-Conversation-mind-map/index.html b/2020/Critical-Conversation-mind-map/index.html new file mode 100644 index 0000000000..5be3cc5b35 --- /dev/null +++ b/2020/Critical-Conversation-mind-map/index.html @@ -0,0 +1,490 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 《关键对话》脑图 | 贾攀的流水账 + + + + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

贾攀的流水账

+ +
+

Panmax's Blog

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+ + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ 《关键对话》脑图 +

+ + +
+ + + + +
+ + +

+

为了更好的 SEO,把大纲放在下边。读者也可根据大纲自行绘制自己的脑图。

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
从「心」开始
对话技巧
关注你的真正目的
拒绝做出「傻瓜式选择」
关键问题
我让人感觉自己的目的是什么
我的真正目的是什么
关于自己的
关于他人的
关于我们之间关系的
怎样做才能实现这些真正目的
我不希望怎样
怎样才能真正实现希望的目的,避免不希望实现的目的

注意观察
对话技巧
关注交谈何时会变成关键对话
关注安全问题
关注你的压力应对方式
关键问题
我正在陷入沉默或暴力状态吗
对方正在陷入沉默或暴力状态吗

保证安全
对话技巧
在必要时道歉
利用对比法消除误解
利用四步法创建共同目的
关键问题
安全感为什么会出现危机
我是否建立了共目的
我是否保持了彼此尊重
怎样做才能重建安全感

控制想法
对话技巧
行为模式回顾
区分事实和想法
留意三种「小聪明」
改变主观臆断
关键问题
我的想法是什么
我是否故意忽略自己在这个问题中的责任
一个理智而正常的人为什么会这样做
要想实现真正的目的应该怎么做

陈述观点
对话技巧
分享事实经过
说出你的想法
征询对方观点
做出试探表述
鼓励做出尝试
关键问题
我是否对对方观点完全开放
我讨论的是不是真正的问题
我是否自信地表达自己的观点

了解动机
对话技巧
询问观点
确认感受
重新描述
主动引导
赞同
补充
比较
关键问题
我是否积极了解对方的看法
我是否努力避免不必要的不合

开始行动
对话技巧
决定如何决策
记录决策并进行监督检查
关键问题
我们应当怎样决策
何人何时完成何种任务
如何对任务实施检查评估
+ +
+ + + + + + + +
+ + + + + + +
+ +
+ + + +
+ + + + + + + + +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2020/Hoverfly-API-simulation/0.jpg b/2020/Hoverfly-API-simulation/0.jpg new file mode 100644 index 0000000000..71098eb7cb Binary files /dev/null and b/2020/Hoverfly-API-simulation/0.jpg differ diff --git a/2020/Hoverfly-API-simulation/1.png b/2020/Hoverfly-API-simulation/1.png new file mode 100644 index 0000000000..b5a1f71099 Binary files /dev/null and b/2020/Hoverfly-API-simulation/1.png differ diff --git a/2020/Hoverfly-API-simulation/2.png b/2020/Hoverfly-API-simulation/2.png new file mode 100644 index 0000000000..8d8dba9931 Binary files /dev/null and b/2020/Hoverfly-API-simulation/2.png differ diff --git a/2020/Hoverfly-API-simulation/3.png b/2020/Hoverfly-API-simulation/3.png new file mode 100644 index 0000000000..a149adcb8a Binary files /dev/null and b/2020/Hoverfly-API-simulation/3.png differ diff --git a/2020/Hoverfly-API-simulation/4.png b/2020/Hoverfly-API-simulation/4.png new file mode 100644 index 0000000000..0ec50aa29b Binary files /dev/null and b/2020/Hoverfly-API-simulation/4.png differ diff --git a/2020/Hoverfly-API-simulation/index.html b/2020/Hoverfly-API-simulation/index.html new file mode 100644 index 0000000000..c66dbe9c93 --- /dev/null +++ b/2020/Hoverfly-API-simulation/index.html @@ -0,0 +1,547 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 使用 Hoverfly 虚拟化服务 | 贾攀的流水账 + + + + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

贾攀的流水账

+ +
+

Panmax's Blog

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+ + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ 使用 Hoverfly 虚拟化服务 +

+ + +
+ + + + +
+ + +
+ +

在说明什么是服务虚拟化前,先介绍另外两个常见的概念:

+

Mock

Mock 代指那些仅记录它们的调用信息的对象,在测试断言中我们需要验证 Mock 被进行了符合期望的调用。当我们并不希望真的调用生产环境下的代码或者在测试中难于验证真实代码执行效果的时候,我们会用 Mock 来替代那些真实的对象。

+

Mock 典型的例子即是对邮件发送服务的测试,我们并不希望每次进行测试的时候都发送一封邮件,毕竟我们很难去验证邮件是否真的被发出了或者被接收了,我们更多地关注于邮件服务是否按照我们的预期在合适的业务流中被调用。

+

Stub

Stub 代指那些包含了预定义好的数据并且在测试时返回给调用者的对象。Stub 常被用于我们不希望返回真实数据或者会造成其他副作用的场景。

+

Stub 的典型应用场景即是当某个对象需要从数据库获取数据时,我们并不需要真正地与数据库进行交互,而是直接返回预定义好的数据。

+

服务虚拟化

服务虚拟化技术可以让你创建一个模拟外部服务行为的应用程序,但是无需实际运行或者连接到外部服务。与 mock 或者 stub 相比,服务虚拟化技术可以管理更复杂的服务行为。

+

服务虚拟化更像一种智能的 mock 或者 stub,非常适合内部逻辑复杂但接口定义良好,数据响应简单的服务。

+

Hoverfly

Hoverfly 是一款比较新的服务虚拟化工具,可以模拟遗留系统复杂的响应,以及支持许多服务相互依赖的微服务架构。Hoverfly 使用 Go 语言编写,轻量、高性能。

+

Hoverfly 有非常丰富的功能和使用场景,今天我们仅介绍 Hoverfly 作为代理服务器时的两种常用模式:Capture 模式 和 Simulate 模式。

+

安装

1
➜ brew install SpectoLabs/tap/hoverfly
+

启动

Hoverfly 带有一个称为 hoverctl 的命令行工具。

+
1
2
3
4
5
6
7
➜ hoverctl start
Hoverfly is now running

+------------+------+
| admin-port | 8888 |
| proxy-port | 8500 |
+------------+------+
+

可以看到默认情况下, Hoverfly 的代理服务运行在 8500 端口,后台页面运行在 8888 断口。

+

状态检查

1
2
3
4
5
6
7
8
9
10
11
➜ hoverctl status

+------------+----------+
| Hoverfly | running |
| Admin port | 8888 |
| Proxy port | 8500 |
| Proxy type | forward |
| Mode | capture |
| Middleware | disabled |
| CORS | disabled |
+------------+----------+
+

Hoverfly 作为代理服务器

+

如图所示,Hoverfly 最常见的使用场景是作为一个代理服务器在客户端和服务器之间传递请求。默认情况下,Hoverfly 作为代理服务器启动。

+

代理服务器 VS Web服务器

Hoverfly 还可以作为 Web服务器启动,这个不作为本文的重点,在这里简单对代理和Web服务器做个区分:

+

代理服务器是一种特殊的Web服务器,二者主要区别在于:当Web服务器接收到来自客户端的请求时,它以预期的响应内容(例如HTML页面)进行响应。 通常,响应的数据存放在该服务器上或同一网络中。

+

代理服务器应将传入的请求转发到另一台服务器(目标),同时,它还需要设置一些适当的头信息,如 X-Forwarded-ForX-Real-IPX-Forwarded-Proto 等。一旦代理服务器从目标接收到响应,再由它回传给客户端。

+

捕获(Capture)模式

捕获模式用于创建 API 模拟数据。

+

+

在捕获模式下,Hoverfly 拦截客户端和外部服务之间的通信,并透明地记录来自客户端的传出请求和来自服务 API 的传入响应(类似于一个中间人)。

+

通常,捕获模式被用作创建 API 模拟过程的起点, 然后将捕获的数据导出并修改,再重新导入到 Hoverfly 中以用作后续的模拟。

+

默认情况下,如果请求未更改,Hoverfly 将忽略重复的请求。 所以尝试捕获有状态的端点可能会出现问题,因为每次发出请求时,该端点可能会返回不同的响应。当然,也可以通过配置禁用重复请求检查,所捕获的重复请求在仿真模式时会被循序使用。

+

仿真(Simulate)模式

在仿真模式下,Hoverfly 使用其模拟数据来仿真外部API。 每次 Hoverfly 收到请求时,它都会使用之前捕获到的数据进行响应(而不是将其转发到真实的服务)。 没有网络流量会到达真正的外部服务。

+

+

仿真数据可以通过在捕获模式下运行 Hoverfly 自动生成,也可以手动创建。

+

示例

我们通过 curl 命令来做一个完整的演示。

+

http://time.jsontest.com/ 是一个可以提供当前时间的 API 端点:

+
1
2
3
4
5
6
➜ curl -s http://time.jsontest.com/ | jq
{
"date": "06-27-2020",
"milliseconds_since_epoch": 1593247079638,
"time": "08:37:59 AM"
}
+

通过 hoverctl 将 Hoverfly 切换到捕获模式:

1
2
3
➜ hoverctl mode capture

Hoverfly has been set to capture mode
+

进入捕获模式后,我们再次想上边的 API 发出一个请求,不过这次指定 Hoverfly 作为代理:

+
1
2
3
4
5
6
➜ curl -s --proxy http://localhost:8500 http://time.jsontest.com | jq
{
"date": "06-27-2020",
"milliseconds_since_epoch": 1593247241163,
"time": "08:40:41 AM"
}
+

通过指定 proxy 参数,请求会首先转到代理,也就是 Hoverfly,然后再被转发到真正的 API 服务。响应的接收过程也是类似,这是 Hoverfly 拦截网络流量的方式。可以通过 Hoverfly 的日志了解具体发生了什么。

+
1
2
3
4
5
➜ hoverctl logs
……
……
INFO[2020-06-27T16:39:31+08:00] Mode has been changed mode=capture
INFO[2020-06-27T16:40:41+08:00] request and response captured mode=capture request="&map[body: destination:time.jsontest.com headers:map[Accept:[*/*] Proxy-Connection:[Keep-Alive] User-Agent:[curl/7.64.1]] method:GET path:/ query:map[] scheme:http]" response="&map[error:nil response]"
+

切换到仿真模式:

1
2
➜ hoverctl mode simulate
Hoverfly has been set to simulate mode with a matching strategy of 'strongest'
+

在仿真模式下,Hoverfly 会使用我们之前记录下来的请求来对客户端进行响应,而不是将流量转发到真正的 API。

+

现在我们继续以 Hoverfly 为代理,重复发起之前的请求:

1
2
3
4
5
6
➜ curl -s --proxy http://localhost:8500 http://time.jsontest.com | jq
{
"date": "06-27-2020",
"milliseconds_since_epoch": 1593247241163,
"time": "08:40:41 AM"
}
+

返回的数据与我们在捕获模式下请求的结果相同。

+

接下来,如果我们想在客户端中模拟一个 100 年之后的响应,可以之前将捕获到的数据进行导出,重新编辑后导入 Hoverfly:

+
1
2
➜ hoverctl export simulation.json
Successfully exported simulation to simulation.json
+

查看 simulation.json` 的内容:

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
{
"data": {
"pairs": [
{
"request": {
"path": [
{
"matcher": "exact",
"value": "/"
}
],
"method": [
{
"matcher": "exact",
"value": "GET"
}
],
"destination": [
{
"matcher": "exact",
"value": "time.jsontest.com"
}
],
"scheme": [
{
"matcher": "exact",
"value": "http"
}
],
"body": [
{
"matcher": "exact",
"value": ""
}
]
},
"response": {
"status": 200,
"body": "{\n \"date\": \"06-27-2020\",\n \"milliseconds_since_epoch\": 1593247241163,\n \"time\": \"08:40:41 AM\"\n}\n",
"encodedBody": false,
"headers": {
"Access-Control-Allow-Origin": [
"*"
],
"Content-Length": [
"100"
],
"Content-Type": [
"application/json"
],
"Date": [
"Sat, 27 Jun 2020 08:40:41 GMT"
],
"Hoverfly": [
"Was-Here"
],
"Server": [
"Google Frontend"
],
"X-Cloud-Trace-Context": [
"50e02f0f588bb1a559fcb071b8da1344;o=1"
]
},
"templated": false
}
}
],
"globalActions": {
"delays": [],
"delaysLogNormal": []
}
},
"meta": {
"schemaVersion": "v5.1",
"hoverflyVersion": "v1.3.0",
"timeExported": "2020-06-27T16:51:04+08:00"
}
}
+

找到其中的body 字段,将其修改为:

1
"body": "{\n   \"date\": \"06-27-2120\",\n   \"milliseconds_since_epoch\": 1593247241163,\n   \"time\": \"08:40:41 AM\"\n}\n",
+

将仿真数据导入 Hoverfly:

1
2
➜ hoverctl import simulation.json
Successfully imported simulation from simulation.json
+

再次发送请求:

1
2
3
4
5
6
➜ curl -s --proxy http://localhost:8500 http://time.jsontest.com | jq
{
"date": "06-27-2120",
"milliseconds_since_epoch": 1593247241163,
"time": "08:40:41 AM"
}
+

时间快进到了 100 年后。

+

到这里,我们已经成功模拟了一个API端点,虽然我们这次演示了 curl 命令,但是在时间的测试中,应该由正在测试的应用程序向 Hoverfly 发起请求。一旦 Hoverfly 存储了请求和响应的数据,我们就不再需要访问真正的服务了,可以控制 Hoverfly 返回准确的响应。

+

最后

Hoverfly 还有一个可视化的管理页面,可以访问:http://localhost:8888/ 来进行查看,也可以通过这个界面来进行模式间的切换。

+
+ +

因为 hoverctl 提供了比较友好的交互命令,所以这个页面的用途不是太大。

+

结尾

使用 Hoverfly 这样的虚拟服务化工具,主要的好处是资源占用少、初始化速度快,因此我们可以在开发电脑上虚拟化出比实际更多的服务,也可以快速集成测试中使用的 Hoverfly。

+ +
+ + + + + + + +
+ + + + + + +
+ +
+ + + +
+ + + + + + + + +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2020/Kibana-dashboard-auto-authenticating/0.jpg b/2020/Kibana-dashboard-auto-authenticating/0.jpg new file mode 100644 index 0000000000..ca9bad9a3c Binary files /dev/null and b/2020/Kibana-dashboard-auto-authenticating/0.jpg differ diff --git a/2020/Kibana-dashboard-auto-authenticating/1.png b/2020/Kibana-dashboard-auto-authenticating/1.png new file mode 100644 index 0000000000..326f96baeb Binary files /dev/null and b/2020/Kibana-dashboard-auto-authenticating/1.png differ diff --git a/2020/Kibana-dashboard-auto-authenticating/index.html b/2020/Kibana-dashboard-auto-authenticating/index.html new file mode 100644 index 0000000000..7874105644 --- /dev/null +++ b/2020/Kibana-dashboard-auto-authenticating/index.html @@ -0,0 +1,504 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 免登陆查看 Kibana Dashboard | 贾攀的流水账 + + + + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

贾攀的流水账

+ +
+

Panmax's Blog

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+ + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ 免登陆查看 Kibana Dashboard +

+ + +
+ + + + +
+ + +
+ +

7.x 中,elastic 公司开放了 x-pack 的认证功能,所以我们可以对 Kibana 的使用也进行登陆认证,保障了系统的安全性。

+
+ +

这样导致的问题是,我们将 Kibana 中创建好的报表通过 iFrame 的方式嵌入到其他系统中后,运营人员在查看报表时也需要进行登陆,有没有什么办法可以不登陆就查看报表呢?

+

可以通过 Nginx,将拼好的 Authorization 请求头传递给 Kibana 服务。

+

请求头的生成策略是:

+
1
base64(用户名:密码)
+

Nginx 示例配置:

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
server {
listen 15601;
server_name 127.0.0.1;

location / {
proxy_pass http://<实际的 kibana IP>:5601;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header REMOTE-HOST $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
# 将 base64 编码的 <用户名:密码> 传过去
proxy_set_header Authorization "Basic cmVhZC1vbmx5OnRtIypkNCZyJXA0aGM=";
}

}
+

建议新建一个只有 read-only 权限的角色和用户,用他的 token 来进行免登陆查看报表。

+

通过 kibana 生成 iFrame 的嵌入代码后,只需将将里边的正常 kibana url 前缀部分替换为这个 Nginx 的地址和对应的端口号就可以了。

+

TIP

可以使用如下命令在 Linux 中生成 base64 编码:

+
1
echo -n username:password | base64
+ +
+ + + + + + + +
+ + + + + + +
+ +
+ + + +
+ + + + + + + + +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2020/Look-out-Look-in-ch3-mind-map/index.html b/2020/Look-out-Look-in-ch3-mind-map/index.html new file mode 100644 index 0000000000..a40c000104 --- /dev/null +++ b/2020/Look-out-Look-in-ch3-mind-map/index.html @@ -0,0 +1,494 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 沟通的艺术第三章《沟通和认同:自我的塑造与展现》脑图 | 贾攀的流水账 + + + + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

贾攀的流水账

+ +
+

Panmax's Blog

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+ + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ 沟通的艺术第三章《沟通和认同:自我的塑造与展现》脑图 +

+ + +
+ + + + +
+ + +

自我的塑造与展现

+

为了更好的 SEO,把大纲放在下边。读者也可根据大纲自行绘制自己的脑图。

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
沟通和自我
自我概念与自尊
定义
自我概念是指你对自己所持有的相对稳定的知觉
自尊是你对自我价值的评估
举例
安静
「我简直是个懦夫,所以才会说不出话来」
「我享受倾听甚于讲话」
好辩
「我太强势了,一定很惹人厌」
「我在自己的信念上坚定不移」
自律
「我太小心翼翼了」
「在我开口说或动手之前,我总会深思熟虑」
影响
高自尊的人倾向于认为别人是好的,并且期待被他们接受
低自尊的人认为所有人都一直用批判的眼光看待他们
自我的生物性和社会性根源
说明
生物性:人格在很大程度上是由基因决定的。
社会化:从我们相关的人那里得到的信息在塑造我们的自我概念时扮演了重要的角色
自我概念的形成
反应评价:我们每个人得出的自我概念反应的是我们认为别人看待我们的方式。
重要他人:你的自我评价或评价标准可能因为他人对待你的方式而受到影响。
社会比较:依据他人的对照方式评估自己
自我概念的特征
特征
主观:我们倾向于相信我们的自我概念是准确的,但事实上它很可能被歪曲了。
抗拒改变:我们倾向于坚持一个现存的自我概念,即使有证据显示它是过时的。
接受正面自我的建议
对自己有真实的认知
有切合实际的期望
有改变的意愿
有改变的技巧
文化、性别和认同
文化:孕育我们长大的文化以不着痕迹的方式塑造着我们对自我的理解
大部分西方文化都是高度个人主义的
大部分的亚洲人是倾向于集体主义的
性征和性别:性别角色和性别标签对男人和女人如何看待自己以及如何沟通有着深刻影响。
自我应验预言和沟通
定义:指如果个体对事件的发生有所预期,并且他接下来的行为是建立在这些预期上的,那么这件事的发生会比没有预期更可能成真。
类型
自我强加的预言:指的是你的自我期待对你的行为产生影响
他人强加的预言

沟通作为印象管理
公开自我和隐私自我
觉知的自我是指你在真诚的自省过程中所相信的自己。(隐私的)
展现的自我是我们想要别人如何看待我们。(公开的)
印象管理的特征
多重身份
合作式的:某一刻发生的事,是由沟通双方及其在长期的交往中积累的经验共同导致的。
深思熟虑或不知不觉:大部分人都在有意识或无意识地,以一种有助于建立在自己和他人眼中的理想身份的方式进行沟通。
为什么要印象管理?
为了开始和经营关系
为了获得别人的顺从
为了保住别人的颜面
为了探索新的自我
面对面印象管理
举止,由一个沟通者的预言和非语言行为组成
外貌,是人们用来塑造印象的个人化方式
配置,即我们用来影响别人如何看待我们的物理工具
网络印象管理
印象管理和诚实
印象管理意味着选择自己的哪个角色或者哪个部分加以展现

在关系中的自我坦露
自我坦露的模式
社会穿透
广度
深度
乔哈里视窗
开放区
盲视区
隐藏区
未知区
自我坦露的好处与风险
好处
宣泄:可以提供心理上和情感上的双重慰藉
互惠:一个自我袒露的行为会引发另一个自我袒露行为
自我澄清:通过和他人谈论你的信念、意见、想法、态度和感觉,可以理清你对于这些话题的看法。
自我确认:旨在确认你自我概念中的重要组成部分
关系的建立和维持:开始一段关系是需要一定程度的自我袒露的
社会控制:袒露个人信息会增加你对他人的控制,有时也会增加你对情境的控制
风险
拒绝
负面印象
降低关系满意度
丧失影响力
伤害别人
自我坦露的指导原则
这个人对你而言重要吗?
坦露的方式合适吗?
坦露的风险合理吗?
有建设性影响吗?
你的自我坦露是互惠的吗?
你在道德上有义务坦露吗?

自我坦露的替代选择
沉默:将自己的想法与感受保留在心中
说谎:善意的谎言被定义为对被告知的人来说是没有恶意的,甚至是有帮助的
模棱两可:当面对是说谎还是说出一个令人不愉快的真相的困境时,沟通者常常会选择一种模棱两可的回答
暗示:暗示要比模棱两可更直接。这是因为模棱两可的说法不要求改变他人的行为,而暗示确实旨在从他人那里得到期待的回应。

指有意透漏与自己相关信息的过程,而且这些信息通常是重要的、不为人所知的。

四个步骤
1. 持有某种期待
2. 表现出与期望一致的行为
3. 期待如是发生
4. 强化起初的期待

原因
过期的信息
歪曲的回馈
完美主义
社会期待

@Panmax
+ +
+ + + + + + + +
+ + + + + + +
+ +
+ + + +
+ + + + + + + + +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git "a/2020/Look-out-Look-in-ch3-mind-map/\350\207\252\346\210\221\347\232\204\345\241\221\351\200\240\344\270\216\345\261\225\347\216\260.png" "b/2020/Look-out-Look-in-ch3-mind-map/\350\207\252\346\210\221\347\232\204\345\241\221\351\200\240\344\270\216\345\261\225\347\216\260.png" new file mode 100644 index 0000000000..a929d8686c Binary files /dev/null and "b/2020/Look-out-Look-in-ch3-mind-map/\350\207\252\346\210\221\347\232\204\345\241\221\351\200\240\344\270\216\345\261\225\347\216\260.png" differ diff --git a/2020/Look-out-Look-in-ch8-mind-map/index.html b/2020/Look-out-Look-in-ch8-mind-map/index.html new file mode 100644 index 0000000000..1308e52335 --- /dev/null +++ b/2020/Look-out-Look-in-ch8-mind-map/index.html @@ -0,0 +1,494 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 沟通的艺术第八章《倾听:不止是听见》脑图 | 贾攀的流水账 + + + + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

贾攀的流水账

+ +
+

Panmax's Blog

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+ + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ 沟通的艺术第八章《倾听:不止是听见》脑图 +

+ + +
+ + + + +
+ + +

倾听

+

为了更好的 SEO,把大纲放在下边。读者也可根据大纲自行绘制自己的脑图。

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
定义
听与倾听
听 是声音传到耳膜引起振动后经听觉传送到大脑的过程
倾听 是大脑将电化学脉冲重构为原始声音的再现,再赋予其意义的过程
心不在焉的倾听
发生在我们自动地或常规地回应别人的信息时
心无旁骛的倾听
对我们接收到的信息给予仔细而审慎的专注和反应

五个要素
听到
生理维度
专注
心理过程
理解
发生在我们弄懂信息的意思时
回应
对说话者给予明显的反馈
好的倾听者会使用非语言行为来表现他们的专注
记忆
记住信息的能力

挑战
无效的倾听类型
虚伪地倾听
虚伪地倾听者看上去是很专注的,但专注的样子只是礼貌的假象
自恋地倾听
设法把谈话的主题转移到他们自己身上
说话者:「我的数学课程真的很难」

自恋倾听者:「你认为你的数学难?那你应该来上一下我的物理课」
另一个特点是打断别人说话
选择性倾听
只会针对他们感兴趣的部分回应
隔绝性倾听
与选择性倾听者相反,这类倾听者避免听到某些信息
防卫性倾听
认为别人说的话都是在攻击自己
埋伏性倾听
仔细地倾听说话者说的话只是为了搜集信息,以便借此攻击说话者的言论
愚钝性倾听
对说话者信息的表面内容做出反应,却漏掉了说话者没有直接表达出来的更为重要的情绪性信息。
导致无效倾听的原因
超负荷的信息
先入为主
我们通常会先将注意力放在自己关心的问题上
飞快的思维
倾听速度比说话速度快得多,当别人说话时我们便有了很多「多余时间」
有效倾听的技巧就是利用「多余时间」来更好地理解说话者的想法,而不是让自己的注意力漫游
费力
仔细倾听别人说话所耗费的心力不亚于一次锻炼
外在噪音
错误假定
有时候我们会假定说话者的想法太简单、太浅显,不值得我们付出注意力,然而事实可能正好相反
缺乏明显的益处
通常我们认为说话可以比倾听带来更多好处
缺乏训练
倾听和说话一样都需要技巧
听力问题
有效倾听技巧
少说话
避免自恋地倾听或一味地把话题转移到自己地想法上
少说话并不意味着必须保持沉默
摆脱注意力分散
如果你要搜集地信息真的很重要,那你应该尽一切可能去消除那些会让你分心的内在和外在干扰
关掉电视
关闭手机
安静的房间
不要过早评断
在理解别人说话的意思之前不要过早下评断
确定你真正理解对方地所有意思后,再去评论
寻找重点
利用思考的速度比说话速度更快的能力,从听到的话中提取出对方的核心观点

回应方式
借力使力
使用沉默和简短的言论来鼓励对方多说一些话
帮助你更好地理解说话者
帮助说话者弄清楚他们的想法和感觉
问话
帮助提问者
更加清楚对方的想法和感受
对事实和细节有更深入的理解
清楚对方想法和感受,以及可能的期望
帮助回答者
有助于自我坦露
了解他自己的各种期望和需要
虚伪的问话类型
给说话者设圈套
「你不喜欢那部电影是吗?」
附加问句
「难道你不认为他会成为一个好老板吗?」
「你说你会在5点钟打电话过来,但是你却忘了,不是吗?」
实为陈述
「你终于挂掉电话了?」
「你借钱给托尼了?」
「你会勇敢地面对他,让他接受应有的惩罚吗?」
带有隐蔽计划
「你星期五晚上忙吗?」
「你会帮我吗?」
「如果我告诉你发生了什么,你能保证不生气吗?」
寻求「正确」答案
「你觉得我应该穿哪双鞋?」
「亲爱的,你觉得我看起来胖吗?」
基于未经核实的假设
「你为什么不听我说?」
「出什么事了?」
释义
倾听者将自己解读的信息重说一次
说话者:「我是很想去,可是我怕我负担不起。」

释义式回应:「所以如果我们能一起想想办法,帮助你负担这笔钱,你就愿意和我们一起去了,是这样吗?」
说话者:「天哪!你看起来真是有点糟糕!」

释义式回应:「你是不是觉得我看起来胖太多了?」
两个层次
事实性信息
「所以你是这周二开会,不是下周二,对吗?」
个人性信息
「所以,我的玩笑让你以为我不在乎你的问题?」
三种方法
改变说话者的修辞
说话者:「双语教育只不过是另一个既失败又浪费钱的政策」

释义者:「你看看我理解的对不对,你很生气是因为你觉得双语教育听起来很棒,但实际上却没什么作用,对吗?」
举出一个例子
实话者:「李是一个浑蛋,我真不敢相信他昨晚所做的事!」

释义者:「你觉得那些笑话很惹人厌,对吗?」
反应说话者的潜在寓意
释义者:「你一直提醒我要小心,听起来好像你在担心会有事发生在我身上,会吗?」
考虑因素
这个问题够复杂吗?
对你来说,有必要投入时间和关注吗?
你能克制住不去评判吗?
释义和你的其他倾听反应成比例吗?
支持
支持性回应就是听者表明自己和说话者立场一致
分类
同理心
「我可以理解你为什么会这么沮丧」
「是啊,这门课对我来说也很困难」
同意
「你说得对,房东真的很不公平」
「听起来那份工作很适合你」
提供协助
「如果你需要我的话,我就在这里」
「如果你喜欢,我很乐意下次考前再和你一起温习」
赞美
「哇,你做得真好!」
「你是一个很好的人,如果她认识不到这一点,那是她的问题」
恢复信心
「最糟糕的情况已经结束了,从现在开始一切都会好转的」
「我确定你会做得很好」
无效的支持性反应
否认别人拥有某种感觉的权力
「不用担心」
「这又没什么,不值得你这样难过」
「你这样真的很好笑」
看轻事情的重要性
「嘿,那就只是……而已」
聚焦在「彼时彼地」,而非「此时此地」
「十年后你会连她的名字也记不起来」
火上浇油的评断
「你知道吗?那是你的错!当初你就不应该这么做」
自我聚焦
「我绝对理解你现在的感受,因为我也遇到过这种情况……」
自我防卫
「不要怪我!我已经做完我要做的那部分了。」
有效的支持性参考原则
要认识到你可以支持他人的努力,而不必赞同他的决定
监控对方对你的支持性回应的反应
要认识到支持也不总会受欢迎
确保你对后果已经做好了准备
分析
倾听者对说话者的信息提供一种解释
「我想真正困扰你的是……」
「她已经在做了,因为……」
「我认为你不是真的那样想。」
「也许这个问题开始于他……」
潜在问题
解释可能不正确
即使分析是正确的,告诉对方也不一定有用
提出分析时遵循的原则
最好用试探的而非绝对的口吻
「也许这个问题的原因是……」
「这个问题在我看来可能是……」
确定对方愿意接受你的分析
确定你提出分析的动机真的是出于帮助对方
忠告
通过提供解决方案来帮助对方
注意事项
这个忠告有必要吗?
「我不能相信你竟然和他一起回来了」
对方真的想听你的忠告吗?
有时人们想要的只是一双倾听的耳朵,而不是他们问题的解决方案
你提出劝告的方式正确吗?
在提供劝告之前了解实情
你的忠告是专家级别的吗?
如果你不具备相关的专业知识,那你最好给说话者提供一些支持性回应,然后鼓励他向专家寻求建议
提出忠告的人是关系密切、值得信任的人吗?
你提出忠告的态度是谨慎、顾全对方面子的吗?
评断
用某种方式去评价信息发送者的想法或行为
可能是讨人喜欢的
「你的意见真棒!」
「现在你正走在正确的道路上」
也可能是不讨人喜欢的
「你这样的态度是不会有什么好结果的」
有时纯粹是为了批评别人
「我早就告诉过你了」
「你真该为你自己感到惭愧」
有些是「建设性评价」,目的是希望能够让对方在未来有更好的进步,例如好朋友之间相互机遇的建设性评价
评判被接受的两个条件
当身处困境的人向你寻求判断时
你判断的动机是真诚的、有建设性的,而不是为了奚落对方

选择回应方式要考虑的因素
性别
男人和女人在倾听和回应方式上都存在差异
女性经常提供情感支持的回应
男性习惯通过评断对方的态度和价值来提供协助
情境
沟通者需要去分析情境,发展出合适的反应
规则
开头时使用寻求理解并提供最少指示的回应:借力使力、问话、释义、支持
一旦收集到足够的实情,并且展现出了你的兴趣和关切,此时说话者更有可能接受你的分析、忠告和评断回应
对象
根据对象的不通而调整你的反应
找出最适合的回应方式的一个办法是直接询问对方他想要你做什么
「你是要听我的忠告,还是只需要吐吐苦水?」
你的个人风格
当你思考如何回应对方的信息时,最好同时反省一下自己的优缺点,然后做出相应的调整

关键在于你要用自己的措辞重述别人的观点

相信别人有能力思考他们自己的问题

具有一种潜在价值:能让我们自由地将心思聚集在需要我们小心注意地的信息上

自由主题

暗示了一个事实:你是那个有权利和资格去评判说话者想法或行为的人

因为分析就是在暗示你比对方优秀,看得比他透彻

@Panmax
+ +
+ + + + + + + +
+ + + + + + +
+ +
+ + + +
+ + + + + + + + +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git "a/2020/Look-out-Look-in-ch8-mind-map/\345\200\276\345\220\254.png" "b/2020/Look-out-Look-in-ch8-mind-map/\345\200\276\345\220\254.png" new file mode 100644 index 0000000000..249ef73bae Binary files /dev/null and "b/2020/Look-out-Look-in-ch8-mind-map/\345\200\276\345\220\254.png" differ diff --git a/2020/Neo4j-cheatsheet/index.html b/2020/Neo4j-cheatsheet/index.html new file mode 100644 index 0000000000..9919f5d3a5 --- /dev/null +++ b/2020/Neo4j-cheatsheet/index.html @@ -0,0 +1,588 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Neo4j 速查手册 | 贾攀的流水账 + + + + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

贾攀的流水账

+ +
+

Panmax's Blog

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+ + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ Neo4j 速查手册 +

+ + +
+ + + + +
+ + +

基础概念

图数据库中使用以下概念来存储数据:

+
    +
  • 节点(Node):图数据记录
  • +
  • 关系(Relationship):用来连接节点(拥有方向类型
  • +
  • 属性(Property):在节点和关系中以键值对的形式存储数据
  • +
  • 标签(Label):节点和关系的分组(可选)
  • +
+

Cypher

匹配

匹配节点

1
2
3
MATCH (ee:Person)
WHERE ee.name = "Emil"
RETURN ee;
+
    +
  • MATCH 子句指定节点和关系的模式
  • +
  • (ee:Person) 带有 Person 标签的单节点模式,并将匹配项分配给变量 ee
  • +
  • WHERE 子句用来对返回结果进行约束
  • +
  • ee.name=”Emil”name 属性与 Emil 进行比较
  • +
  • RETURN 用于请求特定结果的子句
  • +
+

匹配节点和关系

1
2
3
MATCH (ee:Person)-[:KNOWS]-(friends)
WHERE ee.name = "Emil"
RETURN ee, friends
+
    +
  • MATCH 子句描述从已知节点查找节点的模式
  • +
  • (ee) 从标签为 Person 的节点开始模式
  • +
  • -[:KNOWS]- 匹配 KNOWS 关系(方向不限)
  • +
  • (friends) 绑定了 Emil 的朋友列表(认识的人)
  • +
+

匹配标签

1
2
MATCH (n:Person)
RETURN n
+

或者

+
1
2
3
MATCH (n)
WHERE n:Person
RETURN n
+

匹配多个标签

匹配 :Car :Person 标签

+
1
2
3
MATCH (n)
WHERE n:Person OR n:Car
RETURN n
+

匹配 :Car :Person 标签

+
1
2
3
MATCH (n)
WHERE n:Person:Car
RETURN n
+

匹配属性

1
2
3
MATCH (a:Person)
WHERE a.from = "Sweden"
RETURN a
+

返回属性 from 的值为 Sweden 的每个节点(和他们的关系)

+

匹配有相同爱好的朋友

Johan 正在学习冲浪,他想认识他的朋友中爱好冲浪的朋友

+
1
2
3
MATCH (js:Person)-[:KNOWS]-()-[:KNOWS]-(surfer)
WHERE js.name = "Johan" AND surfer.hobby = "surfing"
RETURN DISTINCT surfer
+
    +
  • () 空括号忽略这些节点
  • +
  • DISTINCT 因为不止一条路径与模式匹配
  • +
+

ID 匹配

每个节点都有一个内部的自增 ID,可以通过 <, <=, =, >=, <>IN 操作进行查询。

+

通过 ID 查询

+
1
2
3
MATCH (n)
WHERE id(n) = 0
RETURN n
+

查询多个 ID

+
1
2
3
MATCH (n)
WHERE id(n) IN [1, 2, 3]
RETURN n
+

根据 ID 查询关系

+
1
2
3
MATCH ()-[n]-()
WHERE id(n) = 0
RETURN n
+

创建

创建节点

1
CREATE (ee:Person { name: "Emil", from: "Sweden", klout: 99 })
+
    +
  • CREATE 子句用来创建数据
  • +
  • () 圆括号用于表示节点
  • +
  • ee:Person 将标签为 Person 的新节点赋值给 ee
  • +
  • {} 花括号为节点添加属性(键值对)
  • +
+

创建节点和关系

1
2
3
4
5
6
7
8
9
MATCH (ee:Person) WHERE ee.name = "Emil"
CREATE (js:Person { name: "Johan", from: "Sweden", learn: "surfing" }),
(ir:Person { name: "Ian", from: "England", title: "author" }),
(rvb:Person { name: "Rik", from: "Belgium", pet: "Orval" }),
(ally:Person { name: "Allison", from: "California", hobby: "surfing" }),
(ee)-[:KNOWS {since: 2001}]->(js),(ee)-[:KNOWS {rating: 5}]->(ir),
(js)-[:KNOWS]->(ir),(js)-[:KNOWS]->(rvb),
(ir)-[:KNOWS]->(js),(ir)-[:KNOWS]->(ally),
(rvb)-[:KNOWS]->(ally)
+
    +
  • MATCH 子句将 Emil 赋给 ee
  • +
  • CREATE 子句创建带有标签和属性的多个节点(用逗号分隔),同时创建了带有方向的关系 (a)-[:Label {key: value}]->(b)
  • +
+

在两个无关系的节点间新建关系

1
2
3
MATCH (n), (m)
WHERE n.name = "Allison" AND m.name = "Emil"
CREATE (n)-[:KNOWS]->(m)
+

或者使用 MERGE,这样可以确保关系只创建一次

+
1
2
MATCH (n:User {name: "Allison"}), (m:User {name: "Emil"})
MERGE (n)-[:KNOWS]->(m)
+

创建带多个标签的节点

1
CREATE (n:Actor:Director)
+

更新

更新节点属性(添加或修改)

添加新的 owns 属性(如果已存在则执行修改)

+
1
2
3
MATCH (n)
WHERE n.name = "Rik"
SET n.owns = "Audi"
+

替换节点属性

警告:如下操作会删除之前的属性并添加 playsage 属性

+
1
2
3
MATCH (n)
WHERE n.name = "Rik"
SET n = {plays: "Piano", age: 23}
+

批量添加新的节点属性(不删除老的)

警告:如果 plays 或者 age 属性已经存在的情况下会被覆盖。

+
1
2
3
MATCH (n)
WHERE n.name = "Rik"
SET n += {plays: "Piano", age: 23}
+

属性不存在的情况下添加属性

1
2
3
MATCH (n)
WHERE n.plays = "Guitar" AND NOT (EXISTS (n.likes))
SET n.likes = "Movies"
+

为所有节点属性重命名

1
2
3
4
MATCH (n)
WHERE NOT (EXISTS (n.instrument))
SET n.instrument = n.plays
REMOVE n.plays
+

或者

+
1
2
3
4
MATCH (n)
WHERE n.instrument is null
SET n.instrument = n.plays
REMOVE n.plays
+

为现有节点添加标签

给 id 为 7 和 8 的节点添加 :Food 标签

+
1
2
3
MATCH (n)
WHERE id(n) IN [7, 8]
SET n:Food
+

如果节点不存在,创建节点并更新(或添加)属性

1
2
MERGE (n:Person {name: "Rik"})
SET n.owns = "Audi"
+

删除

删除节点

为了删除一个节点(比如,id=5),我们需要先删除他们的关系,然后才可以删除节点。

+
1
2
3
MATCH (n)-[r]-()
WHERE id(n) = 5
DELETE r, n
+

2.3+ 之后的简便写法:

+
1
2
3
MATCH (n)
WHERE id(n) = 5
DETACH DELETE n
+

删除指定节点的属性

1
2
3
MATCH (n)
WHERE n:Person AND n.name = "Rik" AND n.plays is NOT null
REMOVE n.plays
+

或者

+
1
2
3
MATCH (n)
WHERE n:Person AND n.name = "Rik" AND EXISTS (n.plays)
REMOVE n.plays
+

删除多个节点

1
2
3
MATCH (n)
WHERE id(n) IN [1, 2, 3]
DELETE n
+

删除全部节点上的标签

从全部节点上删除 :Person 标签

+
1
2
MATCH (n)
REMOVE n:Person
+

删除具有特定标签节点上的标签

从带有 :Food:Person 标签的节点中删除 :Person 标签

+
1
2
3
MATCH (n)
WHERE n:Food:Person
REMOVE n:Person
+

删除节点中的多个标签

从带有 :Food:Person 标签的节点中删除这两标签

+
1
2
3
MATCH (n)
WHERE n:Food:Person
REMOVE n:Food:Person
+

删除全部数据

1
2
3
MATCH (n)
OPTIONAL MATCH (n)-[r]-()
DELETE n, r
+

2.3+ 之后的简便写法:

+
1
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.

+

Count

全部节点数量

+
1
2
MATCH (n)
RETURN count(n)
+

全部关系数量

+
1
2
MATCH ()-->()
RETURN count(*);
+

Limit

最多返回 2 个 from 属性值为 Sweden 的节点(及其关系)

+
1
2
3
4
MATCH (a:Person)
WHERE a.from = "Sweden"
RETURN a
LIMIT 2
+

创建唯一属性约束

使带有 Person 标签节点的 name 属性值唯一

+
1
2
CREATE CONSTRAINT ON (n:Person)
ASSERT n.name IS UNIQUE
+

删除唯一属性约束

1
2
DROP CONSTRAINT ON (n:Person)
ASSERT n.name IS UNIQUE
+ +
+ + + + + + + +
+ + + + + + +
+ +
+ + + +
+ + + + + + + + +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2020/SOLID-Design-Principles/0.jpeg b/2020/SOLID-Design-Principles/0.jpeg new file mode 100644 index 0000000000..0e0b84e53d Binary files /dev/null and b/2020/SOLID-Design-Principles/0.jpeg differ diff --git a/2020/SOLID-Design-Principles/1.png b/2020/SOLID-Design-Principles/1.png new file mode 100644 index 0000000000..db272db5e4 Binary files /dev/null and b/2020/SOLID-Design-Principles/1.png differ diff --git a/2020/SOLID-Design-Principles/10.png b/2020/SOLID-Design-Principles/10.png new file mode 100644 index 0000000000..a8a47112a3 Binary files /dev/null and b/2020/SOLID-Design-Principles/10.png differ diff --git a/2020/SOLID-Design-Principles/2.png b/2020/SOLID-Design-Principles/2.png new file mode 100644 index 0000000000..a77b9e90b8 Binary files /dev/null and b/2020/SOLID-Design-Principles/2.png differ diff --git a/2020/SOLID-Design-Principles/3.png b/2020/SOLID-Design-Principles/3.png new file mode 100644 index 0000000000..0637bf9be2 Binary files /dev/null and b/2020/SOLID-Design-Principles/3.png differ diff --git a/2020/SOLID-Design-Principles/4.png b/2020/SOLID-Design-Principles/4.png new file mode 100644 index 0000000000..6676724943 Binary files /dev/null and b/2020/SOLID-Design-Principles/4.png differ diff --git a/2020/SOLID-Design-Principles/5.png b/2020/SOLID-Design-Principles/5.png new file mode 100644 index 0000000000..8d196defd4 Binary files /dev/null and b/2020/SOLID-Design-Principles/5.png differ diff --git a/2020/SOLID-Design-Principles/6.png b/2020/SOLID-Design-Principles/6.png new file mode 100644 index 0000000000..e176eb0202 Binary files /dev/null and b/2020/SOLID-Design-Principles/6.png differ diff --git a/2020/SOLID-Design-Principles/7.png b/2020/SOLID-Design-Principles/7.png new file mode 100644 index 0000000000..37590c1141 Binary files /dev/null and b/2020/SOLID-Design-Principles/7.png differ diff --git a/2020/SOLID-Design-Principles/8.jpg b/2020/SOLID-Design-Principles/8.jpg new file mode 100644 index 0000000000..4409c3e827 Binary files /dev/null and b/2020/SOLID-Design-Principles/8.jpg differ diff --git a/2020/SOLID-Design-Principles/9.png b/2020/SOLID-Design-Principles/9.png new file mode 100644 index 0000000000..cae519d896 Binary files /dev/null and b/2020/SOLID-Design-Principles/9.png differ diff --git a/2020/SOLID-Design-Principles/index.html b/2020/SOLID-Design-Principles/index.html new file mode 100644 index 0000000000..8ffc10446e --- /dev/null +++ b/2020/SOLID-Design-Principles/index.html @@ -0,0 +1,819 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + SOLID:面向对象设计的五个基本原则 | 贾攀的流水账 + + + + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

贾攀的流水账

+ +
+

Panmax's Blog

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+ + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ SOLID:面向对象设计的五个基本原则 +

+ + +
+ + + + +
+ + +

+

先来看下维基百科对 SOLID 的介绍:

+
+

在程序设计领域,SOLID 是由罗伯特·C·马丁在 21 世纪早期引入的记忆术首字母缩略字,指代了面向对象编程和面向对象设计的五个基本原则。当这些原则被一起应用时,它们使得一个程序员开发一个容易进行软件维护和扩展的系统变得更加可能。

+
+

SOLID 是以下五个单词的缩写:

+
    +
  • Single Responsibility Principle(单一职责原则)
  • +
  • Open Closed Principle(开闭原则)
  • +
  • Liskov Substitution Principle(里氏替换原则)
  • +
  • Interface Segregation Principle(接口隔离原则)
  • +
  • Dependency Inversion Principle(依赖倒置原则)
  • +
+

单一职责原则

单一职责原则的英文是 Single Responsibility Principle,缩写为 SRP

+

可以从两个角度来理解单一职责原则:

+
    +
  1. 一个类或者模块只负责完成一个职责(或者功能)。
  2. +
  3. 一个类,应该只有一个引起它变化的原因。
  4. +
+

对于这两种理解方式,我分别举例来说明。

+

一个类或者模块只负责完成一个职责(或者功能)

这里的模块可以看作比类更加粗粒度的代码块,模块中包含多个类,多个类组成一个模块。

+

来看下边的代码:

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class UserInfo {
private long userId;
private String username;
private String email;
private String telephone;
private long createTime;
private long lastLoginTime;
private String avatarUrl;
private String provinceOfAddress; // 省
private String cityOfAddress; // 市
private String regionOfAddress; // 区
private String detailedAddress; // 详细地址
// ...省略其他属性和方法...
}
+

站在不同的应用场景不同阶段的需求背景下,对 UserInfo 类的职责是否单一的判定,可能都是不一样的:

+
    +
  • 如果在这个社交产品中,用户的地址信息跟其他信息一样,只是单纯地用来展示,那 UserInfo 现在的设计就是合理的。
  • +
  • 如果这个社交产品发展得比较好,之后又在产品中添加了电商的模块,用户的地址信息还会用在电商物流中,那我们最好将地址信息从 UserInfo 中拆分出来,独立成用户物流信息(或者叫地址信息、收货信息等)。
  • +
  • 如果做这个社交产品的公司发展得越来越好,公司内部又开发出了跟多其他产品(可以理解为其他 App)。公司希望支持统一账号系统,也就是用户一个账号可以在公司内部的所有产品中登录。这个时候,我们就需要继续对 UserInfo 进行拆分,将跟身份认证相关的信息(比如,email、telephone 等)抽取成独立的类。
  • +
+
+

在某种应用场景或者当下的需求背景下,一个类的设计可能已经满足单一职责原则了,但如果换个应用场景或着在未来的某个需求背景下,可能就不满足了,需要继续拆分成粒度更细的类。

+
+

一个类,应该只有一个引起它变化的原因

我们这里以一个矩形类 Rectangle 为例,如图所示:

+

+

Rectangle 有两个方法:

+
    +
  • 绘图方法 draw()
  • +
  • 计算面积方法 area()
  • +
+

现在有两个应用程序要依赖这个 Rectangle 类:

+
    +
  1. 几何计算应用程序:只需要计算面积,不需要绘图。
  2. +
  3. 图形界面应用程序:绘图的时候,程序需要计算面积。
  4. +
+

在计算机屏幕上绘图是一件非常麻烦的事情,所以对于绘图这个需求来说,需要依赖专门的 GUI 库。一个 GUI 库可能有几十 M 甚至数百 M。

+

本来几何计算程序作为一个纯科学计算程序,主要是一些数学计算代码,现在程序打包完,却不得不把一个不相关的 GUI 库也打包进来。本来程序包可能只有几百 K,现在变成了几百 M。

+

当图形界面应用程序不得不修改 Rectangle 类的时候,还得重新编译几何计算应用程序,反之亦然。这个情况下,我们就可以说 Rectangle 类有两个引起它变化的原因。

+

当然,这里用前一种理解也是可以的(一个类或者模块只负责完成一个职责):Rectangle承担了两个职责,一个是几何形状的计算,一个是在屏幕上绘制图形。

+

我们可以将 Rectangle 拆分成两个类:

+
    +
  1. GeometricRectangle: 这个类负责实现图形面积计算方法 area()
  2. +
  3. Rectangle:只保留单一绘图方法 draw()
  4. +
+

现在绘制长方形的时候可以使用计算面积的方法,而几何计算应用程序则不需要依赖一个不相关的绘图方法以及一大堆的 GUI 组件。

+

拆分后的类图如下所示:

+

+

从 Web 应用架构演进看单以职责原则

从事过 Java Web 开发的老码农都经历过下边这 3 个开发阶段。

+

阶段 1:请求处理以及响应的全部操作都在 Servlet 里,Servlet 获取请求数据,进行逻辑处理,访问数据库,得到处理结果,根据处理结果构造返回的 HTML

+

+

阶段 2:于是后来就有了 JSP,如果说 Servlet 是在程序中输出 HTML,那么 JSP 就是在 HTML 中调用程序。

+

+

这个阶段,基于 JSP 开发的 Web 程序在职责上进行了一些最基本的分离:构造页面的 JSP 和处理逻辑的业务模型分离。

+

阶段 3:各种 MVC 框架的出现,MVC 框架通过控制器将视图与模型彻底分离。

+

+

有了 MVC,就可以顺理成章地将复杂的业务模型进行分层了。通过分层方式,将业务模型分为业务层、服务层、数据持久层,使各层职责进一步分离,更符合单一职责原则。

+

+
+

也是因为 MVC 框架的出现,才使得前后端开发成为两个不同的工种,前端工程师只做视图模板开发,后端工程师只做业务开发,彼此之间没有直接的依赖和耦合,各自独立开发、维护自己的代码。

+
+

如何判断一个类是否满足单一职责?

前边提到过,不同的应用场景、不同阶段的需求背景、不同的业务层面,对同一个类的职责是否单一,可能会有不同的判定结果。我们可以通过一些侧面指标来指导我们的判断。

+

出现下面这些情况就有可能说明类的设计不满足单一职责原则:

+
    +
  • 类中的代码行数、函数或者属性过多
  • +
  • 依赖的其他类过多,或者依赖此类的其他类过多
  • +
  • 私有方法过多
  • +
  • 比较难给类起一个合适的名字
  • +
  • 类中大量的方法都是集中操作类中的某几个属性
  • +
+

开闭原则

+

开闭原则是所有设计原则中最有用的,因为扩展性是代码质量最重要的衡量标准之一。在 23 种经典设计模式中,大部分设计模式都是为了解决代码的扩展性问题而存在的,主要遵从的设计原则就是开闭原则

+
+

开闭原则的英文是 Open Closed Principle,缩写为 OCP

+

开闭原则说的是:软件实体(模块、类、函数等等)应该对扩展是开放的,对修改是关闭的。

+
    +
  • 对扩展是开放的,意味着软件实体的行为是可扩展的,当需求变更的时候,可以对模块进行扩展,使其满足需求变更的要求。
  • +
  • 对修改是关闭的,意味着当对软件实体进行扩展的时候,不需要改动当前的软件实体;不需要修改代码;对于已经完成的类文件不需要重新编辑;对于已经编译打包好的模块,不需要再重新编译。
  • +
+

两者结合起来表述为:添加一个新的功能应该是,在已有代码基础上扩展代码(新增模块、类、方法等),而非修改已有代码(修改模块、类、方法等)。

+

举例说明开闭原则

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class Alert {
private AlertRule rule;
private Notification notification;

public Alert(AlertRule rule, Notification notification) {
this.rule = rule;
this.notification = notification;
}

public void check(String api, long requestCount, long errorCount,
long durationOfSeconds) {
long tps = requestCount / durationOfSeconds;

if (tps > rule.getMatchedRule(api).getMaxTps()) {
notification.notify(NotificationEmergencyLevel.URGENCY, "...");
}

if (errorCount > rule.getMatchedRule(api).getMaxErrorCount()) {
notification.notify(NotificationEmergencyLevel.SEVERE, "...");
}
}
}
+

以上代码中的 AlertRule 存储告警规则,可以自由设置。

+

Notification 是告警通知类,支持邮件、短信、微信、手机等多种通知渠道。

+

NotificationEmergencyLevel 表示通知的紧急程度,不同的紧急程度对应不同的发送渠道。

+

业务逻辑主要集中在 check() 函数中:当接口的 TPS 超过某个预先设置的最大值时,以及当接口请求出错数大于某个最大允许值时,就会触发告警。

+

现在,如果我们需要添加一个功能,当每秒钟接口超时请求个数,超过某个预先设置的最大阈值时,我们也要触发告警发送通知。

+

不遵循开闭原则的修改

主要的改动有两处:

+
    +
  1. 修改 check() 函数的入参,添加一个新的统计数据 timeoutCount,表示超时接口请求数
  2. +
  3. check() 函数中添加新的告警逻辑
  4. +
+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

public class Alert {
// ...省略AlertRule/Notification属性和构造函数...

// 改动一:添加参数timeoutCount
public void check(String api, long requestCount, long errorCount, long timeoutCount, long durationOfSeconds) {
long tps = requestCount / durationOfSeconds;
if (tps > rule.getMatchedRule(api).getMaxTps()) {
notification.notify(NotificationEmergencyLevel.URGENCY, "...");
}
if (errorCount > rule.getMatchedRule(api).getMaxErrorCount()) {
notification.notify(NotificationEmergencyLevel.SEVERE, "...");
}
// 改动二:添加接口超时处理逻辑
long timeoutTps = timeoutCount / durationOfSeconds;
if (timeoutTps > rule.getMatchedRule(api).getMaxTimeoutTps()) {
notification.notify(NotificationEmergencyLevel.URGENCY, "...");
}
}
}
+

如此进行的代码修改导致将导致以下问题:

+
    +
  1. 调用这个接口的代码都要做相应的修改
  2. +
  3. 修改了 check() 函数,相应的单元测试都需要修改
  4. +
+

粗暴一点说,当我们在代码中看到 if/else 或者 switch/case 关键字的时候,基本可以判断违反开闭原则了。

+

遵循开闭原则的修改

重构一下之前的 Alert 代码,让它的扩展性更好一些:

+
    +
  1. check() 函数的多个入参封装成 ApiStatInfo
  2. +
  3. 引入 handler 的概念,将 if 判断逻辑分散在各个 handler
  4. +
+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
public class Alert {
private List<AlertHandler> alertHandlers = new ArrayList<>();
public void addAlertHandler(AlertHandler alertHandler) {
this.alertHandlers.add(alertHandler);
}
public void check(ApiStatInfo apiStatInfo) {
for (AlertHandler handler : alertHandlers) {
handler.check(apiStatInfo);
}
}
}
public class ApiStatInfo {
//省略constructor/getter/setter方法
private String api;
private long requestCount;
private long errorCount;
private long durationOfSeconds;
}
public abstract class AlertHandler {
protected AlertRule rule;
protected Notification notification;
public AlertHandler(AlertRule rule, Notification notification) {
this.rule = rule;
this.notification = notification;
}
public abstract void check(ApiStatInfo apiStatInfo);
}
public class TpsAlertHandler extends AlertHandler {
public TpsAlertHandler(AlertRule rule, Notification notification) {
super(rule, notification);
}
@Override
public void check(ApiStatInfo apiStatInfo) {
long tps = apiStatInfo.getRequestCount()/ apiStatInfo.getDurationOfSeconds();
if (tps > rule.getMatchedRule(apiStatInfo.getApi()).getMaxTps()) {
notification.notify(NotificationEmergencyLevel.URGENCY, "...");
}
}
}
public class ErrorAlertHandler extends AlertHandler {
public ErrorAlertHandler(AlertRule rule, Notification notification){
super(rule, notification);
}
@Override
public void check(ApiStatInfo apiStatInfo) {
if (apiStatInfo.getErrorCount() > rule.getMatchedRule(apiStatInfo.getApi()).getMaxErrorCount()) {
notification.notify(NotificationEmergencyLevel.SEVERE, "...");
}
}
}
+

现在我们基于重构之后的代码来实现每秒钟接口超时请求个数超过某个最大阈值就告警的新功能就方便多了。

+

不考虑调用方修改的情况下,实现方只需进行两处的改动:

+
    +
  1. ApiStatInfo 类中添加新的属性 timeoutCount
  2. +
  3. 添加新的 TimeoutAlertHander
  4. +
+

调用方的改动也很简单:

+
    +
  1. TimeoutAlertHander 类的实例注册到 alert 对象中
  2. +
  3. apiStatInfo 对象 设置 timeoutCOunt 的值。
  4. +
+

修改后代码如下:

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
public class Alert { // 代码未改动... }
public class ApiStatInfo {//省略constructor/getter/setter方法
private String api;
private long requestCount;
private long errorCount;
private long durationOfSeconds;
private long timeoutCount; // 改动一:添加新字段
}
public abstract class AlertHandler { //代码未改动... }
public class TpsAlertHandler extends AlertHandler {//代码未改动...}
public class ErrorAlertHandler extends AlertHandler {//代码未改动...}
// 改动二:添加新的handler
public class TimeoutAlertHandler extends AlertHandler {//省略代码...}

public class ApplicationContext {
private AlertRule alertRule;
private Notification notification;
private Alert alert;

public void initializeBeans() {
alertRule = new AlertRule(/*.省略参数.*/); //省略一些初始化代码
notification = new Notification(/*.省略参数.*/); //省略一些初始化代码
alert = new Alert();
alert.addAlertHandler(new TpsAlertHandler(alertRule, notification));
alert.addAlertHandler(new ErrorAlertHandler(alertRule, notification));
// 改动三:注册handler
alert.addAlertHandler(new TimeoutAlertHandler(alertRule, notification));
}
//...省略其他未改动代码...
}

public class Demo {
public static void main(String[] args) {
ApiStatInfo apiStatInfo = new ApiStatInfo();
// ...省略apiStatInfo的set字段代码
apiStatInfo.setTimeoutCount(289); // 改动四:设置tiemoutCount值
ApplicationContext.getInstance().getAlert().check(apiStatInfo);
}
+

重构之后的代码更加灵活和易扩展。如果我们要想添加新的告警逻辑,只需要基于扩展的方式创建新的 handler 类即可,不需要改动原来的 check() 函数的逻辑。而且,我们只需要为新的 handler 类添加单元测试,老的单元测试都不会失败,也不用修改。

+

开闭原则的设计初衷是:只要它没有破坏原有的代码的正常运行,没有破坏原有的单元测试,我们就可以说,这是一个合格的代码改动。

+

通过上边举过的例子可以看出:添加一个新功能,不可能任何模块、类、方法的代码都不「修改」,这个是做不到的。我们要做的是尽量让修改操作更集中、更少、更上层,尽量让最核心、最复杂的那部分逻辑代码满足开闭原则。

+

同样一个代码改动,在粗代码粒度下,被认定为「修改」,在细代码粒度下,又可以被认定为「扩展」。

+
    +
  • 比如,改动一,添加属性和方法相当于修改类,在类这个层面,这个代码改动可以被认定为「修改」;
  • +
  • 代码改动并没有修改已有的属性和方法,在方法(及其属性)这一层面,它又可以被认定为「扩展」。
  • +
+

如何做到「对扩展开放、修改关闭」?

站在「」的角度:

为了尽量写出扩展性好的代码,我们要时刻具备扩展意识、抽象意识、封装意识。这些「潜意识」可能比任何开发技巧都重要。

+

站在「」的角度:

最常用来提高代码扩展性的方法有:多态、依赖注入、基于接口而非实现编程,以及大部分的设计模式(比如,装饰、策略、模板、职责链、状态)。

+

来看一个遵循开闭原则的例子:

结合了多态、依赖注入、基于接口而非实现通过 Kafka 来发送异步消息。

+
    +
  • 我们抽象了一组跟具体消息队列(Kafka)无关的异步消息接口
  • +
  • 所有上层系统都依赖这组抽象的接口编程,并且通过依赖注入的方式来调用
  • +
  • 当我们要替换新的消息队列的时候,比如将 Kafka 替换成 RocketMQ,可以很方便地拔掉老的消息队列实现,插入新的消息队列实现。
  • +
+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

// 这一部分体现了抽象意识
public interface MessageQueue { //... }
public class KafkaMessageQueue implements MessageQueue { //... }
public class RocketMQMessageQueue implements MessageQueue {//...}

public interface MessageFormatter { //... }
public class JsonMessageFormatter implements MessageFormatter {//...}
public class MessageFormatter implements MessageFormatter {//...}

public class Demo {
private MessageQueue msgQueue; // 基于接口而非实现编程
public Demo(MessageQueue msgQueue) { // 依赖注入
this.msgQueue = msgQueue;
}

// msgFormatter:多态、依赖注入
public void sendNotification(Notification notification, MessageFormatter msgFormatter) {
//...
}
}
+

实现开闭原则的关键是抽象。当一个模块依赖的是一个抽象接口的时候,就可以随意对这个抽象接口进行扩展,这个时候,不需要对现有代码进行任何修改,利用接口的多态性,通过增加一个新实现该接口的实现类,就能完成需求变更。

+

开闭原则可以说是软件设计原则的原则,是软件设计的核心原则,其他的设计原则更偏向技术性,具有技术性的指导意义,而开闭原则是方向性的,在软件设计的过程中,应该时刻以开闭原则指导、审视自己的设计:当需求变更的时候,现在的设计能否不修改代码就可以实现功能的扩展?如果不是,那么就应该进一步使用其他的设计原则和设计模式去重新设计。

+

如何在项目中灵活应用开闭原则?

写出支持「对扩展开放、对修改关闭」的代码的关键是预留扩展点:

+
    +
  • 对于一些比较确定的、短期内可能就会扩展,或者需求改动对代码结构影响比较大的情况,或者实现成本不高的扩展点,在编写代码的时候,我们就可以事先做些扩展性设计。
  • +
  • 反之,对于一些不确定未来是否要支持的需求,或者实现起来比较复杂的扩展点,我们可以等到有需求驱动的时候,再通过重构代码的方式来支持扩展的需求。
  • +
+
+

话外音:这种技术视野的前提是需要在某个领域进行深耕。

+
+

最后提醒一下,天下没有免费的午餐,有些情况下,代码的扩展性会跟可读性相冲突。很多时候,我们都需要在扩展性可读性之间做权衡

+

里氏替换原则

单一职责原则的英文是 Liskov Substitution Principle,缩写为 LSP

+

官方一些的介绍:子类对象能够替换程序中父类对象出现的任何地方,并且保证原来程序的逻辑行为不变及正确性不被破坏。

+

通俗地说就是:子类型必须能够替换掉它们的基类型。

+

通俗地详细点说:程序中,所有使用基类的地方,都应该可以用子类代替。

+

里氏替换原则示例 1

如下代码中,父类 Transporter 使用 org.apache.http 库中的 HttpClient 类来传输网络数据。子类 SecurityTransporter 继承父类 Transporter,增加了额外的功能,支持传输 appIdappToken 安全认证信息。

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
public class Transporter {
private HttpClient httpClient;

public Transporter(HttpClient httpClient) {
this.httpClient = httpClient;
}

public Response sendRequest(Request request) {
// ...use httpClient to send request
}
}

public class SecurityTransporter extends Transporter {
private String appId;
private String appToken;

public SecurityTransporter(HttpClient httpClient, String appId, String appToken) {
super(httpClient);
this.appId = appId;
this.appToken = appToken;
}

@Override
public Response sendRequest(Request request) {
if (StringUtils.isNotBlank(appId) && StringUtils.isNotBlank(appToken)) {
request.addPayload("app-id", appId);
request.addPayload("app-token", appToken);
}
return super.sendRequest(request);
}
}

public class Demo {
public void demoFunction(Transporter transporter) {
Reuqest request = new Request();
//...省略设置request中数据值的代码...
Response response = transporter.sendRequest(request);
//...省略其他逻辑...
}
}

// 里式替换原则
Demo demo = new Demo();
demo.demofunction(new SecurityTransporter(/*省略参数*/););
+

子类 SecurityTransporter 的设计完全符合里式替换原则,可以替换父类出现的任何位置,并且原来代码的逻辑行为不变且正确性也没有被破坏。

+

这样看来里氏替换原则不就是简单利用了多态的特性吗?我们通过一个反例来看下这两者的区别:

+
+

通俗地说,接口(抽象类)的多个实现就是多态。多态可以让程序在编程时面向接口进行编程,在运行期绑定具体类,从而使得类之间不需要直接耦合,就可以关联组合,构成一个更强大的整体对外服务。

+
+

我们对刚刚那个例子中 SecurityTransporter 类的 sendRequest() 方法稍加改造一下。

+
    +
  • 改造前,如果 appId 或者 appToken 没有设置,我们就不做校验;
  • +
  • 改造后,如果 appId 或者 appToken 没有设置,则直接抛出 NoAuthorizationRuntimeException 未授权异常。
  • +
+
1
2
3
4
5
6
7
8
9
10
11
12
public class SecurityTransporter extends Transporter {
//...省略其他代码..
@Override
public Response sendRequest(Request request) {
if (StringUtils.isBlank(appId) || StringUtils.isBlank(appToken)) {
throw new NoAuthorizationRuntimeException(...);
}
request.addPayload("app-id", appId);
request.addPayload("app-token", appToken);
return super.sendRequest(request);
}
}
+

改造之后的代码仍然可以通过 Java 的多态语法,动态地用子类 SecurityTransporter 来替换父类 Transporter,也并不会导致程序编译或者运行报错。但是,从设计思路上来讲,SecurityTransporter 的设计是不符合里式替换原则的。

+

虽然从定义描述和代码实现上来看,多态和里式替换有点类似,但它们关注的角度是不一样的。

+
    +
  • 多态是面向对象编程的一大特性,也是面向对象编程语言的一种语法。它是一种代码实现的思路
  • +
  • 里式替换是一种设计原则,是用来指导继承关系中子类该如何设计的,子类的设计要保证在替换父类的时候,不改变原有程序的逻辑以及不破坏原有程序的正确性。
  • +
+

所以,判断子类的设计实现是否违背里式替换原则,还有一个小窍门:那就是拿父类的单元测试去验证子类的代码。如果某些单元测试运行失败,就有可能说明,子类的设计实现没有完全地遵守父类的约定,子类有可能违背了里式替换原则。

+

里氏替换原则示例 2

我们来看个违反历史替换原则的例子:

+

CircleSquare 继承了基类 Shape,然后在应用的方法中,根据输入 Shape 对象类型进行判断,根据对象类型选择不同的绘图函数将图形画出来。

+
1
2
3
4
5
6
7
8
9
void drawShape(Shape shape) {
if (shape.type == Shape.Circle ) {
drawCircle((Circle) shape);
} else if (shape.type == Shape.Square) {
drawSquare((Square) shape);
} else {
……
}
}
+

这种写法的代码既常见又糟糕,它同时违反了开闭原则和里氏替换原则。

+
    +
  • 首先看到这样的 if/else 代码,就可以判断违反了(我们刚刚在上个部分讲过的)开闭原则:当增加新的 Shape 类型的时候,必须修改这个方法,增加 else if 代码。
  • +
  • 其次也因为同样的原因违反了里氏替换原则:当增加新的Shape 类型的时候,如果没有修改这个方法,没有增加 else if 代码,那么这个新类型就无法替换基类 Shape
  • +
+

要解决这个问题其实也很简单,只需要在基类 Shape 中定义 draw 方法,所有 Shape 的子类,CircleSquare 都实现这个方法就可以了:

+
1
2
3
public abstract Shape{
public abstract void draw();
}
+

上面那段 drawShape() 代码也就可以变得更简单:

+
1
2
3
void drawShape(Shape shape) {
shape.draw();
}
+

这段代码既满足开闭原则:增加新的类型不需要修改任何代码。也满足里氏替换原则:在使用基类的这个方法中,可以用子类替换,程序正常运行。

+

如何在实践中遵循里氏替换原则

子类在设计的时候,要遵守父类的行为约定(或者叫协议)。父类定义了函数的行为约定,那子类可以改变函数的内部实现逻辑,但不能改变函数原有的行为约定,这也是我们常说的「Design By Contract」,中文翻译就是「按照协议(契约、约定)来设计」。

+

以下是三种常见的违背约定的情况:

+
    +
  1. 子类违背父类声明要实现的功能
      +
    • 如:父类中提供的 sortOrdersByAmount() 订单排序函数,是按照金额从小到大来给订单排序的,而子类重写这个 sortOrdersByAmount() 订单排序函数之后,是按照创建日期来给订单排序的。那子类的设计就违背里式替换原则。
    • +
    +
  2. +
  3. 子类违背父类对输入、输出、异常的约定
      +
    • 如:在父类中,某个函数约定:运行出错的时候返回 null;获取数据为空的时候返回空集合(empty collection)。而子类重载函数之后,实现变了,运行出错返回异常(exception),获取不到数据返回 null
    • +
    • 在父类中,某个函数约定,输入数据可以是任意整数,但子类实现的时候,只允许输入数据是正整数,负数就抛出异常,也就是说,子类对输入的数据的校验比父类更加严格,那子类的设计就违背了里式替换原则。
    • +
    • 在父类中,某个函数约定,只会抛出 ArgumentNullException 异常,那子类的设计实现中只允许抛出 ArgumentNullException 异常,任何其他异常的抛出,都会导致子类违背里式替换原则。
    • +
    +
  4. +
  5. 子类违背父类注释中所罗列的任何特殊说明
      +
    • 如:父类中定义的 withdraw() 提现函数的注释是这么写的:「用户的提现金额不得超过账户余额……」,而子类重写 withdraw() 函数之后,针对 VIP 账号实现了透支提现的功能,也就是提现金额可以大于账户余额
    • +
    +
  6. +
+

子类的协议不能比父类更严格,否则使用者在用子类替换父类的时候,就会因为更严格的协议而失败。

+

在类的继承中,如果父类方法的访问控制是 protected,那么子类 override 这个方法的时候,可以改成是 public,但是不能改成 private。因为 private 的访问控制比 protected 更严格,能使用父类 protected 方法的地方,不能用子类的 private 方法替换,否则就是违反里氏替换原则的。相反,如果子类方法的访问控制改成 public 就没问题,即子类可以有比父类更宽松的协议。同样,子类 override 父类方法的时候,不能将父类的 public 方法改成 protected,否则会出现编译错误。

+

实践中,当你继承一个父类仅仅是为了复用父类中的方法的时候,那么很有可能你离错误的继承已经不远了。一个类如果不是为了被继承而设计,那么最好就不要继承它。

+

粗暴一点地说,如果不是抽象类或者接口,最好不要继承它

+

如果你确实需要使用一个类的方法,最好的办法是组合这个类而不是继承这个类,这就是人们通常说的组合优于继承

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
Class A{
public Element query(int id){...}
public void modify(Element e){...}
}

Class B{
private A a;
public Element select(int id){
a.query(id);
}
public void modify(Element e){
a.modify(e);
}
}
+

接口隔离原则

接口隔离原则的英文是 SInterface Segregation Principle,缩写为 ISP

+

这个原则是说:客户端不应该强迫依赖它不需要的接口

+

我们可以从三个角度理解「接口」:

+
    +
  • 一组 API 接口集合
  • +
  • 单个 API 接口或函数
  • +
  • OOP 中的接口概念
  • +
+

下面我们逐个进行说明。

+

把「接口」理解为一组 API 接口集合

在设计微服务或者类库接口的时候,如果部分接口只被部分调用者使用,那我们就需要将这部分接口隔离出来,单独给对应的调用者使用,而不是强迫其他调用者也依赖这部分不会被用到的接口。

+

举例说明:

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public interface UserService {
boolean register(String cellphone, String password);
boolean login(String cellphone, String password);
UserInfo getUserInfoById(long id);
UserInfo getUserInfoByCellphone(String cellphone);
}

public interface RestrictedUserService {
boolean deleteUserByCellphone(String cellphone);
boolean deleteUserById(long id);
}

public class UserServiceImpl implements UserService, RestrictedUserService {
// ...省略实现代码...
}
+

删除用户是一个非常慎重的操作,我们只希望通过后台管理系统来执行,所以这个接口只限于给后台管理系统使用。如果我们把它放到 UserService 中,那所有使用到 UserService 的系统,都可以调用这个接口。不加限制地被其他业务系统调用,就有可能导致误删用户。

+

参照接口隔离原则,调用者不应该强迫依赖它不需要的接口,将删除接口单独放到另外一个接口 RestrictedUserService 中,然后将 RestrictedUserService 只打包提供给后台管理系统来使用。

+

把「接口」理解为单个 API 接口或函数

隔离原则就可以理解为:函数的设计要功能单一,不要将多个不同的功能逻辑在一个函数中实现。

+

接口隔离原则跟单一职责原则有点类似,不过稍微还是有点区别。

+
    +
  • 单一职责原则针对的是模块、类、接口的设计
  • +
  • 接口隔离原则相对于单一职责原则,一方面它更侧重于接口的设计,另一方面它的思考的角度不同
  • +
+

接口隔离原则提供了一种判断接口是否职责单一的标准:通过调用者如何使用接口来间接地判定。如果调用者只使用部分接口或接口的部分功能,那接口的设计就不够职责单一。

+

举例说明:

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class Statistics {
private Long max;
private Long min;
private Long average;
private Long sum;
private Long percentile99;
private Long percentile999;
//...省略constructor/getter/setter等方法...
}

public Statistics count(Collection<Long> dataSet) {
Statistics statistics = new Statistics();
//...省略计算逻辑...
return statistics;
}
+

在上面的代码中,count() 函数的功能不够单一,包含很多不同的统计功能,比如,求最大值、最小值、平均值等等。如果某个统计需求只涉及 Statistics 罗列的统计信息中一部分,而 count() 函数每次都会把所有的统计信息计算一遍,就会做很多无用功,势必影响代码的性能

+

按照接口隔离原则,我们应该把 count() 函数拆成几个更小粒度的函数,每个函数负责一个独立的统计功能:

+
1
2
3
4
public Long max(Collection<Long> dataSet) { //... }
public Long min(Collection<Long> dataSet) { //... }
public Long average(Colletion<Long> dataSet) { //... }
// ...省略其他统计函数...
+

把「接口」理解为 OOP 中的接口概念

接口的设计要尽量单一,不要让接口的实现类和调用者,依赖不需要的接口函数或方法。

+

使用接口隔离原则,就是定义多个接口,不同调用者依赖不同的接口,只看到自己需要的方法。而实现类则实现这些接口,通过多个接口将类内部不同的方法隔离开来。

+

那么如果强迫用户依赖他们不需要的方法,会导致什么后果呢?

+
    +
  • 一来,用户可以看到这些他们不需要,也不理解的方法,这样无疑会增加他们使用的难度,如果错误地调用了这些方法,就会产生 bug。
  • +
  • 二来,当这些方法如果因为某种原因需要更改的时候,虽然不需要但是依赖这些方法的用户程序也必须做出更改,这是一种不必要的耦合。
  • +
+

举例说明:

+

把「接口」理解为 OOP 中的接口概念

假如我们需要开发一个支持根据远程配置中心配置来动态更改缓存配置的缓存服务。

+

+

这个缓存服务 Client 类的方法主要包含两个部分:

+
    +
  • 一部分是缓存服务方法,get()put()delete() 这些,这些方法是面向调用者的
  • +
  • 另一部分是配置更新方法 reBuild(),这个方法主要是给远程配置中心调用的
  • +
+

但是问题是,Cache 类的调用者如果看到 reBuild() 方法,并错误地调用了该方法,就可能导致 Cache 连接被错误重置,导致无法正常使用 Cache 服务。所以必须要将 reBuild() 方法向缓存服务的调用者隐藏,而只对远程配置中心的本地代理开放这个方法。

+

我们可以进行如下调整:

+

实现类同时实现 Cache 接口和 CacheManageable 接口,其中 Cache 接口提供标准的 Cache 服务方法,应用程序只需要依赖该接口。而 CacheManageable 接口则对外暴露 reBuild() 方法。

+

+

使用接口隔离原则,就是定义多个接口,不同调用者依赖不同的接口,只看到自己需要的方法。而实现类则实现这些接口,通过多个接口将类内部不同的方法隔离开来。

+

依赖倒置原则

+

单一职责原则和开闭原则的原理比较简单,但是,想要在实践中用好却比较难。而依赖倒置原则正好相反。依赖倒置原则用起来比较简单,但概念理解起来比较难。

+
+

依赖倒置原则的英文是 Dependency Inversion Principle,缩写为 DIP

+

依赖倒置原则说的是:高层模块不依赖低层模块,它们共同依赖同一个抽象,这个抽象接口通常是由高层模块定义,低层模块实现。同时抽象不要依赖具体实现细节,具体实现细节依赖抽象。

+

所谓高层模块和低层模块的划分,简单来说就是,在调用链上,调用者属于高层,被调用者属于低层

+

在具体讲解依赖倒置原则前,我们先来看几个与之有关的常见概念:控制反转、依赖注入、依赖注入框架。

+

控制反转(IOC)

控制反转是一个比较笼统的设计思想,并不是一种具体的实现方法,一般用来指导框架层面的设计

+

框架提供了一个可扩展的代码骨架,用来组装对象、管理整个执行流程。程序员利用框架进行开发的时候,只需要往预留的扩展点上,添加跟自己业务相关的代码,就可以利用框架来驱动整个程序流程的执行。

+

这里的「控制」指的是对程序执行流程的控制,而「反转」指的是在没有使用框架之前,程序员自己控制整个程序的执行。在使用框架之后,整个程序的执行流程可以通过框架来控制。流程的控制权从程序员「反转」到了框架。

+

我们举个例子来看一下:

+
1
2
3
4
5
6
7
8
9
10
11
12
13
public class UserServiceTest {
public static boolean doTest() {
// ...
}

public static void main(String[] args) {//这部分逻辑可以放到框架中
if (doTest()) {
System.out.println("Test succeed.");
} else {
System.out.println("Test failed.");
}
}
}
+

在上面的代码中,所有的流程都由程序员来控制。如果我们抽象出一个下面这样一个框架,我们再来看,如何利用框架来实现同样的功能。

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public abstract class TestCase {
public void run() {
if (doTest()) {
System.out.println("Test succeed.");
} else {
System.out.println("Test failed.");
}
}

public abstract boolean doTest();
}

public class JunitApplication {
private static final List<TestCase> testCases = new ArrayList<>();

public static void register(TestCase testCase) {
testCases.add(testCase);
}

public static final void main(String[] args) {
for (TestCase case: testCases) {
case.run();
}
}
+

把这个简化版本的测试框架引入到工程中之后,我们只需要在框架预留的扩展点,也就是 TestCase 类中的 doTest() 抽象函数中,填充具体的测试代码就可以实现之前的功能了,完全不需要写负责执行流程的 main() 函数了。

+
1
2
3
4
5
6
7
8
9
public class UserServiceTest extends TestCase {
@Override
public boolean doTest() {
// ...
}
}

// 注册操作还可以通过配置的方式来实现,不需要程序员显示调用register()
JunitApplication.register(new UserServiceTest();
+

控制反转的方式有很多,除了依赖注入,还有模板模式等,我们常用的 Spring 框架主要是通过依赖注入来实现的控制反转。

+

下面我们来看看依赖注入。

+

依赖注入(DI)

依赖注入跟控制反转恰恰相反,它是一种具体的编码技巧

+

依赖注入用一句话来概括就是:不通过 new() 的方式在类内部创建依赖类对象,而是将依赖的类对象在外部创建好之后,通过构造函数、函数参数等方式传递(或注入)给类使用。

+

这里给出一个例子,分别用非依赖注入和依赖注入来实现同一个需求:Notification 类负责消息推送,依赖 MessageSender 类实现推送商品促销、验证码等消息给用户。

+

代码如下:

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
// 非依赖注入实现方式
public class Notification {
private MessageSender messageSender;

public Notification() {
this.messageSender = new MessageSender(); //此处有点像hardcode
}

public void sendMessage(String cellphone, String message) {
//...省略校验逻辑等...
this.messageSender.send(cellphone, message);
}
}

public class MessageSender {
public void send(String cellphone, String message) {
//....
}
}
// 使用Notification
Notification notification = new Notification();

// 依赖注入的实现方式
public class Notification {
private MessageSender messageSender;

// 通过构造函数将messageSender传递进来
public Notification(MessageSender messageSender) {
this.messageSender = messageSender;
}

public void sendMessage(String cellphone, String message) {
//...省略校验逻辑等...
this.messageSender.send(cellphone, message);
}
}
//使用Notification
MessageSender messageSender = new MessageSender();
Notification notification = new Notification(messageSender);
+

通过依赖注入的方式来将依赖的类对象传递进来,这样就提高了代码的扩展性,我们可以灵活地替换依赖的类(将 MessageSender 定义成接口)。

+

依赖注入框架(DI Framework)

在实际的软件开发中,一些项目可能会涉及几十、上百、甚至几百个类,类对象的创建和依赖注入会变得非常复杂。如果这部分工作都是靠程序员自己写代码来完成,容易出错且开发成本也比较高。而对象创建和依赖注入的工作,本身跟具体的业务无关,我们完全可以抽象成框架来自动完成。

+

这个框架就是「依赖注入框架」。我们只需要通过依赖注入框架提供的扩展点,简单配置一下所有需要创建的类对象、类与类之间的依赖关系,就可以实现由框架来自动创建对象、管理对象的生命周期、依赖注入等原本需要程序员来做的事情。

+

常见的依赖注入框架有:Google Guice、Java Spring、Pico Container、Butterfly Container 等。

+
+

框架的一个特点是,当开发者使用框架开发一个应用程序时,无需在程序中调用框架的代码,就可以使用框架的功能特性。比如:

+
+
    +
  • 程序不需要调用 Spring 的代码,就可以使用 Spring 的依赖注入、MVC 这些特性,开发出低耦合、高内聚的应用代码
  • +
  • 程序不需要调用 Tomcat 的代码,就可以监听HTTP 协议端口,处理 HTTP 请求
  • +
+

依赖倒置原则(DIP)

最后回到我们这部分的主角。

+

这条原则主要也是用来指导框架层面的设计,跟前面讲到的控制反转类似。

+

我们先用 Tomcat 来说明一下这个原则:Tomcat 是运行 Java Web 应用程序的容器。我们编写的 Web 应用程序代码只需要部署在 Tomcat 容器下,便可以被 Tomcat 容器调用执行。按照之前的划分原则,Tomcat 就是高层模块,我们编写的 Web 应用程序代码就是低层模块。Tomcat 和应用程序代码之间并没有直接的依赖关系,两者都依赖同一个「抽象」,也就是 Servlet 规范。Servlet 规范不依赖具体的 Tomcat 容器和应用程序的实现细节,而 Tomcat 容器和应用程序依赖 Servlet 规范。

+

再用 JDBC 为例子说明一下依赖倒置原则:我们在 Java 开发中访问数据库,代码并不直接依赖数据库的驱动,而是依赖 JDBC。各种数据库的驱动都实现了 JDBC,当应用程序需要更换数据库的时候,不需要修改任何代码。这正是因为应用代码,也就是高层模块,不依赖数据库驱动,而是依赖抽象 JDBC,而数据库驱动,作为低层模块,也依赖 JDBC。

+

这里可能会存在一个误区:我们在日常的 Web 开发中, Service 层会依赖 DAO 层提供的接口,但这种依赖并不是依赖倒置原则!在依赖倒置原则中,除了具体实现要依赖抽象,最重要的是,抽象是属于谁的抽象

+

最后再举一个依赖倒置原则的例子:

+

Button 按钮控制 Lamp 灯泡,按钮按下的时候,灯泡点亮或者关闭。按照常规的设计思路,我们可能会设计出如下的类图关系,Button 类直接依赖 Lamp 类。

+

+

这样设计的问题在于,Button 依赖 Lamp,那么对 Lamp 的任何改动,都可能会使 Button 受到牵连,做出联动的改变。同时,我们也无法重用 Button 类。

+

解决之道就是将这个设计中的依赖于实现,重构为依赖于抽象。这里的抽象就是:打开关闭目标对象。

+
    +
  • Button 定义一个抽象接口 ButtonServer,在 ButtonServer 中描述抽象:打开、关闭目标对象
  • +
  • 由具体的目标对象,比如 Lamp 实现这个接口,从而完成 Button 控制 Lamp 这一功能需求
  • +
+

+

通过这样一种依赖倒置,Button 不再依赖 Lamp,而是依赖抽象 ButtonServer,而 Lamp 也依赖 ButtonServer,高层模块和低层模块都依赖抽象。Lamp 的改动不会再影响 Button,而 Button 可以复用控制其他目标对象,比如电机,或者任何由按钮控制的设备,只要这些设备实现 ButtonServer 接口就可以了。

+

依赖倒置原则也被称为好莱坞原则:Don’t call me,I will call you.

+

遵循依赖倒置原则有这样几个编码守则:

+
    +
  1. 应用代码中多使用抽象接口,尽量避免使用那些多变的具体实现类。
  2. +
  3. 不要继承具体类,如果一个类在设计之初不是抽象类,那么尽量不要去继承它。对具体类的继承是一种强依赖关系,维护的时候难以改变。
  4. +
  5. 不要重写(Override)包含具体实现的函数。
  6. +
+
+

软件开发有时候像变魔术一样,常常表现出违反常识的特性,让人目眩神晕,而这正是软件编程这门艺术的魅力所在,感受到这种魅力,在自己的软件设计开发中体现出这种魅力,你就迈进了软件高手的大门。

+
+

参考

极客时间:后端技术面试38讲设计模式之美

+ +
+ + + + + + + +
+ + + + + + +
+ +
+ + + +
+ + + + + + + + +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2020/Strongly-type-interface-based-on-Feign/1.png b/2020/Strongly-type-interface-based-on-Feign/1.png new file mode 100644 index 0000000000..a7921f7bfb Binary files /dev/null and b/2020/Strongly-type-interface-based-on-Feign/1.png differ diff --git a/2020/Strongly-type-interface-based-on-Feign/2.png b/2020/Strongly-type-interface-based-on-Feign/2.png new file mode 100644 index 0000000000..de3fb25a26 Binary files /dev/null and b/2020/Strongly-type-interface-based-on-Feign/2.png differ diff --git a/2020/Strongly-type-interface-based-on-Feign/3.png b/2020/Strongly-type-interface-based-on-Feign/3.png new file mode 100644 index 0000000000..12f8919a77 Binary files /dev/null and b/2020/Strongly-type-interface-based-on-Feign/3.png differ diff --git a/2020/Strongly-type-interface-based-on-Feign/index.html b/2020/Strongly-type-interface-based-on-Feign/index.html new file mode 100644 index 0000000000..da99513f14 --- /dev/null +++ b/2020/Strongly-type-interface-based-on-Feign/index.html @@ -0,0 +1,526 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 基于 Feign 实现强类型接口 | 贾攀的流水账 + + + + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

贾攀的流水账

+ +
+

Panmax's Blog

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+ + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ 基于 Feign 实现强类型接口 +

+ + +
+ + + + +
+ + +

强弱类型语言

众所周知编程语言有强弱类型之分,进一步还有动态和静态之分。比如 Java、C# 是强类型的(strongly typed)静态语言,Javascript、PHP 是弱类型的(weakly typed)动态语言。

+

强类型静态语言常常被称为类型安全(type safe)语言,一般在会编译期间进行强制类型检查,提前避免一些类型错误。弱类型动态语言虽然也有类型的概念,但是比较松散灵活,而且大多是解释型语言,一般没有强制类型检查,类型问题一般要在运行期才能暴露出来。

+

强弱类型的语言各有优劣、相互补充,各有适用的场景。比如服务端开发经常用强类型的,前端 Web 界面经常会用 Javascript 这种弱类型语言。

+

1.png

+

强弱类型 API

对于服务 API 也有强弱类型之分,传统的 RPC 服务一般是强类型的,RPC 通常采用订制的二进制协议对消息进行编码和解码,采用 TCP 传输消息。RPC 服务通常有严格的契约(contract),开发服务器前先要定义 IDL(Interface Definition Language),用 IDL 来定义契约,再通过契约自动生成强类型的服务端和客户端的接口。服务调用的时候直接使用强类型客户端,不需要手动进行消息的编码和解码,gRPC 和 Apache Thrift 是目前两款主流的 RPC 框架。

+

而现在的大部分 Restful 服务通常是弱类型的,Rest 通常采用 Json 作为传输消息,使用 HTTP 作为传输协议,Restful 服务通常没有严格的契约的概念,使用普通的 HTTP Client 就可以调用,但是调用方通常需要对 Json 消息进行手动编码和解码的工作。在现实世界当中,大部分服务框架都是弱类型 Restful 的服务框架,比方说 Java 生态当中的 SpringBoot 可以认为是目前主流的弱类型 Restful 框架之一。

+

2.png

+

当然以上区分并不是业界标准,只是个人基于经验总结出来的一种区分的方法。

+

强弱类型 API 优劣

强类型服务接口的好处是:接口规范、自动代码生成、自动编码解码、编译期自动类型检查。强类型接口的好处也带来不利的一面:首先是客户端和服务端强耦合,任何一方升级改动可能会造成另一方 break,另外自动代码生成需要工具支持,而开发这些工具的成本也比较高。其次强类型接口开发测试不太友好,一般的浏览器、Postman 这样的工具无法直接访问强类型接口。

+

弱类型服务接口的好处是客户端和服务器端不强耦合,不需要开发特别的代码生成工具,一般的 HTTP Client就可以调用,开发测试友好,不同的浏览器、Postman 可以轻松访问。弱类型服务接口的不足是需要调用方手动编码解码消息、没有自动代码的生成、没有编译器接口类型检查、代码不容易规范、开发效率相对低,而且容易出现运行期的错误。

+

有没有办法结合强弱类型服务接口各自的好处同时又规避他们的不足呢?

+

我们的做法是在 Spring Rest 弱类型接口的基础上借助 Spring Feign 支持的强类型接口的特性实现强类型 Rest 接口的调用机制,同时兼备强弱类型接口的好处。

+

首先我们来介绍下 Spring FeignSpring Feign 本质上是一种动态代理机制(Dynamic Proxy),只需要我们给出 Restful API 对应的 Java 接口,它就可以在运行期动态的拼装出对应接口的强类型客户端。拼装出的客户端的结构和请求响应流程如下图所示:

+

3.png

+
    +
  1. 客户应用发起一个请求并传入一个 Request Bean,这个请求通过 Java 接口首先被动态代理截获
  2. +
  3. 通过相应的编码器(Encoder)进行编码,成为 Request Json
  4. +
  5. Request Json 根据需要可以经过一些拦截器(Interceptor)做进一步处理
  6. +
  7. 处理完之后传递给 HTTP Client,HTTP Client 将 Request Json通过 HTTP 协议发送至服务器端
  8. +
  9. 当服务端响应回来后,相应的 Response Json 会被 HTTP Client 接收到
  10. +
  11. 经过一些拦截器做一些响应处理
  12. +
  13. 转发给解码器(Decoder)解码为 Response Bean
  14. +
  15. 最后 Response Bean 通过 Java 接口返回给调用方
  16. +
+

整个请求响应流程的关键步骤是编码和解码,也就是 Java 对象和 Json 消息的互转,这个过程也被称为序列化和反序列化,另外一种叫法为「Json 对象绑定」。对于一个服务框架而言,序列化、反序列化器的性能对于服务框架性能影响是最大的,也就是说可以认为 DecoderEncoder 决定了服务框架总体的性能。

+

虽然我们开发出来的服务是弱类型的 Restful 服务,但是因为有 Spring Feign 的支持,我们只要简单的给出一个强类型的 Java API 接口就自动获得了一个强类型客户端,也就是说利用 Spring Feign 我们可以同时获得强弱类型的好处(编译器自动类型检查、不需要手动编码解码、不需要开发代码生成工具、客户端和服务器端不强耦合),这样可以同时规范代码风格,提升开发测试效率。

+

我们可以在项目内为每个微服务提供两个模块,一个是 API 接口模块(如 mail-api),另一个是服务实现模块(如 mail-svc)。API接口模块内就是强类型的 Java API 接口(包括请求响应的 DTO),可以直接被 Spring Feign 引用并动态拼装出强类型客户端。

+

项目结构如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
.
├── README.md
├── account
│   ├── Dockerfile
│   ├── pom.xml
│   └── src
│   ├── main
│   └── test
├── account-api
│   ├── pom.xml
│   └── src
│   └── main
├── mail
│   ├── Dockerfile
│   ├── mail.iml
│   ├── pom.xml
│   └── src
│   ├── main
│   └── test
├── mail-api
│   ├── pom.xml
│   └── src
│   └── main
└── pom.xml
+
+

注:我这里没有采用 xxx-apixx-svc 的命名方式,直接是 xxx-api 表示 API Client 模块, xxx 为服务实现模块。

+
+

我们以用户注册后发送通知邮件来写一个简单的例子。发送邮件 client 定义如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// mail-api/src/main/java/com/demo/mail/client/MailClient.java

import com.demo.common.api.dto.Response;
import com.demo.mail.dto.EmailSendDTO;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;

import javax.validation.Valid;

@FeignClient(name = "mail")
public interface MailClient {
@PostMapping(path = "/send")
Response<Boolean> send(@RequestBody @Valid EmailSendDTO mailSendDTO);
}
+

其中 Response 定义如下:

1
2
3
4
5
6
7
8
9
10
@Data
public class Response<T> {

private int code = 0;

private String message = "OK";

private T data;

}
+

用户服务调用发邮件 API 实现如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
// account/src/main/java/com/demo/account/service/UserService.java

public class UserService {

@Autowired
private final UserRepository userRepository;

@Autowired
private MailClient mailClient;

public boolean register(UserDTO userVO) {
// 忽略参数验证部分代码...

User user= userRepository.save(UserDTOConvert.convertTo(userVO));

EmailSendDTO mail = EmailSendDTO.builder()
.to("user.getEmail()")
.subject("welcome!")
.htmlBody("hello," + user.getName()).build();
try {
Response<Boolean> response = mailClient.send(mail);
} catch (Exception e) {
log.error(e.getMessage());
throw new AppException(SysErrorEnum.SYSTEM_ERROR);
}

if (response.getCode() != 0) {
throw new ServiceException(response.getCode(), response.getMessage());
} else if (!response.getData()) {
throw new ServiceException(AccountErrorEnum.MAIL_SEND_ERROR);
}

return true;
}
}
+

为了体现异常处理流程,上边代码仅用于演示,在生产环境下发邮件应该为异步处理,无需检查发送结果。我们在服务内加了全局异常处理,所以直接向上抛出即可。

+

最后再补充一点,业界 Restful API 的设计通常采用 HTTP 协议状态码来传递和表达错误语义,但是我们的设计是将错误码打包在一个正常的 Json 消息当中,也就是 Response 当中,这一种称为封装消息 + 捎带的设计模式,这样设计的目标是为了支持强类型的客户端同时简化和规范错误处理,如果借用 HTTP 协议状态码来传递和表达错误语义,虽然也可以开发对应的强类型客户端,但是内部的调用处理逻辑就会比较复杂,需要处理各种 HTTP 的错误码,开发成本会比较高。

+ +
+ + + + + + + +
+ + + + + + +
+ +
+ + + +
+ + + + + + + + +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2020/actors-and-csp-introduce/0.jpeg b/2020/actors-and-csp-introduce/0.jpeg new file mode 100644 index 0000000000..0a7f244d6b Binary files /dev/null and b/2020/actors-and-csp-introduce/0.jpeg differ diff --git a/2020/actors-and-csp-introduce/1.png b/2020/actors-and-csp-introduce/1.png new file mode 100644 index 0000000000..4178586252 Binary files /dev/null and b/2020/actors-and-csp-introduce/1.png differ diff --git a/2020/actors-and-csp-introduce/2.png b/2020/actors-and-csp-introduce/2.png new file mode 100644 index 0000000000..d85c0b0959 Binary files /dev/null and b/2020/actors-and-csp-introduce/2.png differ diff --git a/2020/actors-and-csp-introduce/3.png b/2020/actors-and-csp-introduce/3.png new file mode 100644 index 0000000000..6007335748 Binary files /dev/null and b/2020/actors-and-csp-introduce/3.png differ diff --git a/2020/actors-and-csp-introduce/index.html b/2020/actors-and-csp-introduce/index.html new file mode 100644 index 0000000000..75ed58cb68 --- /dev/null +++ b/2020/actors-and-csp-introduce/index.html @@ -0,0 +1,543 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Actors 和 CSP 并发模型介绍 | 贾攀的流水账 + + + + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

贾攀的流水账

+ +
+

Panmax's Blog

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+ + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ Actors 和 CSP 并发模型介绍 +

+ + +
+ + + + +
+ + +

+

并发 vs 并行

介绍并发模型前,我们先来理解一下并发和并行的区别,下边这张图说明了两者之间的区别:

+

+
    +
  • 并发:一个处理器同时处理多个任务。
  • +
  • 并行:多个处理器或者是多核的处理器同时处理多个不同的任务.
  • +
+

并发性 vs 并行性

    +
  • 并发性(concurrency),又称共行性,是指能处理多个同时性活动的能力,并发事件之间不一定要同一时刻发生。
  • +
  • 并行(parallelism) 是指同时发生的两个并发事件。
  • +
+
+

并行具有并发的含义,而并发则不一定并行。

+
+

并发模型

并发模型分类

并发编程模型按照实现方式可以分为两类:

+
    +
  • 共享状态并发(Shared state concurrency)
  • +
  • 消息传递并发(Message passing concurrency)
  • +
+

共享状态并发

共享状态并发涉及到可变状态(Mutable state,即内存可修改)的概念。大多数语言,如 C、Java、C++ 等等,都有这个概念,即有一种叫内存的东西,我们可以修改它。

+

在只有一个进程(或线程)工作的情况下,这个模式可以很好地运行。但如果有多个进程共享和修改相同的内存,就会产生问题和危险。

+

为了防止同时修改共享内存,我们需要一个锁机制。你可以称它为互斥量或同步方法,但它本质上仍然是锁。

+

如果程序在关键区域发生崩溃(例如,当它在持有锁的时候)就会有灾难的发生:其他所有的进程都不知道该做什么。

+

多线程模型就是通过共享状态实现的并发,代表语言有:Java, C#, C++。

+

同时,根据上边的内容可以推导出:

+

不可变数据结构(Immutable) = 没有锁

+

不可变数据结构(Immutable)= 易于并发

+

消息传递并发

在消息传递并发中,不存在共享状态。所有计算都是在进程中完成的,交换数据的唯一方法是通过异步消息传递。

+

如何理解这句话?

+

想象一群人,他们没有共享的状态。

+

我有自己的记忆,同时你也有你的记忆。它们是不共享的。我们通过传递信息(如,说话)进行交流,我们根据接收到的这些消息更新私有状态(也就是自己的记忆)。

+

Actors 模型CSP 模型 是通过消息传递实现的并发。

+
    +
  • Actors 代表语言:Erlang, Scala, Rust
  • +
  • CSP 代表语言:Golang
  • +
+

Actors vs CSP

对于多线程模型大部分开发人员都是比较熟悉的,也知道它存在很多缺点,如:死锁、不易伸缩。

+

接下来的内容我们重点对两个基于消息传递并发的模型来进行介绍和对比,Actors 和 CSP 是实现程序并行工作的两种最有效的模型。

+

Actors 模型,顾名思义,侧重的是 Actor。每个 Actor 与其他 Actor 进行直接通信,不经过中介,且消息是异步发送和处理的。

+

+

CSP 是 Communicating Sequential Processes(通信顺序进程)的简称。在 CSP 中,多了一个角色 Channel,Worker 之间不直接通信,而是通过 Channle 进行通信。

+

Channel 是过程的中间媒介,Worker1 想要跟 Worker2 发信息时,直接把信息放到 Channel 里(在程序中其实就是一块内存),然后 Worker2 在方便的时候到 Channel 里获取。

+

Channel 类似 Unix 中的 Pipe,后文将 Channel 称为通道

+

+

CSP 是完全同步的。通道的写入方在接受方读取前会被一直阻塞。这种基于阻塞机制的优点是一个通道只需要保存一条消息,在很多方面也更容易推理。

+

Actors 的发送方是异步的。无论消息接收方是否将消息读取出来,发送方都不会阻塞,而是将消息放入通常称为邮箱(mailbox)的队列中。这提供了很多的便利,但困难之处在于邮箱可能需要容纳大量的信息。

+

CSP 进程使用通道(channel)进行通信。程序可以将通道作为第一类对象(first class objects)创建并传递。Actors 有地址系统和收件箱,每个进程只有一个地址。在耦合度上两者是有区别的,CSP 更加松耦合。

+

在 CSP 中,发送和接收操作可能会阻塞。在 Actors 模型中,只有接收操作可能被阻塞。

+

在 CSP 中,消息是按发送顺序传递的,而在 Actors 模型中不是这样。事实上,系统可能根本无法传递某些消息(意味着消息可能会丢失)。

+

到目前为止,CSP 模型在一台机器上工作得最好,而 Actors 模型很容易实现跨多台机器的扩展。

+

结论

Actors 更适合于分布式系统。

+

由于 CSP 具有阻塞性,因此很难在多台计算机中使用它们。

+ +
+ + + + + + + +
+ + + + + + +
+ +
+ + + +
+ + + + + + + + +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2020/auto-backup-database/0.jpg b/2020/auto-backup-database/0.jpg new file mode 100644 index 0000000000..7ddf1c2760 Binary files /dev/null and b/2020/auto-backup-database/0.jpg differ diff --git a/2020/auto-backup-database/1.png b/2020/auto-backup-database/1.png new file mode 100644 index 0000000000..b813e7168a Binary files /dev/null and b/2020/auto-backup-database/1.png differ diff --git a/2020/auto-backup-database/2.png b/2020/auto-backup-database/2.png new file mode 100644 index 0000000000..968cedcd30 Binary files /dev/null and b/2020/auto-backup-database/2.png differ diff --git a/2020/auto-backup-database/3.png b/2020/auto-backup-database/3.png new file mode 100644 index 0000000000..50b423366a Binary files /dev/null and b/2020/auto-backup-database/3.png differ diff --git a/2020/auto-backup-database/index.html b/2020/auto-backup-database/index.html new file mode 100644 index 0000000000..ac65603f7b --- /dev/null +++ b/2020/auto-backup-database/index.html @@ -0,0 +1,516 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 自动备份数据库并上传到 S3 | 贾攀的流水账 + + + + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

贾攀的流水账

+ +
+

Panmax's Blog

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+ + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ 自动备份数据库并上传到 S3 +

+ + +
+ + + + +
+ + +
+ +

我开发的老板管库虽然没太多收入,但是还是有不少的用户量,为了节约成本,我并没有使用厂商提供的云数据库,而是在服务器本地搭了一个 MariaDB 实例。考虑到用户数据安全还是第一位的,所以我每天会通过定时任务的方式进行全量备份,并上传到我的七牛云,脚本如下:

+
1
2
3
4
5
6
7
8
9
10
#!/bin/bash

dir=$(dirname $(readlink -f "$0"))

filename=bossku_$(date +%Y%m%d%H%M).sql
echo ${filename}
cd ${dir}
mysqldump -h{ipaddress} -P{port} -uroot -p{password} bossku > ${filename}

qshell rput bosskudb ${filename} ${filename}
+

上边的命令会生成一个以执行时间为后缀的 .sql 文件并上传到我的七牛云中名为 bosskudb 的bucket中,同时我还会配置这个 bucket 的生命周期,只保留近7天的数据。这实际上是套比较通用的流程,昨天恰好看到一个 repo:https://github.com/appleboy/docker-backup-database 就是用来提供这套流程的封装的,看到作者又是个自己比较崇拜的开发者,于是准备上手用一用。

+

(P.S. appleboy 大神是个非常活跃的 golang开发者,在去年学习 go 的时候就 fo 了他)

+

这个工具目前支持备份 PG 和 MySQL 数据库,并上传到 S3(包括支持S3协议的 minio) 或者本地路径下,启用方式也非常方便,写个 docker-compose 文件就可以了。

+

以下是我的操作记录:

+

准备环境

首先在我的 AWS 中新建了一个 bucket,我所选择的区域为亚太地区(香港) ap-east-1,bucket 名为 bossku-db-backup。

+
+ +

准备 docker-compose.yml 脚本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
version: '3'

services:
backup_mysql:
image: appleboy/docker-backup-database:mysql-5.7
logging:
options:
max-size: "100k"
max-file: "3"
environment:
STORAGE_DRIVER: s3
STORAGE_ENDPOINT: s3.amazonaws.com
STORAGE_BUCKET: bossku-db-backup
STORAGE_REGION: ap-east-1
STORAGE_PATH: backup
STORAGE_SSL: "false"
STORAGE_INSECURE_SKIP_VERIFY: "false"
ACCESS_KEY_ID: AKI*******UFT
SECRET_ACCESS_KEY: 4u********************NU

DATABASE_DRIVER: mysql
DATABASE_HOST: {ip}:{port}
DATABASE_USERNAME: root
DATABASE_PASSWORD: {password}
DATABASE_NAME: bossku
DATABASE_OPTS:

TIME_SCHEDULE: "0 0 * * *"
TIME_LOCATION: Asia/Shanghai
+

ACCESS_KEY_IDSECRET_ACCESS_KEY 获取方式可以查看:Where’s My Secret Access Key?

+

因为我所启动的数据库实例为 MariaDB:10.2,根据官方介绍,其所对应的 MySQL 版本为 5.7,所以上边命令中的 image 我指定的是 appleboy/docker-backup-database:mysql-5.7

+
+ +

测试

测试的时候,为了方便查看效果,可以将 TIME_SCHEDULE 删掉,这样会立即执行,且执行一次后退出。

+
1
2
3
4
5
6
7
8
9
10
docker-compose up -d

# 然后观察日志
docker-compose logs -f
Attaching to bossku-db-backup_backup_mysql_1_6cc39ab97f2c
backup_mysql_1_6cc39ab97f2c | $ mysqldump --version
backup_mysql_1_6cc39ab97f2c | mysqldump Ver 10.13 Distrib 5.7.32, for Linux (x86_64)
backup_mysql_1_6cc39ab97f2c | $ bash -c mysqldump -h {ip} -P {port} -u root bossku | gzip > dump.sql.gz

bossku-db-backup_backup_mysql_1_6cc39ab97f2c exited with code 0
+

可以看到成功了,再到 S3 中验证一下文件有没有上传上来:

+
+ +

文件也传成功了!

+

如果在最后的上传步骤遇到无权限的错误,可以通过尝试调整 bucket 权限来解决。

+

写在最后

通过日志和上传上来的文件名可以看出,其实他也是通过 mysqldump 先生成备份文件,然后通过 S3 的 SDK 进行上传,同时也是使用了日期最为文件名的命名方式。我也大致看了下代码,所使用的 SDK 为 minio 提供的,这样又可以同时支持上传到 minio 了。

+

创新就是将一些已有的东西进行重新组合,比如这里只是将 dockermysqldumpS3 进行了组合,就创造出了这么一个好用且通用的工具,非常值得学习。

+ +
+ + + + + + + +
+ + + + + + +
+ +
+ + + +
+ + + + + + + + +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2020/big-sur-turn-on-HiDPI/0.jpeg b/2020/big-sur-turn-on-HiDPI/0.jpeg new file mode 100644 index 0000000000..a24c91fea9 Binary files /dev/null and b/2020/big-sur-turn-on-HiDPI/0.jpeg differ diff --git a/2020/big-sur-turn-on-HiDPI/1.png b/2020/big-sur-turn-on-HiDPI/1.png new file mode 100644 index 0000000000..80a66b9eee Binary files /dev/null and b/2020/big-sur-turn-on-HiDPI/1.png differ diff --git a/2020/big-sur-turn-on-HiDPI/index.html b/2020/big-sur-turn-on-HiDPI/index.html new file mode 100644 index 0000000000..8b2f17db07 --- /dev/null +++ b/2020/big-sur-turn-on-HiDPI/index.html @@ -0,0 +1,508 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Big Sur 开启HiDPI | 贾攀的流水账 + + + + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

贾攀的流水账

+ +
+

Panmax's Blog

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+ + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ Big Sur 开启HiDPI +

+ + +
+ + + + +
+ + +
+ +

上周我的电脑升级了 Big Sur,果不其然之前配置的 HiDPI 失效了。而且这次苹果做了更严格的限制,即便禁用 SIP 也无法对系统目录进行修改了。

+

网上找了很多 Big Sur 开启 HiDPI 的方法,最后找到一种有效的方式,记录在这里,为了方便自己查阅,也希望能帮助到其他人。

+

运行下边这条命令:

1
bash -c "$(curl -fsSL https://raw.githubusercontent.com/xzhih/one-key-hidpi/master/hidpi.sh)"
+

执行路径为:

    +
  1. 选择自己的外接显示器
  2. +
  3. 开启HIDPI(同时注入EDID)
  4. +
  5. 保持原样
  6. +
  7. 手动输入分辨率
  8. +
+

最后手动输入我需要的分辨率:1920x1080 2560x1440,重启后就可以通过 RDM(https://github.com/avibrazil/RDM) 来开启 HiDPI 了。

+
+ +

如果无效的话

尝试删除 /Library/Displays/Contents/Resources/Overrides/DisplayVendorID-xxx 目录后再试一次(xxx 为你的 VendorID)。

+
+

我之前一直失败就是用这个方式才成功的,估计是用其他方法写入了脏数据,hidpi.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

+ +
+ + + + + + + +
+ + + + + + +
+ +
+ + + +
+ + + + + + + + +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2020/circular-dependence/0.jpg b/2020/circular-dependence/0.jpg new file mode 100644 index 0000000000..52da11c57b Binary files /dev/null and b/2020/circular-dependence/0.jpg differ diff --git a/2020/circular-dependence/1.png b/2020/circular-dependence/1.png new file mode 100644 index 0000000000..e6e3ac1e0b Binary files /dev/null and b/2020/circular-dependence/1.png differ diff --git a/2020/circular-dependence/11.png b/2020/circular-dependence/11.png new file mode 100644 index 0000000000..41ce171b56 Binary files /dev/null and b/2020/circular-dependence/11.png differ diff --git a/2020/circular-dependence/12.png b/2020/circular-dependence/12.png new file mode 100644 index 0000000000..ee5794613d Binary files /dev/null and b/2020/circular-dependence/12.png differ diff --git a/2020/circular-dependence/13.png b/2020/circular-dependence/13.png new file mode 100644 index 0000000000..5e846f497e Binary files /dev/null and b/2020/circular-dependence/13.png differ diff --git a/2020/circular-dependence/2.png b/2020/circular-dependence/2.png new file mode 100644 index 0000000000..6bbcd3a934 Binary files /dev/null and b/2020/circular-dependence/2.png differ diff --git a/2020/circular-dependence/3.png b/2020/circular-dependence/3.png new file mode 100644 index 0000000000..1260c3944f Binary files /dev/null and b/2020/circular-dependence/3.png differ diff --git a/2020/circular-dependence/4.png b/2020/circular-dependence/4.png new file mode 100644 index 0000000000..dbb55ce5b8 Binary files /dev/null and b/2020/circular-dependence/4.png differ diff --git a/2020/circular-dependence/5.png b/2020/circular-dependence/5.png new file mode 100644 index 0000000000..9bbe8b2383 Binary files /dev/null and b/2020/circular-dependence/5.png differ diff --git a/2020/circular-dependence/6.png b/2020/circular-dependence/6.png new file mode 100644 index 0000000000..dbc5c6a87d Binary files /dev/null and b/2020/circular-dependence/6.png differ diff --git a/2020/circular-dependence/7.png b/2020/circular-dependence/7.png new file mode 100644 index 0000000000..cc153dcd3d Binary files /dev/null and b/2020/circular-dependence/7.png differ diff --git a/2020/circular-dependence/8.png b/2020/circular-dependence/8.png new file mode 100644 index 0000000000..8a8f267d01 Binary files /dev/null and b/2020/circular-dependence/8.png differ diff --git a/2020/circular-dependence/9.png b/2020/circular-dependence/9.png new file mode 100644 index 0000000000..9d69ca0557 Binary files /dev/null and b/2020/circular-dependence/9.png differ diff --git a/2020/circular-dependence/index.html b/2020/circular-dependence/index.html new file mode 100644 index 0000000000..3a5154ee23 --- /dev/null +++ b/2020/circular-dependence/index.html @@ -0,0 +1,589 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 如何解决代码中存在的循环依赖问题 | 贾攀的流水账 + + + + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

贾攀的流水账

+ +
+

Panmax's Blog

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+ + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ 如何解决代码中存在的循环依赖问题 +

+ + +
+ + + + +
+ + +
+ +
+

代码中存在的循环依赖问题跟代码的维护工作有很大关系,也是日常开发中经常会碰到的一个问题。

+
+
+ +

任何系统在开发了一段时间之后随着业务功能和代码数量的不断增加,代码之间的调用与被调用关系也会变得越来越复杂,各个类和组件之间就会存在出乎开发人员想象的复杂关系。

+

一种常见的复杂关系为类与类之间的循环依赖关系。

+
+ +

所谓循环依赖,简单来说就是一个类A会引用类B中的方法,而反过来类B也会引用类A中的方法,这就导致两者之间有了一种相互引用的关系,从而形成循环依赖。

+

合理的系统架构以及持续的重构优化工作能够减轻这种复杂关系,但是如果有效识别系统中存在的循环依赖,仍然是开发人员面临的一个很大的挑战。主要原因在于类之间的循环依赖存在传递性

+

举个例子:如果系统中只存在类A和类B,那么他们之间的依赖关系就非常容易识别。

+
+ +

如果再来一个类C,那么这三个类之间的组合就有很多种情况了。

+
+ +

如果一个系统中存在几十个类,那么他们之间的依赖关系就很难通过简单的关系图进行逐一列举。一般的系统中类的数量显然不止几十个。更宽泛地讲,类之间的这种循环依赖关系也可以扩展到组件级别。产生组件之间的循环依赖的原因在于:组件1中的类A与组件2中的类B之间存在循环依赖,从而导致组件与组件之间产生了循环依赖关系。

+
+ +

在软件设计领域有一条公认的设计原则:无环依赖原则

+

无环依赖原则:在组件之间不应该存在循环依赖关系。通过将系统划分为不同的可发布组件,对某一个组件的修改所产生的影响,可以不扩展到其他组件。

+

所谓的无环依赖指的是在包的依赖关系中不允许存在环,也就是说包之间的依赖关系必须是一个直接的无环图。

+

下面我们通过一个具体的代码示例,介绍一下组件之间循环依赖的产生过程。也是在为本文要介绍的如何消除循环依赖做好准备工作。

+

现在我们正在开发一款健康管理类APP,每个用户都有一份自己的健康档案,档案中记录着用户当前的健康等级,以及一系列可以让用户更加健康的任务列表(如:忌烟酒、慢跑)。用户当前的等级是和用户所需要完成的任务列表挂钩的,任务列表越多,说明越不健康,对应的健康等级也就越低(最低为1、最高为3)。

+

用户可以通过完成APP所指定的任务来获取一定的积分,这个积分的计算过程取决于这个用户当前的健康等级。也就是说不同的等级之下同一个任务所产生的积分也是不一样的。而每个任务也有自己的初始积分,每个任务最终所能得到的积分算法为 12 / <当前的等级> + <任务初始积分>,健康等级越低,做任务所能得到的积分也就越高,这样可以鼓励用户多做任务。

+

背景就介绍到这里,对于这个常见我们可以抽象出两个类:一个是代表档案的 HealthRecord 类、另一个是代表健康任务的 HealthTask 类。

+
+ +

其中 HealthRecord 类中提供了一个获取健康等级的方法 getHealthLevel() 来计算健康等级,同时也提供了添加任务的方法 addTask()

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public class HealthRecord {

private List<HealthTask> tasks = new ArrayList<>();

public Integer getHealthLevel() {
if (tasks.size() > 5) {
return 1;
}
if (tasks.size() < 2) {
return 3;
}
return 2;
}

public void addTask(String taskName, Integer initialHealthPoint) {
HealthTask task = new HealthTask(this, taskName, initialHealthPoint);
tasks.add(task);
}

public List<HealthTask> getTasks() {
return tasks;
}

}
+

对应的 HealthTask 中,显然应该包含对 HealthRecord 的引用,同时也实现了计算任务积分的方法 calculateHealthPointForTask()calculateHealthPointForTask() 方法中用到了 HealthRecord 中的健康等级信息 getHealthLevel()

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
public class HealthTask {

private HealthRecord record;

private String taskName;

private Integer initialHealthPoint;

public HealthTask(HealthRecord record, String taskName, Integer initialHealthPoint) {
this.record = record;
this.taskName = taskName;
this.initialHealthPoint = initialHealthPoint;
}

public Integer calculateHealthPointForTask() {
Integer healthPointFromHealthLevel = 12 / record.getHealthLevel();

return initialHealthPoint + healthPointFromHealthLevel;
}

public String getTaskName() {
return taskName;
}

public Integer getInitialHealthPoint() {
return initialHealthPoint;
}
}
+

不难看出,HealthRecordHealthTask 之间存在明显的相互依赖关系。

+

我们可以使用 IDEA 自带的 Analyze Dependency Matrix 对包含 HealthRecordHealthTask 类的包进行分析,得出系统中存在循环依赖代码的提示。

+
+ +

Analyze Dependency Matrix 的使用细节可以参考官方文档:https://www.jetbrains.com/help/idea/dsm-analysis.html,这里我们只关心是否存在循环依赖,也就是那个红色的框框。

+

通过上边的例子,我们了解了如何有效识别代码中存在循环依赖的问题,下边再来看看如何消除代码中的循环依赖。

+

软件行业有一句非常经典的话:「当我们在碰到一个问题无从下手时,不妨考虑一下是否可以通过加一层的方法来解决」。消除循环依赖的基本思路也是一样的,有三种常见的方法:提取中介者、转移业务逻辑、采用回调接口。

+

提取中介者

提取中介者方法也被称为关系上移,其核心思想就是把两个相互依赖的组件中的交互部分抽象出来形成一个新的组件,而这个新的组件包含着原有两个组件的引用,这样就把循环依赖关系剥离出来,并提取到一个专门的中介者的组件中。

+
+ +

这个中介者组件的实现也不难,可以通过提供一个计算积分的方法对循环依赖进行剥离,这个方法同时依赖 HealthRecordHealthTask 对象,并实现了原有 HealthTask 中根据 HealthRecord 的健康等级信息计算积分的业务逻辑。

+
1
2
3
4
5
6
7
8
9
10
11
12
13
public class HealthPointMediator {
 
    private HealthRecord record;
 
    public HealthPointMediator(HealthRecord record) {
        this.record = record;
    }
 
    public Integer calculateHealthPointForTask(HealthTask task) {
        Integer healthPointFromHealthLevel = 12 / record.getHealthLevel();
        return task.getInitialHealthPoint() + healthPointFromHealthLevel;
    }
}
+

可以看到上边的 calculateHealthPointForTask() 方法中,我们从 HealthRecord 中获取了等级,然后再从传入的 HealthTask 中获取初始积分,从而完成了对整个积分的计算过程,这个时候的HealthTask 就变得非常简单了,因为已经不包含任何有关 HealthRecord 的依赖。

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class HealthTask {

private String taskName;

private Integer initialHealthPoint;

public HealthTask(String taskName, Integer initialHealthPoint) {
this.taskName = taskName;
this.initialHealthPoint = initialHealthPoint;
}

public String getTaskName() {
return taskName;
}

public Integer getInitialHealthPoint() {
return initialHealthPoint;
}
}
+

下边针对「提取中介者」这种消除循环依赖的实现方法来编写一个测试用例:

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class HealthPointTest {

public static void main(String[] args) {
HealthRecord record = new HealthRecord();
record.addTask("忌烟酒", 5);
record.addTask("每周跑步3次", 4);
record.addTask("每天喝2升水", 4);
record.addTask("晚上10点按时睡觉", 3);
record.addTask("晚上8点后不再吃东西", 1);

HealthPointMediator mediator = new HealthPointMediator(record);

for (HealthTask task : record.getTasks()) {
System.out.println(mediator.calculateHealthPointForTask(task));
}
}

}
+

HealthRecord 中我们创建了 5 个 HealthTask,并赋予了不同的初始积分。然后通过 HealthPointMediator 这个中间者分别对每个 Task 进行积分计算。最后我们再次使用 Analyze Dependency Matrix 分析下当前的代码是否有循环依赖。

+
+ +

可以发现这次代码中已经不存在任何的环了。

+

转移业务逻辑

转移业务逻辑也被称为关系下移,其实现思路在于提取一个专门的业务组件 HealthLevelHandler 来完成对健康等级的计算过程,HealthTask 原有的对 HealthRecord 的依赖,就转移到了对 HealthLevelHandler 的依赖,而 HealthLevelHandler 本身是不需要依赖任何业务对象的。

+
+ +
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class HealthLevelHandler {

private Integer taskCount;

public HealthLevelHandler(Integer taskCount) {
this.taskCount = taskCount;
}

public Integer getHealthLevel() {
if (taskCount > 5) {
return 1;
}
if (taskCount < 2) {
return 3;
}
return 2;
}
}
+

HealthLevelHandler 的实现也不难,包含了对等级的计算过程,具体到这里就是实现 getHealthLevel() 方法,随着业务组件的提取,HealthRecord 需要做相应的改造,getHealthPointHandler 就封装了对 HealthLevelHandler 的创建过程:

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class HealthRecord {

private List<HealthTask> tasks = new ArrayList<>();

public HealthLevelHandler getHealthLevelHandler() {
return new HealthLevelHandler(tasks.size());
}

public void addTask(String taskName, Integer initialHealthPoint) {
HealthTask task = new HealthTask(taskName, initialHealthPoint);
tasks.add(task);
}

public List<HealthTask> getTasks() {
return tasks;
}

}
+

对应的 HealthTask 也需要进行改造:

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
public class HealthTask {
 
    private String taskName;
 
    private Integer initialHealthPoint;
 
    public HealthTask(String taskName, Integer initialHealthPoint) {
        this.taskName = taskName;
        this.initialHealthPoint = initialHealthPoint;
    }
 
    public Integer calculateHealthPointForTask(HealthLevelHandler handler) {
        Integer healthPointFromHealthLevel = 12 / handler.getHealthLevel();
 
        return initialHealthPoint + healthPointFromHealthLevel;
    }
 
    public String getTaskName() {
        return taskName;
    }
 
    public Integer getInitialHealthPoint() {
        return initialHealthPoint;
    }
}
+

calculateHealthPointForTask() 方法中,传入一个 HealthLevelHandler 来获取等级,然后根据获取的等级计算最终的积分。

+

最后我们对测试类进行改造:

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class HealthPointTest {

public static void main(String[] args) {
HealthRecord record = new HealthRecord();
record.addTask("忌烟酒", 5);
record.addTask("每周跑步3次", 4);
record.addTask("每天喝2升水", 4);
record.addTask("晚上10点按时睡觉", 3);
record.addTask("晚上8点后不再吃东西", 1);

HealthLevelHandler handler = record.getHealthPointHandler();

for (HealthTask task : record.getTasks()) {
System.out.println(task.calculateHealthPointForTask(handler));
}
}

}
+

现在 HealthTaskHealthRecord 都已经只剩下对 HealthLevelHandler 的依赖了。

+

采用回调接口

所谓的回调本质上就是一种双向的调用关系,也就是说被调用方在调用别人的同时也会被别人所调用。

+

我们可以提取一个用于计算健康等级的业务接口(HealthLevelHandler),然后让 HealthRecord 去实现这个接口,HealthTask 在计算积分的时候只需要依赖这个业务接口而不需要关心这个接口的具体实现类。

+
+ +

我们同样将这个接口命名为 HealthLevelHandler,包含一个计算健康等级的方法定义。

+
1
2
3
4
5
public interface HealthLevelHandler {

Integer getHealthLevel();

}
+

有了这个接口,HealthTask 就再不存在对 HealthRecord 的依赖,而是在构造函数中注入 Handler 接口:

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
public class HealthTask {

private String taskName;

private Integer initialHealthPoint;

private HealthLevelHandler handler;

public HealthTask(String taskName, Integer initialHealthPoint, HealthLevelHandler handler) {
this.taskName = taskName;
this.initialHealthPoint = initialHealthPoint;
this.handler = handler;
}

public Integer calculateHealthPointForTask() {
Integer healthPointFromHealthLevel = 12 / handler.getHealthLevel();

return initialHealthPoint + healthPointFromHealthLevel;
}

public String getTaskName() {
return taskName;
}

public Integer getInitialHealthPoint() {
return initialHealthPoint;
}
}
+

在这里的 calculateHealthPointForTask() 方法中,我们也只会使用 Handler 接口所提供的方法来获取所需的健康等级,并计算积分。

+

现在的 HealthRecord 需要实现 HealLevelHandler 接口,并提供计算健康等级的具体业务逻辑:

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
public class HealthRecord implements HealthLevelHandler {

private List<HealthTask> tasks = new ArrayList<>();

@Override
public Integer getHealthLevel() {
if (tasks.size() > 5) {
return 1;
}
if (tasks.size() < 2) {
return 3;
}
return 2;
}

public void addTask(String taskName, Integer initialHealthPoint) {
HealthTask task = new HealthTask(taskName, initialHealthPoint, this);
tasks.add(task);
}

public List<HealthTask> getTasks() {
return tasks;
}

}
+

addTask() 方法中,当创建 HealthTask 时,HealthRecord 需要把自己作为一个参数传入到 HealthTask 的构造函数中,这样我们就通过回调方法完成了对系统的改造。

+

采用回调方法,测试用例的代码业务变得更加简洁:

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class HealthPointTest {

public static void main(String[] args) {
HealthRecord record = new HealthRecord();
record.addTask("忌烟酒", 5);
record.addTask("每周跑步3次", 4);
record.addTask("每天喝2升水", 4);
record.addTask("晚上10点按时睡觉", 3);
record.addTask("晚上8点后不再吃东西", 1);

for (HealthTask task : record.getTasks()) {
System.out.println(task.calculateHealthPointForTask());
}
}

}
+

我们没有发现除了 HealthRecordHealthTask 之外的任何第三方对象,同样也可以使用 Analyze Dependency Matrix 来验证当前系统中是否存在循环依赖关系。

+

最后我放一张整体分析结果,从上到下依次为:回调接口、不采用任何措施、提取中介者、转移业务逻辑。

+
+ +

总结

对于处理循环依赖问题而言,难点在于当识别了系统中存在循环依赖场景时如何采用一种合适的方法对代码进行重构。在日常开发过程中,有三种常见的消除循环依赖的方法,可以根据场景进行灵活的应用。

+

一般而言回调方法是优先推荐的,因为它将依赖关系抽象成了接口:一来方便后续的扩展,二来从测试用例中也可以看出这种方式不需要改变系统的使用过程。

+

在无法改变现有类的内存结构时,也就是说无法为现有类添加新的接口实现关系时,可采取提取中介者转移业务逻辑这两种实现方式。其中提取中介者的方法相对比较固定,结构上与设计模式的中介者模式也比较类似。而转移业务逻辑需要根据具体的场景进行分析,具有最大的灵活性。

+

本文中的示例代码见:https://github.com/Panmax/CircularDependenceExample

+ +
+ + + + + + + +
+ + + + + + +
+ +
+ + + +
+ + + + + + + + +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2020/code-serch-cheat-sh/0.jpg b/2020/code-serch-cheat-sh/0.jpg new file mode 100644 index 0000000000..adb23015de Binary files /dev/null and b/2020/code-serch-cheat-sh/0.jpg differ diff --git a/2020/code-serch-cheat-sh/1.png b/2020/code-serch-cheat-sh/1.png new file mode 100644 index 0000000000..9fd119dcaa Binary files /dev/null and b/2020/code-serch-cheat-sh/1.png differ diff --git a/2020/code-serch-cheat-sh/2.png b/2020/code-serch-cheat-sh/2.png new file mode 100644 index 0000000000..5f53d4eea0 Binary files /dev/null and b/2020/code-serch-cheat-sh/2.png differ diff --git a/2020/code-serch-cheat-sh/3.png b/2020/code-serch-cheat-sh/3.png new file mode 100644 index 0000000000..8e03b99b51 Binary files /dev/null and b/2020/code-serch-cheat-sh/3.png differ diff --git a/2020/code-serch-cheat-sh/4.png b/2020/code-serch-cheat-sh/4.png new file mode 100644 index 0000000000..4d20596b51 Binary files /dev/null and b/2020/code-serch-cheat-sh/4.png differ diff --git a/2020/code-serch-cheat-sh/5.png b/2020/code-serch-cheat-sh/5.png new file mode 100644 index 0000000000..67adc5ebe6 Binary files /dev/null and b/2020/code-serch-cheat-sh/5.png differ diff --git a/2020/code-serch-cheat-sh/6.png b/2020/code-serch-cheat-sh/6.png new file mode 100644 index 0000000000..d5bb71b8c6 Binary files /dev/null and b/2020/code-serch-cheat-sh/6.png differ diff --git a/2020/code-serch-cheat-sh/7.png b/2020/code-serch-cheat-sh/7.png new file mode 100644 index 0000000000..c9d5bade5f Binary files /dev/null and b/2020/code-serch-cheat-sh/7.png differ diff --git a/2020/code-serch-cheat-sh/index.html b/2020/code-serch-cheat-sh/index.html new file mode 100644 index 0000000000..45c2b3f411 --- /dev/null +++ b/2020/code-serch-cheat-sh/index.html @@ -0,0 +1,529 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 非常好用的代码速查工具 cheat.sh | 贾攀的流水账 + + + + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

贾攀的流水账

+ +
+

Panmax's Blog

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+ + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ 非常好用的代码速查工具 cheat.sh +

+ + +
+ + + + +
+ + +
+ +

介绍

cheat.sh 号称自己提供了世界优质技术社区中代码速查表的统一访问。

+

安装

1
2
curl https://cht.sh/:cht.sh | sudo tee /usr/local/bin/cht.sh
chmod +x /usr/local/bin/cht.sh
+

使用

基本的查询命令为:cht.sh 语言 问题关键词

+

例1:查命令

比如,你不知道上边安装命令中 tee 是什么意思,可以尝试用下边的命令查看提示:

+
1
cht.sh linux tee
+
+ +

由此可以看出 cht.sh 不限于查代码,还可以查命令的用法。

+

例2:查函数

再举个例子,假如我不知道 go 中有没有能够判断字符串中是否包含某个字符的函数,可以使用:

+
1
cht.sh go string contain
+
+ +

例3:查实现

或者我想知道 go 中如何反转一个 list

+
1
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 到其他语言的情况下临时查询其他语言的用法,可以通过以 / 开头 临时指定语言:

+
+ +

更多用法可以参考项目的 README.md

https://github.com/chubin/cheat.sh

+ +
+ + + + + + + +
+ + + + + + +
+ +
+ + + +
+ + + + + + + + +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2020/database-isolation-levels/0.jpg b/2020/database-isolation-levels/0.jpg new file mode 100644 index 0000000000..10a94f4c3c Binary files /dev/null and b/2020/database-isolation-levels/0.jpg differ diff --git a/2020/database-isolation-levels/1.jpg b/2020/database-isolation-levels/1.jpg new file mode 100644 index 0000000000..774819f185 Binary files /dev/null and b/2020/database-isolation-levels/1.jpg differ diff --git a/2020/database-isolation-levels/2.png b/2020/database-isolation-levels/2.png new file mode 100644 index 0000000000..66a0d952bd Binary files /dev/null and b/2020/database-isolation-levels/2.png differ diff --git a/2020/database-isolation-levels/3.png b/2020/database-isolation-levels/3.png new file mode 100644 index 0000000000..7bb441dbfc Binary files /dev/null and b/2020/database-isolation-levels/3.png differ diff --git a/2020/database-isolation-levels/4.png b/2020/database-isolation-levels/4.png new file mode 100644 index 0000000000..c5b3fb1a1c Binary files /dev/null and b/2020/database-isolation-levels/4.png differ diff --git a/2020/database-isolation-levels/5.png b/2020/database-isolation-levels/5.png new file mode 100644 index 0000000000..762952ad28 Binary files /dev/null and b/2020/database-isolation-levels/5.png differ diff --git a/2020/database-isolation-levels/6.png b/2020/database-isolation-levels/6.png new file mode 100644 index 0000000000..39d4c85767 Binary files /dev/null and b/2020/database-isolation-levels/6.png differ diff --git a/2020/database-isolation-levels/index.html b/2020/database-isolation-levels/index.html new file mode 100644 index 0000000000..050d3fafbb --- /dev/null +++ b/2020/database-isolation-levels/index.html @@ -0,0 +1,543 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 数据库隔离级别简介 | 贾攀的流水账 + + + + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

贾攀的流水账

+ +
+

Panmax's Blog

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+ + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ 数据库隔离级别简介 +

+ + +
+ + + + +
+ + +
+ +

数据库行业有四种常见的隔离级别,分别是 RU、RC、RR、SERIALIZABLE,其中用到最多的是 RR 和 RR。下边分别看一下这四种隔离级别的异同。

+

RU(READ-UNCOMMITTED) - 能读到未提交的数据

RU 级别,实际上就是完全不隔离。每个进行中事务的中间状态,对其他事务都是可见的,所以有可能会出现「脏读」。

+

RU 举例

+ +

用户1设置 x=3,在用户1的事务未提交之前,用户2 执行 get x 时却看到了 x=3

+

RC(READ-COMMITEED) - 能读到已提交的数据

RC 举例

+ + +

用户1设置 x=3,在用户1的事务未提交之前,用户2 执行 get x 操作依旧返回的时旧值 2

+

RR(REPEATABLE-READ) - 可重复读

RC 和 RR 唯一的区别在于“是否可重复读”:在一个事务执行过程中,它能不能读到其他已提交事务对数据的更新,如果能读到数据变化,就是“不可重复读”,否则就是“可重复读”。

+

RR 举例

继续上边的例子,如果用户2 读取 x 是在同一个事务内,那么永远读到的都是事务开始前x的值。也就是说每个事务都从数据库的一致性快照中读取数据。

+
+ +

在 RR 隔离级别下,在一个事务进行过程中,对于同一条数据,每次读到的结果总是相同的,无论其他会话是否已经更新了这条数据,这就是「可重复读」。

+

不可重复读导致的问题

+ +

假设用户在银行有 1000 块钱,分别存放在两个账户上,每个账户 500。现在有这样一笔转账交易从账户1转 100 到账户2。如果用户在他提交转账请求之后而银行系统执行转账的过程中间,来查看两个账户的余额,他有可能看到帐号1收到转账前的余额(500元),和帐号2完成钱款转出后的余额(400元)。对于用户来说,貌似他的账户总共只有 900 元,有 100 元消失了。

+

这种异常现象称为不可重复读(nonrepeatable read)或读倾斜(read skew)

+

SERIALIZABLE - 串行化

串行化隔离通常被认为是最强的隔离级别。它保证即使事物可能会并行执行,但最终的结果与每次一个即串行执行结果相同。不过由于这种隔离级别性能较差,所以在实际开发中很少被用到,以下是三种实现串行化的技术方案:

+
    +
  • 严格按照串行顺序执行
  • +
  • 两阶段锁定
  • +
  • 乐观并发控制技术
  • +
+

隔离级别的要点:

脏读

客户端读到了其他客户端未提交的写入。

+

脏写

客户端覆盖了另一个客户端尚未提交的写入。

+

读倾斜(不可重复读)

客户端在不同时间点看到了不同值。

+

更新丢失

两个客户端同时执行读-修改-写入操作序列,出现了其中一个覆盖了另一个的写入,但又没有包含对方最新值的情况,最终导致了部分修改发生了丢失。

+

写倾斜

事务首先查询数据,根据返回的结果而作出某些决定,然后修改数据库。当事务提交时,支持决定的前提条件已不再成立。

+

幻读

事务读取了某些符合查询条件的对象,同时另一个客户端执行写入,改变了先前的查询结果。

+

幻读这个概念有些抽象,举例说明一下:

+
+ +
    +
  • 用户1在一个会话中开启一个事务,准备插入一条 ID 为 1000 的流水记录。查询一下当前流水,不存在 ID 为 1000 的记录,可以安全地插入数据。
  • +
  • 这时候,另外一个会话抢先插入了这条 ID 为 1000 的流水记录。
  • +
  • 然后用户1再执行相同的插入语句时,就会报主键冲突错误,但是由于事务的隔离性,它执行查询的时候,却查不到这条 ID 为 1000 的流水,就像出现了“幻觉”一样,这就是幻读。
  • +
+

在实际业务中,很少能遇到幻读,即使遇到,也基本不会影响到数据准确性。

+

最后用一张表格总结一下上边的内容:

+ +
    +
  • RU 级别隔即没有任何隔离,存在脏读、不可重复读、幻读的风险
  • +
  • RC 可以避免脏读,还是会存在不可重复读和幻读
  • +
  • NR 可以避免脏读和不可重复读(通常通过一致性快照),但无法避免幻读
  • +
  • 只有 SERIALIZABLE 才可以避免幻读
  • +
+ +
+ + + + + + + +
+ + + + + + +
+ +
+ + + +
+ + + + + + + + +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2020/deployment-strategy-introduce/0.jpg b/2020/deployment-strategy-introduce/0.jpg new file mode 100644 index 0000000000..f5fdc200e4 Binary files /dev/null and b/2020/deployment-strategy-introduce/0.jpg differ diff --git a/2020/deployment-strategy-introduce/1.png b/2020/deployment-strategy-introduce/1.png new file mode 100644 index 0000000000..f8f55a8cf2 Binary files /dev/null and b/2020/deployment-strategy-introduce/1.png differ diff --git a/2020/deployment-strategy-introduce/10.png b/2020/deployment-strategy-introduce/10.png new file mode 100644 index 0000000000..33bbd9dd4e Binary files /dev/null and b/2020/deployment-strategy-introduce/10.png differ diff --git a/2020/deployment-strategy-introduce/2.png b/2020/deployment-strategy-introduce/2.png new file mode 100644 index 0000000000..410fc383e9 Binary files /dev/null and b/2020/deployment-strategy-introduce/2.png differ diff --git a/2020/deployment-strategy-introduce/3.png b/2020/deployment-strategy-introduce/3.png new file mode 100644 index 0000000000..9b77c676f5 Binary files /dev/null and b/2020/deployment-strategy-introduce/3.png differ diff --git a/2020/deployment-strategy-introduce/4.png b/2020/deployment-strategy-introduce/4.png new file mode 100644 index 0000000000..38f58705c6 Binary files /dev/null and b/2020/deployment-strategy-introduce/4.png differ diff --git a/2020/deployment-strategy-introduce/5.png b/2020/deployment-strategy-introduce/5.png new file mode 100644 index 0000000000..63295d714c Binary files /dev/null and b/2020/deployment-strategy-introduce/5.png differ diff --git a/2020/deployment-strategy-introduce/6.png b/2020/deployment-strategy-introduce/6.png new file mode 100644 index 0000000000..842f90e41d Binary files /dev/null and b/2020/deployment-strategy-introduce/6.png differ diff --git a/2020/deployment-strategy-introduce/7.png b/2020/deployment-strategy-introduce/7.png new file mode 100644 index 0000000000..1f01fd7804 Binary files /dev/null and b/2020/deployment-strategy-introduce/7.png differ diff --git a/2020/deployment-strategy-introduce/8.png b/2020/deployment-strategy-introduce/8.png new file mode 100644 index 0000000000..a3fb400c48 Binary files /dev/null and b/2020/deployment-strategy-introduce/8.png differ diff --git a/2020/deployment-strategy-introduce/9.png b/2020/deployment-strategy-introduce/9.png new file mode 100644 index 0000000000..837c62b7f4 Binary files /dev/null and b/2020/deployment-strategy-introduce/9.png differ diff --git a/2020/deployment-strategy-introduce/index.html b/2020/deployment-strategy-introduce/index.html new file mode 100644 index 0000000000..bd7c6fa1c1 --- /dev/null +++ b/2020/deployment-strategy-introduce/index.html @@ -0,0 +1,614 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 常见部署策略介绍 | 贾攀的流水账 + + + + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

贾攀的流水账

+ +
+

Panmax's Blog

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+ + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ 常见部署策略介绍 +

+ + +
+ + + + +
+ + +
+ +

在持续交付和大规模互联网应用流行前,企业一般采用手动的方式进行部署,通常选择活动用户最少的时间(如周末或者凌晨),并且告诉大家需要一段时间来维护系统。在这段时间内,运维团队会停止旧的版本,部署新的版本并检查一切是否恢复正常。

+

但是现在,由于微服务的出现,我们会不断地将各服务的新版本部署到生产环境中,不能简单的认为部署就意味着停机,因为这样系统会一直有不同部分处于停机状态,我们需要考虑新的部署策略。

+

本文将介绍 6 种常见的部署策略:单目标部署、一次性全部部署、最小服务部署、滚动部署、蓝/绿部署、金丝雀部署,每种策略背后的理念都如其名。

+

术语

为了更好地介绍和对比这些策略,我们先来说明一组术语:

+

期望的实例数量

这是当服务功能完全正常时,预计将运行的服务副本数量。

+

期望的实例数量简写为:desired

+

如:desired=3,表示在任何一次部署中,需要将 3 个旧版本的服务实例更新为 3 个新版本的服务实例。

+

最小的健康实例数量

当删除旧实例、启动新实例的过程中,我们希望至少有一些实例是处于健康状态(无论是旧实例还是新实例),这样可以保证系统能够最低限度地提供服务。

+

最小的健康实例数量简写为:minimum

+

最大的实例数量

有时我们希望在删除旧实例前,先启动一些新版本的服务实例,以便减少服务部署过程中的停机时间,这意味着我们需要更多的资源。通过限制最大实例数量,我们同时也限制了部署过程中最大资源使用量。

+

最大的实例数量简写为:maximum

+

图表元素说明

+

对于每个策略,我们通过一张图来表示部署过程中的事件连续性。

+

表示旧版本的服务实例

+
+ +

表示正在启动、尚不可用的新版本实例

+
+ +

表示新版本的实例

+
+ + +

单目标部署

这是最简单的策略,也是需要资源最少的策略。在这种策略下,我们可以假设服务只有一个正在运行的实例,无论何时都必须先停止它,然后再部署新的实例。这意味着服务会存在中断,但是不需要额外的资源

+

单目标部署策略配置参数为:

+
    +
  • desired: 1
  • +
  • minimum: 0%
  • +
  • maximum: 0%
  • +
+

下图展示了单目标部署策略的实施步骤:

+
+ +
    +
  • 开始:存在一个旧版本的实例。
  • +
  • 步骤1:该实例被新版本实例替换,在新实例完全启动前,服务实际上不可用。
  • +
  • 结束:新实例启动完成,可以开始接收外部请求。
  • +
+

一次性全部部署

改策略类似于单目标部署策略,唯一的区别是我们可以拥有任意固定数量的实例,而不是只有一个实例。和单目标部署的情况一样,一次性全部部署策略升级期间不需要额外的资源,但是也存在服务中断

+

一次性全部部署策略配置参数为:

+
    +
  • desired: 5
  • +
  • minimum: 0%
  • +
  • maximum: 0%
  • +
+
+ +
    +
  • 开始:存在 5 个旧版本的实例。
  • +
  • 步骤1:同时停止所有 5 个实例,替换为 5 个新版本的实例,在新版本启动之前,该服务实际上不可用。
  • +
  • 结束:5个新实例启动完成,可以开始接收外部请求。
  • +
+

最小服务部署

前边两个策略的问题在于,它们都会中断服务。我们可以调整策略来改善这一点。

+

最小服务部署表示确保始终存在着一部分健康的服务实例,我们可以先将一部分实例进行更新,等它们完成启动后再去更新另一部分旧实例。可以不断重复这个过程,直到所有的旧实例都被新的实例所替换。

+

这种方式可以在不需要额外资源的情况下避免服务中断,但风险是这些存活的实例需要能够承受住额外的流量。

+

最小服务部署策略配置参数为:

+
    +
  • desired: 5
  • +
  • minimum: 40%(也可以用绝对值:2)
  • +
  • maximum: 0%
  • +
+
+ +
    +
  • 开始:存在 5 个旧版本的实例。
  • +
  • 步骤1:由于我们要求 minimum 最小值为 40%,也就是至少要保留 2 个实例一直提供服务,所以只能先停止 3 个实例并将它们升级成新版本。
  • +
  • 步骤2:新实例完全启动后,可以停止之前的 2 个旧实例,并将它们升级为新的版本。
  • +
  • 结束:所有新实例都处于正常运行状态。
  • +
+

滚动部署

我们可以将滚动部署看作最小服务部署的另一种形式,不过它的重点不在于健康实例的最小数量,而在于停止实例的最大数量。

+

滚动部署最典型的情况是将停止实例的最大数量设置为 1,也就是任意时刻只有 1 个实例处于更新过程中。

+

与最小服务部署相比,滚动部署的最主要有点在于,通过限制同时停止实例的数量,我们可以控制需要保留多少实例来承接额外的负载,它的缺点是部署需要更长的时间

+

最小服务部署策略配置参数为:

+
    +
  • desired: 5
  • +
  • minimum: 80%(也可以用绝对值:4)
  • +
  • maximum: 0%
  • +
+
+ +
    +
  • 开始:存在 5 个旧版本实例。
  • +
  • 步骤1:停止其中一个实例并将它替换为新的实例。
  • +
  • 步骤2:当步骤1中启动的实例完成启动后,停止另一个旧实例,将其也替换为新的实例。
  • +
  • 步骤3、4、5:对其余实例重复相同的过程。
  • +
  • 结束:所有的新实例现在都可以正常运行。
  • +
+

注:因为滚动部署相当于最小服务部署的另一种形式,所以 minimum = desired - 1

+

蓝/绿部署

蓝/绿部署是微服务领域种最受欢迎的一种部署策略,之前介绍过的最小服务和滚动部署存在两个缺点:1)升级期间承担总负载的健康实例数量会减少。2)部署期间,生产环境种会混合新旧两个版本的应用程序。

+

蓝/绿部署无法简单地通过组合 desired、minimum、maximum 这几个参数来完成,它要求只有当所有的新实例都准备好的时候,用户才能访问新版本的服务,同时所有的旧实例立即变为不可用。为了实现这一目标,我们需要控制请求路由服务编排

+
+ +
    +
  • 开始:存在多个旧版本的实例,请求通过负载均衡器/路由器被发送到旧版本的服务。
  • +
  • 步骤1:创建多个新实例,这些实例不可访问,负载均衡器/路由器仍将所有请求发送到旧的实例。
  • +
  • 步骤2:新实例启动完成可以处理请求了,但是尚未向他们发送任何请求。
  • +
  • 步骤3:重新配置负载均衡器/路由器,将所有接收的请求转发到新版本的服务。这个过程几乎是瞬间完成的,此时出了正在处理的请求外,没有新请求再被发送到旧版本的服务。
  • +
  • 结束:旧实例将已有请求处理完不再有用后,将他们停止。
  • +
+

蓝/绿部署提供了最佳的用户体验,但是代价是增加了复杂性,以及占用了更多资源

+

金丝雀部署

金丝雀部署是另一种无法通过组合 desired、minimum、maximum 参数实现的策略。这种策略允许我们尝试新版本的服务,但不完全承诺切换到新版本。这样我们只需在原来的旧版本的实例中添加一个新版本的实例,而不必停止其中的旧版本。负载均衡器会将一部分请求转发到金丝雀实例上,我们可以通过检查日志、指标来了解新实例的运行情况。

+

金丝雀部署可以分为两个步骤执行:

+
+ +
    +
  • 开始:存在多个旧版本的实例。
  • +
  • 步骤1:创建一个新版本的实例,不删除任何旧版本的实例。
  • +
  • 结束:新的实例启动完成并正常运行,可以与旧的实例一起提供服务。
  • +
+

金丝雀实例有时需要较长的时间,才能充分观察到它在新环境下的运行状况,在这个过程中,我们可能会部署其他的新版本,这时需要确保只重新部署金丝雀意外的实例。

+

真正用到金丝雀的实例情况非常少,如果我们只想向一部分用户开放新功能,可以使用功能开关来实现。金丝雀部署的另一个好处是可以测试一些底层的配置变化,例如日志、指标框架、垃圾回收或者新版 jvm 等。

+

不同策略的特点及代价总结

+ + + +
+ + + + + + + +
+ + + + + + +
+ +
+ + + +
+ + + + + + + + +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2020/do-not-run-database-in-docker/0.jpg b/2020/do-not-run-database-in-docker/0.jpg new file mode 100644 index 0000000000..93149c9910 Binary files /dev/null and b/2020/do-not-run-database-in-docker/0.jpg differ diff --git a/2020/do-not-run-database-in-docker/index.html b/2020/do-not-run-database-in-docker/index.html new file mode 100644 index 0000000000..8da6046184 --- /dev/null +++ b/2020/do-not-run-database-in-docker/index.html @@ -0,0 +1,506 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 为什么不应该使用 Docker 部署数据库 | 贾攀的流水账 + + + + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

贾攀的流水账

+ +
+

Panmax's Blog

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+ + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ 为什么不应该使用 Docker 部署数据库 +

+ + +
+ + + + +
+ + +
+ +

服务容器化变得越来越流行,如今大部分的 Web 服务会首选部署在容器中。容器的优点是否也适用于部署数据库?

+

很多文章在分析这个问题时会站在:性能、网络、资源隔离等方面来考虑。比如提到多加一层(Union FS)会导致性能下降甚至数据不可靠、Docker 在网络方面的诟病、Docker 的资源隔离不适合用于数据库(同时在一台机器上启动多个数据库实例,共享同一份数据,但两个实例由于隔离互相不可见,就会导致数据混乱的问题)。

+

我并没有找到 Union FS 会让性能下降多少的性能测试,网络方面虽然遇到过坑但也都能解决,资源隔离通常使用端口号进行互斥即可,保证只有一个实例运行。

+

Docker 的使用场景并不适用于数据库组件

上边那些并不是最核心的问题,我认为最核心的是 Docker 的使用场景并不适用于数据库组件。

+

我们来看一下使用 Docker 带来优势:

+
    +
  1. 标准化应用发布:Docker包含了运行环境和可执行程序,可以跨平台和主机使用;
  2. +
  3. 节约时间,快速部署和启动:VM 启动一般是分钟级,Docker容器启动是秒级;
  4. +
  5. 方便构建基于SOA架构或微服务架构的系统:通过服务编排,更好的松耦合;
  6. +
  7. 节约成本:以前一个虚拟机至少需要几个G的磁盘空间,Docker容器可以减少到MB级;
  8. +
  9. 方便持续集成:通过与代码进行关联使持续集成非常方便;
  10. +
  11. 可以作为集群系统的轻量主机或节点:在IaaS平台上,已经出现了CaaS,通过容器替代原来的主机。
  12. +
+

以上提到的大多数优势并不适用于数据库的运行环境:数据库通常是长期运行的,数据完整性是重中之重。我们不需要数据库自动扩容(在 Docker 中水平伸缩只能用于无状态计算服务,而不是数据库)、也不需要持续更新数据库的代码来进行持续集成。

+

数据库版本升级

除了场景不适合之外,另一个问题是数据库软件版本升级。对于无状态应用或者数据库的小版本更新来说,直接修改 Dockerfile 中的版本号并重新构建、重启即可完成升级,但数据库的大版本升级就没有这么简单了,大版本升级数据库版本会伴随数据存储结构的更新,仅仅升级版本是不行的。通常数据库提供商会提供相应的命令来让我们对数据库进行升级,但这样做的前提是数据库不能运行在容器中(需要进入容器才能执行命令、软件版本无法持久化)。

+

在开发环境中通过 Docker 运行数据库

凡事也不是绝对的,在开发环境中通过 Docker 来运行数据库就是个不错的选择。或者将它用于数据量不大、对可靠性要求不是那么高并且所有东西都放在单机中运行的项目中也是可以的,不过前提是要做好数据的日常备份工作。

+ +
+ + + + + + + +
+ + + + + + +
+ +
+ + + +
+ + + + + + + + +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2020/docker-component/index.html b/2020/docker-component/index.html new file mode 100644 index 0000000000..f07aec0419 --- /dev/null +++ b/2020/docker-component/index.html @@ -0,0 +1,498 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 通过 Docker 部署常用组件 | 贾攀的流水账 + + + + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

贾攀的流水账

+ +
+

Panmax's Blog

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+ + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ 通过 Docker 部署常用组件 +

+ + +
+ + + + +
+ + +

日常开发中,会用到一些数据库之类的基础组件,通过源码或者安装包的方式进行部署有时会比较麻烦,这种情况下可以用 Docker 来部署,以下是我积累的一些常用组件的启动命令。

+

MySQL

1
2
3
4
5
6
docker run -d --restart=always --env TZ='Asia/Shanghai' \
--name mysql \
-v /data/mysql:/var/lib/mysql \
-e MYSQL_ROOT_PASSWORD=MSIfUCuL5XCfGIS0SF6y \
-p 13306:3306 \
mariadb:10.2 --character-set-server=utf8mb4 --collation-server=utf8mb4_unicode_ci --max_connections=1024
+

Redis

1
2
3
4
5
6
docker run -d --restart=always \
--name redis \
-v /data/redis:/data \
-p 16379:6379 \
redis:4.0 \
--requirepass "D0mD4dGLXdSo3rFOz7kG8"
+

Minio

1
2
3
4
5
6
7
docker run -d --restart=always \
--name minio \
-p 19000:9000 \
-e "MINIO_ACCESS_KEY=Yltu9cY5Fa6T7BimY9" \
-e "MINIO_SECRET_KEY=Af94ajEW3qyxvXR5pVLiNTJWwY3V" \
-v /data/minio:/data \
minio/minio:RELEASE.2020-03-14T02-21-58Z server /data
+

Neo4j

1
2
3
4
5
6
7
8
9
docker run -d --restart=always \
--memory 2g --cpus 2 \
--name neo4j \
-v /data/neo4j:/data \
-e NEO4J_AUTH=neo4j/DPHSr195sqVQMDmKya \
-e NEO4J_ACCEPT_LICENSE_AGREEMENT=yes \
-p 17474:7474 \
-p 17687:7687 \
neo4j:3.5.16-enterprise
+

Zookeeper

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
docker-compose.yml

version: '3'
services:
zookeeper:
image: wurstmeister/zookeeper
restart: always
ports:
- "12181:2181"
kafka:
image: wurstmeister/kafka:2.11-1.1.1
restart: always
depends_on: [ zookeeper ]
ports:
- "19092:9092"
environment:
KAFKA_ADVERTISED_HOST_NAME: 192.168.5.58
KAFKA_ZOOKEEPER_CONNECT: zookeeper:2181
KAFKA_LOG_DIRS: /logs
volumes:
- /var/run/docker.sock:/var/run/docker.sock
- /data/kafka/logs:/logs
+

YAPI

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
docker-compose.yml

version: '2.1'
services:
yapi:
image: mrjin/yapi:latest
container_name: yapi
environment:
- VERSION=1.5.14
- LOG_PATH=/tmp/yapi.log
- HOME=/home
- PORT=3000
- ADMIN_EMAIL=yapi@posbao.net
- DB_SERVER=mongo
- DB_NAME=yapi
- DB_PORT=27017
restart: always
ports:
- 3000:3000
volumes:
- /data/yapi/log:/home/vendors/log
depends_on:
- mongo
entrypoint: "bash /wait-for-it.sh mongo:27017 -- entrypoint.sh"
networks:
- yapi
mongo:
image: mongo
container_name: yapi_mongo
restart: always
volumes:
- /data1/yapi/mongodb:/data/db
networks:
- yapi

networks:
yapi: {}
+

zipkin

1
docker run --restart=always --name zipkin -d -p 9411:9411 openzipkin/zipkin
+

xxl-job-admin

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
docker-compose.yml

version: "2.1"

services:
admin:
environment:
PARAMS: "--server.port=7995 --spring.datasource.url=jdbc:mysql://mysql:3306/xxl_job?Unicode=true&characterEncoding=UTF-8 --spring.datasource.username=root --spring.datasource.password=123456"
image: xuxueli/xxl-job-admin:2.1.2
restart: always
ports:
- 7995:7995
mysql:
image: mariadb:10.2
restart: always
volumes:
- ./tables_xxl_job.sql:/docker-entrypoint-initdb.d/tables_xxl_job.sql
- /data/xxl-job/mysql:/var/lib/mysql
environment:
TZ: "Asia/Shanghai"
MYSQL_ROOT_PASSWORD: 123456
command:
[
"--character-set-server=utf8mb4",
"--collation-server=utf8mb4_unicode_ci",
"--max_connections=1024",
]
+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
tables_xxl_job.sql

CREATE database if NOT EXISTS `xxl_job` default character set utf8mb4 collate utf8mb4_unicode_ci;
use `xxl_job`;

SET NAMES utf8mb4;

CREATE TABLE `xxl_job_info` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`job_group` int(11) NOT NULL COMMENT '执行器主键ID',
`job_cron` varchar(128) NOT NULL COMMENT '任务执行CRON',
`job_desc` varchar(255) NOT NULL,
`add_time` datetime DEFAULT NULL,
`update_time` datetime DEFAULT NULL,
`author` varchar(64) DEFAULT NULL COMMENT '作者',
`alarm_email` varchar(255) DEFAULT NULL COMMENT '报警邮件',
`executor_route_strategy` varchar(50) DEFAULT NULL COMMENT '执行器路由策略',
`executor_handler` varchar(255) DEFAULT NULL COMMENT '执行器任务handler',
`executor_param` varchar(512) DEFAULT NULL COMMENT '执行器任务参数',
`executor_block_strategy` varchar(50) DEFAULT NULL COMMENT '阻塞处理策略',
`executor_timeout` int(11) NOT NULL DEFAULT '0' COMMENT '任务执行超时时间,单位秒',
`executor_fail_retry_count` int(11) NOT NULL DEFAULT '0' COMMENT '失败重试次数',
`glue_type` varchar(50) NOT NULL COMMENT 'GLUE类型',
`glue_source` mediumtext COMMENT 'GLUE源代码',
`glue_remark` varchar(128) DEFAULT NULL COMMENT 'GLUE备注',
`glue_updatetime` datetime DEFAULT NULL COMMENT 'GLUE更新时间',
`child_jobid` varchar(255) DEFAULT NULL COMMENT '子任务ID,多个逗号分隔',
`trigger_status` tinyint(4) NOT NULL DEFAULT '0' COMMENT '调度状态:0-停止,1-运行',
`trigger_last_time` bigint(13) NOT NULL DEFAULT '0' COMMENT '上次调度时间',
`trigger_next_time` bigint(13) NOT NULL DEFAULT '0' COMMENT '下次调度时间',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

CREATE TABLE `xxl_job_log` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`job_group` int(11) NOT NULL COMMENT '执行器主键ID',
`job_id` int(11) NOT NULL COMMENT '任务,主键ID',
`executor_address` varchar(255) DEFAULT NULL COMMENT '执行器地址,本次执行的地址',
`executor_handler` varchar(255) DEFAULT NULL COMMENT '执行器任务handler',
`executor_param` varchar(512) DEFAULT NULL COMMENT '执行器任务参数',
`executor_sharding_param` varchar(20) DEFAULT NULL COMMENT '执行器任务分片参数,格式如 1/2',
`executor_fail_retry_count` int(11) NOT NULL DEFAULT '0' COMMENT '失败重试次数',
`trigger_time` datetime DEFAULT NULL COMMENT '调度-时间',
`trigger_code` int(11) NOT NULL COMMENT '调度-结果',
`trigger_msg` text COMMENT '调度-日志',
`handle_time` datetime DEFAULT NULL COMMENT '执行-时间',
`handle_code` int(11) NOT NULL COMMENT '执行-状态',
`handle_msg` text COMMENT '执行-日志',
`alarm_status` tinyint(4) NOT NULL DEFAULT '0' COMMENT '告警状态:0-默认、1-无需告警、2-告警成功、3-告警失败',
PRIMARY KEY (`id`),
KEY `I_trigger_time` (`trigger_time`),
KEY `I_handle_code` (`handle_code`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

CREATE TABLE `xxl_job_log_report` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`trigger_day` datetime DEFAULT NULL COMMENT '调度-时间',
`running_count` int(11) NOT NULL DEFAULT '0' COMMENT '运行中-日志数量',
`suc_count` int(11) NOT NULL DEFAULT '0' COMMENT '执行成功-日志数量',
`fail_count` int(11) NOT NULL DEFAULT '0' COMMENT '执行失败-日志数量',
PRIMARY KEY (`id`),
UNIQUE KEY `i_trigger_day` (`trigger_day`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

CREATE TABLE `xxl_job_logglue` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`job_id` int(11) NOT NULL COMMENT '任务,主键ID',
`glue_type` varchar(50) DEFAULT NULL COMMENT 'GLUE类型',
`glue_source` mediumtext COMMENT 'GLUE源代码',
`glue_remark` varchar(128) NOT NULL COMMENT 'GLUE备注',
`add_time` datetime DEFAULT NULL,
`update_time` datetime DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

CREATE TABLE `xxl_job_registry` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`registry_group` varchar(50) NOT NULL,
`registry_key` varchar(255) NOT NULL,
`registry_value` varchar(255) NOT NULL,
`update_time` datetime DEFAULT NULL,
PRIMARY KEY (`id`),
KEY `i_g_k_v` (`registry_group`,`registry_key`,`registry_value`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

CREATE TABLE `xxl_job_group` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`app_name` varchar(64) NOT NULL COMMENT '执行器AppName',
`title` varchar(12) NOT NULL COMMENT '执行器名称',
`order` int(11) NOT NULL DEFAULT '0' COMMENT '排序',
`address_type` tinyint(4) NOT NULL DEFAULT '0' COMMENT '执行器地址类型:0=自动注册、1=手动录入',
`address_list` varchar(512) DEFAULT NULL COMMENT '执行器地址列表,多地址逗号分隔',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

CREATE TABLE `xxl_job_user` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`username` varchar(50) NOT NULL COMMENT '账号',
`password` varchar(50) NOT NULL COMMENT '密码',
`role` tinyint(4) NOT NULL COMMENT '角色:0-普通用户、1-管理员',
`permission` varchar(255) DEFAULT NULL COMMENT '权限:执行器ID列表,多个逗号分割',
PRIMARY KEY (`id`),
UNIQUE KEY `i_username` (`username`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

CREATE TABLE `xxl_job_lock` (
`lock_name` varchar(50) NOT NULL COMMENT '锁名称',
PRIMARY KEY (`lock_name`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;


INSERT INTO `xxl_job_group`(`id`, `app_name`, `title`, `order`, `address_type`, `address_list`) VALUES (1, 'xxl-job-executor-sample', '示例执行器', 1, 0, NULL);
INSERT INTO `xxl_job_info`(`id`, `job_group`, `job_cron`, `job_desc`, `add_time`, `update_time`, `author`, `alarm_email`, `executor_route_strategy`, `executor_handler`, `executor_param`, `executor_block_strategy`, `executor_timeout`, `executor_fail_retry_count`, `glue_type`, `glue_source`, `glue_remark`, `glue_updatetime`, `child_jobid`) VALUES (1, 1, '0 0 0 * * ? *', '测试任务1', '2018-11-03 22:21:31', '2018-11-03 22:21:31', 'XXL', '', 'FIRST', 'demoJobHandler', '', 'SERIAL_EXECUTION', 0, 0, 'BEAN', '', 'GLUE代码初始化', '2018-11-03 22:21:31', '');
INSERT INTO `xxl_job_user`(`id`, `username`, `password`, `role`, `permission`) VALUES (1, 'admin', 'e10adc3949ba59abbe56e057f20f883e', 1, NULL);
INSERT INTO `xxl_job_lock` ( `lock_name`) VALUES ( 'schedule_lock');

commit;
+

nexus

1
2
docker run -d --restart=always -p 8081:8081 \
--name nexus -v /data/nexus-data:/nexus-data sonatype/nexus3
+

gitlab-runner

1
2
3
4
5
6
7
8
9
10
11
version: "3"
services:
app:
image: gitlab/gitlab-runner
container_name: gitlab-runner-docker
restart: always
volumes:
- ./config:/etc/gitlab-runner
- /var/run/docker.sock:/var/run/docker.sock
- ./id_rsa:/home/gitlab-runner/.ssh/id_rsa
- ./known_hosts:/home/gitlab-runner/.ssh/known_hosts
+ +
+ + + + + + + +
+ + + + + + +
+ +
+ + + +
+ + + + + + + + +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2020/docker-leak-filling/0.png b/2020/docker-leak-filling/0.png new file mode 100644 index 0000000000..9495240738 Binary files /dev/null and b/2020/docker-leak-filling/0.png differ diff --git a/2020/docker-leak-filling/1.png b/2020/docker-leak-filling/1.png new file mode 100644 index 0000000000..e18b38d7a8 Binary files /dev/null and b/2020/docker-leak-filling/1.png differ diff --git a/2020/docker-leak-filling/10.png b/2020/docker-leak-filling/10.png new file mode 100644 index 0000000000..24ecbf9021 Binary files /dev/null and b/2020/docker-leak-filling/10.png differ diff --git a/2020/docker-leak-filling/11.png b/2020/docker-leak-filling/11.png new file mode 100644 index 0000000000..6a5a5de721 Binary files /dev/null and b/2020/docker-leak-filling/11.png differ diff --git a/2020/docker-leak-filling/12.png b/2020/docker-leak-filling/12.png new file mode 100644 index 0000000000..c936f88f32 Binary files /dev/null and b/2020/docker-leak-filling/12.png differ diff --git a/2020/docker-leak-filling/13.png b/2020/docker-leak-filling/13.png new file mode 100644 index 0000000000..7684651411 Binary files /dev/null and b/2020/docker-leak-filling/13.png differ diff --git a/2020/docker-leak-filling/2.png b/2020/docker-leak-filling/2.png new file mode 100644 index 0000000000..3217e3bf0a Binary files /dev/null and b/2020/docker-leak-filling/2.png differ diff --git a/2020/docker-leak-filling/3.png b/2020/docker-leak-filling/3.png new file mode 100644 index 0000000000..a539c75b0c Binary files /dev/null and b/2020/docker-leak-filling/3.png differ diff --git a/2020/docker-leak-filling/4.png b/2020/docker-leak-filling/4.png new file mode 100644 index 0000000000..36e36f9154 Binary files /dev/null and b/2020/docker-leak-filling/4.png differ diff --git a/2020/docker-leak-filling/5.png b/2020/docker-leak-filling/5.png new file mode 100644 index 0000000000..4c1cfcc805 Binary files /dev/null and b/2020/docker-leak-filling/5.png differ diff --git a/2020/docker-leak-filling/6.png b/2020/docker-leak-filling/6.png new file mode 100644 index 0000000000..4bcad43255 Binary files /dev/null and b/2020/docker-leak-filling/6.png differ diff --git a/2020/docker-leak-filling/7.png b/2020/docker-leak-filling/7.png new file mode 100644 index 0000000000..9697cd73ba Binary files /dev/null and b/2020/docker-leak-filling/7.png differ diff --git a/2020/docker-leak-filling/8.png b/2020/docker-leak-filling/8.png new file mode 100644 index 0000000000..27b50e5abf Binary files /dev/null and b/2020/docker-leak-filling/8.png differ diff --git a/2020/docker-leak-filling/9.png b/2020/docker-leak-filling/9.png new file mode 100644 index 0000000000..806e9bd97b Binary files /dev/null and b/2020/docker-leak-filling/9.png differ diff --git a/2020/docker-leak-filling/index.html b/2020/docker-leak-filling/index.html new file mode 100644 index 0000000000..5daccb5f32 --- /dev/null +++ b/2020/docker-leak-filling/index.html @@ -0,0 +1,1250 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Docker 与虚拟化技术查漏补缺 | 贾攀的流水账 + + + + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

贾攀的流水账

+ +
+

Panmax's Blog

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+ + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ Docker 与虚拟化技术查漏补缺 +

+ + +
+ + + + +
+ + +

我的 Docker 使用经验都是通过在项目中的运用学到的,实际上已经可以满足日常所需了,但是自认为缺乏一些细节方面的知识,所以这几天通过阅读一本掘金小册《开发者必备的 Docker 实践指南》,进行了一次系统性学习,以下是我记录的一些我认为的重点和我之前不太了解或不熟悉的内容。

+

本文不适合作为 Docker 初学者学习的指南,适合于查漏补缺时的参考。

+
+

+

容器技术

所谓容器技术,指的是操作系统自身支持一些接口,能够让应用程序间可以互不干扰的独立运行,并且能够对其在运行中所使用的资源进行干预。

+

由于没有指令转换,运行在容器中的应用程序自身必须支持在真实操作系统上运行,也就是必须遵循硬件平台的指令规则。

+
    +
  • 容器技术提供了相对独立的应用程序运行的环境,也提供了资源控制的功能,所以我们依然可以归纳其为一种实现不完全的虚拟化技术
  • +
+

虚拟机 VS 容器

+
    +
  • 由于没有了虚拟操作系统虚拟机监视器这两个层次,大幅减少了应用程序运行带来的额外消耗。
  • +
  • 运行在容器虚拟化中的应用程序,在运行效率上与真实运行在物理平台上的应用程序不相上下。
  • +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
属性虚拟机Docker
启动速度分钟级秒级
硬盘使用GB 级MB 级
性能较低接近原生
普通机器支撑量几个数百个
+

Docker 技术实现

Docker 的实现,主要归结于三大技术:

+
    +
  • 命名空间 ( Namespaces )
      +
    • Linux 核心在 2.4 版本后逐渐引入的一项用于运行隔离的模块。
    • +
    +
  • +
  • 控制组 ( Control Groups )
      +
    • Linux 内核在 2.6 版本后逐渐引入的一项对计算机资源控制的模块。
    • +
    • CGroups 主要做的是硬件资源的隔离。
    • +
    +
  • +
  • 联合文件系统 ( Union File System )
      +
    • 联合文件系统 ( Union File System ) 是一种能够同时挂载不同实际文件或文件夹到同一目录,形成一种联合文件结构的文件系统。
    • +
    • 在 Docker 中,提供了一种对 UnionFS 的改进实现,也就是 AUFS ( Advanced Union File System )
    • +
    +
  • +
+

Docker 的理念

与其他虚拟化实现甚至其他容器引擎不同的是,Docker 推崇一种轻量级容器的结构

+
    +
  • 即一个应用一个容器
  • +
+

我们能用 Docker 做些什么

    +
  1. 更快、更一致的交付你的应用程序
  2. +
  3. 跨平台部署和动态伸缩
  4. +
  5. 让同样的硬件提供更多的产出能力
  6. +
+

Docker 核心

四大组成对象

+
    +
  • 镜像 ( Image )
      +
    • 可以理解为一个只读的文件包,其中包含了虚拟环境运行最原始文件系统的内容。
    • +
    +
  • +
  • 容器 ( Container )
      +
    • 如果把镜像理解为编程中的类,那么容器就可以理解为类的实例。
    • +
    +
  • +
  • 网络 ( Network )
  • +
  • 数据卷 ( Volume )
      +
    • 在 Docker 中,通过这几种方式进行数据共享或持久化的文件或目录,我们都称为数据卷 ( Volume )。
    • +
    +
  • +
+

Docker Engine

在 Docker Engine 中,实现了 Docker 技术中最核心的部分,也就是容器引擎这一部分。

+

docker daemon 和 docker CLI

Docker Engine 是由多个独立软件所组成的软件包。最核心的是 docker daemondocker CLI

+

+

在 docker daemon 管理容器等相关资源的同时,它也向外暴露了一套 RESTful API

+

+

docker daemon 和 docker CLI 所组成的,正是一个标准 C/S ( Client-Server ) 结构的应用程序。衔接这两者的,正是 docker daemon 所提供的这套 RESTful API

+

搭建 Docker 运行环境

Docker Engine 的稳定版固定为每三个月更新一次,而预览版则每月都会更新。

+

+

不论是稳定版还是预览版,它们都会以发布时的年月来命名版本号,例如如 17 年 3 月的版本,版本号就是 17.03。

+
    +
  • 在主要版本之外,Docker 官方也以解决 Bug 为主要目的,不定期发布次要版本。次要版本的版本号由主要版本和发布序号组成
      +
    • 如:17.03.2 就是对 17.03 版本的第二次修正。
    • +
    +
  • +
+

Docker 的环境依赖

以目前 Docker 官方主要维护的版本为例,我们需要使用基于 Linux kernel 3.10 以上版本的 Linux 系统来安装 Docker。

+

在Mac 和 Windows 中使用 Docker

Docker Desktop

Docker 官方为 Windows 和 macOS 系统单独开辟了一条产品线,名为 Docker Desktop,其定位是快速为开发者提供在 Windows 和 macOS 中运行 Docker 环境的工具。

+

Docker Desktop 的实现原理

既然 Windows 和 macOS 中没有 Docker 能够利用的 Linux 环境,那么我们需要提供一个 Linux 环境

+
    +
  • 在 Windows 中,通过 Hyper-V 实现虚拟化
      +
    • 对于 Windows 系统来说,安装 Docker for Windows 需要符合以下条件:
        +
      • 必须使用 Windows 10 Pro ( 专业版 )
      • +
      • 必须使用 64 bit 版本的 Windows
      • +
      +
    • +
    +
  • +
  • 在 macOS 中,通过 HyperKit 实现虚拟化
  • +
+

+

镜像与容器

容器的生命周期

+

Docker 容器的生命周期里分为五种状态:

+
    +
  • Created:容器已经被创建,容器所需的相关资源已经准备就绪,但容器中的程序还未处于运行状态。
  • +
  • Running:容器正在运行,也就是容器中的应用正在运行。
  • +
  • Paused:容器已暂停,表示容器中的所有程序都处于暂停 ( 不是停止 ) 状态。
  • +
  • Stopped:容器处于停止状态,占用的资源和沙盒环境都依然存在,只是容器中的应用程序均已停止。
  • +
  • Deleted:容器已删除,相关占用的资源及存储在 Docker 中的管理信息也都已释放和移除。
  • +
+

主进程

在 Docker 的设计中,容器的生命周期其实与容器中 PID 为 1 这个进程有着密切的关系。

+
    +
  • 当我们启动容器时,Docker 其实会按照镜像中的定义,启动对应的程序,并将这个程序的主进程作为容器的主进程 ( 也就是 PID 为 1 的进程 )。
  • +
  • 而当我们控制容器停止时,Docker 会向主进程发送结束信号,通知程序退出。
  • +
  • 而当容器中的主进程主动关闭时 ( 正常结束或出错停止 ),也会让容器随之停止。
  • +
+

写时复制 ( Copy on Write ) 机制

Docker 的写时复制与编程中的相类似,也就是在通过镜像运行容器时,并不是马上就把镜像里的所有内容拷贝到容器所运行的沙盒文件系统中,而是利用 UnionFS 将镜像以只读的方式挂载到沙盒文件系统中。只有在容器中发生对文件的修改时,修改才会体现到沙盒环境上。

+
    +
  • 也就是说,容器在创建和启动的过程中,不需要进行任何的文件系统复制操作,也不需要为容器单独开辟大量的硬盘空间
  • +
  • 采用写时复制机制来设计的 Docker,既保证了镜像在生成为容器时,以及容器在运行过程中,不会对自身造成修改。又借助剔除常见虚拟化在初始化时需要从镜像中拷贝整个文件系统的过程,大幅提高了容器的创建和启动速度。
  • +
  • 可以说,Docker 容器能够实现秒级启动速度,写时复制机制在其中发挥了举足轻重的作用。
  • +
+

运行和管理容器

管理容器

通过 docker ps 命令,可以罗列出 Docker 中的容器。

+
    +
  • 默认情况下,docker ps 列出的容器是处于运行中的容器,如果要列出所有状态的容器,需要增加 -a--all 选项。
  • +
  • CONTAINER IDIMAGECREATED、NAMES 分别表示容器 ID,容器所基于的镜像,容器的创建时间和容器的名称。
  • +
  • COMMAND 表示的是容器中主程序 ( 也就是与容器生命周期所绑定进程所关联的程序 ) 的启动命令,这条命令是在镜像内定义的,而容器的启动其实质就是启动这条命令。
  • +
  • STATUS 表示容器所处的状态,常见的状态表示有三种:
      +
    • Created 此时容器已创建,但还没有被启动过。
    • +
    • Up [ Time ] 这时候容器处于正在运行状态,而这里的 Time 表示容器从开始运行到查看时的时间。
    • +
    • Exited ([ Code ]) [ Time ] 容器已经结束运行,这里的 Code 表示容器结束运行时,主程序返回的程序退出码,而 Time 则表示容器结束到查看时的时间。
    • +
    +
  • +
+

进入容器

在开发过程中,我们更常使用它来作为我们进入容器的桥梁。

+
    +
  • 这里说的进入容器,就是通过 docker exec 命令来启动 sh 或 bash,并通过它们实现对容器内的虚拟环境的控制。
  • +
  • 由于 bash 的功能要比 sh 丰富,所以在能够使用 bash 的容器里,我们优先选择它作为控制台程序。
  • +
  • docker exec -it nginx bash
      +
    • -i ( –interactive ) 表示保持我们的输入流
        +
      • 只有使用它才能保证控制台程序能够正确识别我们的命令
      • +
      +
    • +
    • -t ( –tty ) 表示启用一个伪终端,形成我们与 bash 的交互
        +
      • 如果没有它,我们无法看到 bash 内部的执行结果
      • +
      +
    • +
    +
  • +
+

衔接到容器

Docker 为我们提供了一个 docker attach 命令,用于将当前的输入输出流连接到指定的容器上。

+
    +
  • docker attach nginx
  • +
  • 可以理解为我们将容器中的主程序转为了“前台”运行 ( 与 docker run 中的 -d 选项有相反的意思 )
  • +
  • 在实际开发中,由于 docker attach 限制较多,功能也不够强大,所以并没有太多用武之地。
  • +
+

为容器配置网络

在 Docker 网络中,有三个比较核心的概念:沙盒 ( Sandbox )、网络 ( Network )、端点 ( Endpoint )

+
    +
  • 沙盒提供了容器的虚拟网络栈
      +
    • 也就是之前所提到的端口套接字、IP 路由表、防火墙等的内容。
    • +
    • 隔离了容器网络与宿主机网络,形成了完全独立的容器网络环境。
    • +
    +
  • +
  • 网络可以理解为 Docker 内部的虚拟子网
      +
    • 网络内的参与者相互可见并能够进行通讯。
    • +
    • Docker 的这种虚拟网络也是与宿主机网络存在隔离关系的,其目的主要是形成容器间的安全通讯环境。
    • +
    +
  • +
  • 端点是位于容器或网络隔离墙之上的洞
      +
    • 其主要目的是形成一个可以控制的突破封闭的网络环境的出入口。
    • +
    • 当容器的端点与网络的端点形成配对后,就如同在这两者之间搭建了桥梁,便能够进行数据传输了。
    • +
    +
  • +
+

这三者形成了 Docker 网络的核心模型,也就是容器网络模型 ( Container Network Model )。

+

网络驱动的种类

目前 Docker 官方为我们提供了五种 Docker 网络驱动,分别是:Bridge Driver、Host Driver、Overlay Driver、MacLan Driver、None Driver

+
    +
  • Bridge 网络是 Docker 容器的默认网络驱动
      +
    • 简而言之其就是通过网桥来实现网络通讯
    • +
    +
  • +
  • Overlay 网络是借助 Docker 集群模块 Docker Swarm 来搭建的跨 Docker Daemon 网络
      +
    • 我们可以通过它搭建跨物理主机的虚拟网络,进而让不同物理机中运行的容器感知不到多个物理机的存在。
    • +
    +
  • +
+

暴露端口

Docker 为容器网络增加了一套安全机制,只有容器自身允许的端口,才能被其他容器所访问。

+
    +
  • 这个容器自我标记端口可被访问的过程,我们通常称为暴露端口
  • +
+

端口的暴露可以通过 Docker 镜像进行定义,也可以在容器创建时进行定义。

+
    +
  • 在容器创建时进行定义的方法是借助 –expose 这个选项。
  • +
  • docker run -d --name mysql -e MYSQL_RANDOM_ROOT_PASSWORD=yes --expose 13306 --expose 23306 mysql:5.7
  • +
+

这里我们为 MySQL 暴露了 13306 和 23306 这两个端口,暴露后我们可以在 docker ps 中看到这两个端口已经成功的打开。

+
1
2
… PORTS                                       NAMES
… 3306/tcp, 13306/tcp, 23306/tcp, 33060/tcp mysql
+

创建网络

在 Docker 里,我们也能够创建网络,形成自己定义虚拟子网的目的。

+

docker network create -d bridge individual

+
    +
  • 通过 -d 选项我们可以为新的网络指定驱动的类型
      +
    • 其值可以是刚才我们所提及的 bridge、host、overlay、maclan、none,也可以是其他网络驱动插件所定义的类型
    • +
    • 这里我们使用的是 Bridge Driver ( 当我们不指定网络驱动时,Docker 也会默认采用 Bridge Driver 作为网络驱动 )。
    • +
    +
  • +
+

通过 docker network ls 或是 docker network list 可以查看 Docker 中已经存在的网络。

+

我们创建容器时,可以通过 --network 来指定容器所加入的网络

+
    +
  • 一旦这个参数被指定,容器便不会默认加入到 bridge 这个网络中了 ( 但是仍然可以通过 --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
    • +
    +
  • +
+

端口映射

在实际使用中,还有一个非常常见的需求,就是我们需要在容器外通过网络访问容器中的应用。

+

+

通过 Docker 端口映射功能,我们可以把容器的端口映射到宿主操作系统的端口上,当我们从外部访问宿主操作系统的端口时,数据请求就会自动发送给与之关联的容器端口。

+
    +
  • 要映射端口,我们可以在创建容器时使用 -p 或者是 –publish 选项。
  • +
  • docker 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

+
    +
  • Bind Mount 能够直接将宿主操作系统中的目录和文件挂载到容器内的文件系统中,通过指定容器外的路径和容器内的路径,就可以形成挂载映射关系,在容器内外对文件的读写,都是相互可见的。
  • +
  • Volume 也是从宿主操作系统中挂载目录到容器内,只不过这个挂载的目录由 Docker 进行管理,我们只需要指定容器内的目录,不需要关心具体挂载到了宿主操作系统中的哪里。
  • +
  • Tmpfs Mount 支持挂载系统内存中的一部分到容器的文件系统里,不过由于内存和容器的特征,它的存储并不是持久的,其中的内容会随着容器的停止而消失。
  • +
+

+

挂载文件到容器

使用 -v--volume 来挂载宿主操作系统目录的形式是 -v <host-path>:<container-path>--volume <host-path>:<container-path>,其中 host-pathcontainer-path 分别代表宿主操作系统中的目录和容器中的目录。

+
    +
  • 需要注意的是,为了避免混淆,Docker 这里强制定义目录时必须使用绝对路径,不能使用相对路径。
  • +
  • 能够指定目录进行挂载,也能够指定具体的文件来挂载
  • +
+

Docker 还支持以只读的方式挂载,通过只读方式挂载的目录和文件,只能被容器中的程序读取,但不接受容器中程序修改它们的请求。在挂载选项 -v 后再接上 :ro 就可以只读挂载了。

+
    +
  • docker run -d --name nginx -v /webapp/html:/usr/share/nginx/html:ro nginx:1.12
  • +
+

Bind Mount 常见场景:

+
    +
  • 当我们需要从宿主操作系统共享配置的时候。
      +
    • 对于一些配置项,我们可以直接从容器外部挂载到容器中,这利于保证容器中的配置为我们所确认的值,也方便我们对配置进行监控。
        +
      • 例如,遇到容器中时区不正确的时候,我们可以直接将操作系统的时区配置,也就是 /etc/timezone 这个文件挂载并覆盖容器中的时区配置。
      • +
      +
    • +
    +
  • +
  • 当我们需要借助 Docker 进行开发的时候。
      +
    • 虽然在 Docker 中,推崇直接将代码和配置打包进镜像,以便快速部署和快速重建。但这在开发过程中显然非常不方便,因为每次构建镜像需要耗费一定的时间,这些时间积少成多,就是对开发工作效率的严重浪费了。如果我们直接把代码挂载进入容器,那么我们每次对代码的修改都可以直接在容器外部进行。
    • +
    +
  • +
+

挂载临时文件目录

Tmpfs Mount 是一种特殊的挂载方式,它主要利用内存来存储数据。由于内存不是持久性存储设备,所以其带给 Tmpfs Mount 的特征就是临时性挂载。

+

挂载临时文件目录要通过 --tmpfs 这个选项来完成。

+
    +
  • 由于内存的具体位置不需要我们来指定,这个选项里我们只需要传递挂载到容器内的目录即可。
  • +
  • docker run -d --name webapp --tmpfs /webapp/cache webapp:latest
  • +
+

Tmpfs Mount 常见场景:

+
    +
  • 应用中使用到,但不需要进行持久保存的敏感数据,可以借助内存的非持久性和程序隔离性进行一定的安全保障。
  • +
  • 读写速度要求较高,数据变化量大,但不需要持久保存的数据,可以借助内存的高读写速度减少操作的时间。
  • +
+

使用数据卷

数据卷的本质其实依然是宿主操作系统上的一个目录,只不过这个目录存放在 Docker 内部,接受 Docker 的管理。

+
    +
  • 在使用数据卷进行挂载时,我们不需要知道数据具体存储在了宿主操作系统的何处,只需要给定容器中的哪个目录会被挂载即可。
  • +
  • docker 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
  • +
  • 前面提到了,-v 在定义绑定挂载时必须使用绝对路径,其目的主要是为了避免与数据卷挂载中命名这种形式的冲突。
  • +
+

数据卷常见场景:

+
    +
  • 当希望将数据在多个容器间共享时,利用数据卷可以在保证数据持久性和完整性的前提下,完成更多自动化操作。
  • +
  • 当我们希望对容器中挂载的内容进行管理时,可以直接利用数据卷自身的管理方法实现。
  • +
  • 使用远程服务器或云服务作为存储介质的时候,数据卷能够隐藏更多的细节,让整个过程变得更加简单。
  • +
+

共用数据卷

数据卷的另一大作用是实现容器间的目录共享,也就是通过挂载相同的数据卷,让容器之间能够同时看到并操作数据卷中的内容。

+
    +
  • docker 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 不会允许我们删除这个数据卷。
  • +
+

docker rm 删除容器的命令中,我们可以通过增加 -v 选项来删除容器关联的数据卷。

+
    +
  • docker rm -v webapp
  • +
+

Docker 向我们提供了 docker volume prune 命令,可以删除那些没有被容器引用的数据卷。

+

数据卷容器

数据卷容器,就是一个没有具体指定的应用,甚至不需要运行的容器,我们使用它的目的,是为了定义一个或多个数据卷并持有它们的引用。

+

由于不需要容器本身运行,因而找个简单的系统镜像都可以完成创建。

+
    +
  • docker create --name appdata -v /webapp/storage ubuntu
  • +
  • 在使用数据卷容器时,我们不建议再定义数据卷的名称,因为我们可以通过对数据卷容器的引用来完成数据卷的引用。
  • +
+

Docker 的 Network 是容器间的网络桥梁,如果做类比,数据卷容器就可以算是容器间的文件系统桥梁

+
    +
  • 我们可以像加入网络一样引用数据卷容器,只需要在创建新容器时使用专门的 --volumes-from 选项即可。
  • +
  • docker run -d --name webapp --volumes-from appdata webapp:latest
  • +
  • 引用数据卷容器时,不需要再定义数据卷挂载到容器中的位置,Docker 会以数据卷容器中的挂载定义将数据卷挂载到引用的容器中
  • +
+

备份和迁移数据卷

利用数据卷容器,我们能够更方便的对数据卷中的数据进行迁移。

+

数据备份、迁移、恢复的过程可以理解为对数据进行打包,移动到其他位置,在需要的地方解压的过程。

+
    +
  • 要备份数据,我们先建立一个临时的容器,将用于备份的目录和要备份的数据卷都挂载到这个容器上。
      +
    • docker 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 目录。
    • +
    +
  • +
+

挂载主要有三种目的:

    +
  1. 将程序的配置通过挂载的方式覆盖容器中对应的文件
      +
    • 这让我们可以直接在容器外修改程序的配置,并通过直接重启容器就能应用这些配置;
    • +
    +
  2. +
  3. 把目录挂载到容器中应用数据的输出目录
  4. +
+
    +
  • 让容器中的程序直接将数据输出到容器外,对于 MySQL、Redis 中的数据,程序的日志等内容,我们可以使用这种方法来持久保存它们;
  • +
+
    +
  1. 把代码或者编译后的程序挂载到容器中
      +
    • 让它们在容器中可以直接运行,这就避免了我们在开发中反复构建镜像带来的麻烦,节省出大量宝贵的开发时间。
    • +
    +
  2. +
+

保存和共享镜像

提交容器更改

Docker 镜像的本质是多个基于 UnionFS 的镜像层依次挂载的结果,而容器的文件系统则是在以只读方式挂载镜像后增加的一个可读可写的沙盒环境。Docker 中为我们提供了将容器中的这个可读可写的沙盒环境持久化为一个镜像层的方法。

+
    +
  • 我们能够在 Docker 里将容器内的修改记录下来,保存为一个新的镜像。
  • +
+

将容器修改的内容保存为镜像的命令是 docker commit

+
    +
  • 由于镜像的结构很像代码仓库里的修改记录,而记录容器修改的过程又像是在提交代码,所以这里我们更形象的称之为提交容器的更改。
  • +
  • docker commit webapp
  • +
  • 像通过 Git 等代码仓库软件提交代码一样,我们还能在提交容器更改的时候给出一个提交信息,方便以后查询。
      +
    • docker commit -m "Configured" webapp
    • +
    +
  • +
  • Docker 执行将容器内沙盒文件系统记录成镜像层的时候,会先暂停容器的运行,以保证容器内的文件系统处于一个相对稳定的状态,确保数据的一致性。
  • +
+

为镜像命名

docker tag 0bc42f7ff218 webapp:1.0

+

使用 docker tag 能够为未命名的镜像指定镜像名,也能够对已有的镜像创建一个新的命名。

+
    +
  • 当我们对未命名的镜像进行命名后,Docker 就不会在镜像列表里继续显示这个镜像,取而代之的是我们新的命名。
  • +
  • 而如果我们对以后镜像使用 docker tag,旧的镜像依然会存在于镜像列表中。
      +
    • docker tag webapp:1.0 webapp:latest
    • +
    • 实质是它们其实引用着相同的镜像层,这个我们能够从镜像 ID 中看得出来 ( 因为镜像 ID 就是最上层镜像层的 ID )。
    • +
    +
  • +
+

还可以直接在 docker commit 命令里指定新的镜像名,这种方式在使用容器提交时会更加方便。

+
    +
  • docker commit -m "Upgrade" webapp webapp:2.0
  • +
+

导出镜像

docker save 命令可以将镜像输出,提供了一种让我们保存镜像到 Docker 外部的方式。

+
    +
  • 在默认定义下,docker save 命令会将镜像内容放入输出流中,这就需要我们使用管道进行接收
      +
    • docker save webapp:1.0 > webapp-1.0.tar
    • +
    +
  • +
  • docker save 命令还为我们提供了 -o 选项,用来指定输出文件,使用这个选项可以让命令更具有统一性。
      +
    • docker save -o ./webapp-1.0.tar webapp:1.0
    • +
    +
  • +
+

导入镜像

导入镜像的方式也很简单,使用与 docker save 相对的 docker load 命令即可。

+
    +
  • docker load 命令是从输入流中读取镜像的数据,所以我们这里也要使用管道来传输内容。当然
      +
    • docker load < webapp-1.0.tar
    • +
    +
  • +
  • 也能够使用 -i 选项指定输入文件。
      +
    • docker load -i webapp-1.0.tar
    • +
    +
  • +
  • 镜像导入后,我们就可以通过 docker images 看到它了,导入的镜像会延用原有的镜像名称
  • +
+

批量迁移

通过 docker savedocker load 命令我们还能够批量迁移镜像,只要我们在 docker save 中传入多个镜像名作为参数,它就能够将这些镜像都打成一个包,便于我们一次性迁移多个镜像。

+
    +
  • docker save -o ./images.tar webapp:1.0 nginx:1.12 mysql:5.7
  • +
+

导出和导入容器

使用 docker export 命令我们可以直接导出容器

+
    +
  • 可以把它简单的理解为 docker commitdocker 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 savedocker export 的区别:

+
    +
  • docker save 保存的是镜像(Image),docker export 保存的是容器(Container);
  • +
  • docker load 用来载入镜像包,docker import 用来载入容器包,但两者都会恢复为镜像;
  • +
  • docker load 不能对载入的镜像重命名,而 docker import 可以为镜像指定新名称。
  • +
+

通过 Dockerfile 创建镜像

常见 Dockerfile 指令

FROM

通常来说,我们不会从零开始搭建一个镜像,而是会选择一个已经存在的镜像作为我们新镜像的基础,这种方式能够大幅减少我们的时间。

+

通过 FROM 指令指定一个基础镜像,接下来所有的指令都是基于这个镜像所展开的。

+

FROM 指令支持三种形式:

+
    +
  • FROM <image> [AS <name>]
  • +
  • FROM <image>[:<tag>] [AS <name>]
  • +
  • FROM <image>[@<digest>] [AS <name>]
  • +
+

Dockerfile 中的第一条指令必须是 FROM 指令,因为没有了基础镜像,一切构建过程都无法开展。

+
    +
  • 当 FROM 第二次或者之后出现时,表示在此刻构建时,要将当前指出镜像的内容合并到此刻构建镜像的内容里。
  • +
+

RUN

在 RUN 指令之后,我们直接拼接上需要执行的命令,在构建时,Docker 就会执行这些命令,并将它们对文件系统的修改记录下来,形成镜像的变化。

+
    +
  • RUN <command>
  • +
  • RUN ["executable", "param1", "param2"]
  • +
  • RUN 指令是支持 \ 换行的,如果单行的长度过长,建议对内容进行切割,方便阅读。
  • +
+

ENTRYPOINT 和 CMD

基于镜像启动的容器,在容器启动时会根据镜像所定义的一条命令来启动容器中进程号为 1 的进程。而这个命令的定义,就是通过 Dockerfile 中的 ENTRYPOINTCMD 实现的。

+
    +
  • ENTRYPOINT ["executable", "param1", "param2"]
  • +
  • ENTRYPOINT command param1 param2
  • +
  • CMD ["executable","param1","param2"]
  • +
  • CMD ["param1","param2"]
  • +
  • CMD command param1 param2
  • +
+

当 ENTRYPOINT 与 CMD 同时给出时,CMD 中的内容会作为 ENTRYPOINT 定义命令的参数,最终执行容器启动的还是 ENTRYPOINT 中给出的命令。

+

EXPOSE

通过 EXPOSE 指令可以为镜像指定要暴露的端口。

+
    +
  • EXPOSE <port> [<port>/<protocol>...]
  • +
+

当我们通过 EXPOSE 指令配置了镜像的端口暴露定义,那么基于这个镜像所创建的容器,在被其他容器通过 --link 选项连接时,就能够直接允许来自其他容器对这些端口的访问了。

+

VOLUME

在 Dockerfile 里,提供了 VOLUME 指令来定义基于此镜像的容器所自动建立的数据卷。

+
    +
  • VOLUME ["/data"]
  • +
  • 在 VOLUME 指令中定义的目录,在基于新镜像创建容器时,会自动建立为数据卷,不需要我们再单独使用 -v 选项来配置了。
  • +
+

COPY 和 ADD

    +
  • COPY [--chown=<user>:<group>] <src>... <dest>
  • +
  • ADD [--chown=<user>:<group>] <src>... <dest>
  • +
  • COPY [--chown=<user>:<group>] ["<src>",... "<dest>"]
  • +
  • ADD [--chown=<user>:<group>] ["<src>",... "<dest>"]
  • +
+

COPYADD 指令的定义方式完全一样,需要注意的仅是当我们的目录中存在空格时,可以使用后两种格式避免空格产生歧义

+

ADD 能够支持使用网络端的 URL 地址作为 src 源,并且在源文件被识别为压缩包时,自动进行解压。

+

构建镜像

构建镜像的命令为 docker build

+
    +
  • docker build ./webapp
  • +
  • docker build 可以接收一个参数,需要特别注意的是,这个参数为一个目录路径
  • +
+

在默认情况下,docker build 也会从这个目录下寻找名为 Dockerfile 的文件,将它作为 Dockerfile 内容的来源。如果我们的 Dockerfile 文件路径不在这个目录下,或者有另外的文件名,我们可以通过 -f 选项单独给出 Dockerfile 文件的路径。

+
    +
  • docker build -t webapp:latest -f ./webapp/a.Dockerfile ./webapp
  • +
  • 在构建时我们最好总是携带上 -t 选项,用它来指定新生成镜像的名称。
  • +
+

Dockerfile 使用技巧

构建中使用变量
在 Dockerfile 里,我们可以用 ARG 指令来建立一个参数变量,我们可以在构建时通过构建指令传入这个参数变量,并且在 Dockerfile 里使用它。

+
1
2
3
4
5
6
7
8
9
10
11
12
FROM debian:stretch-slim

## ......

ARG TOMCAT_MAJOR
ARG TOMCAT_VERSION

## ......

RUN wget -O tomcat.tar.gz "https://www.apache.org/dyn/closer.cgi?action=download&filename=tomcat/tomcat-$TOMCAT_MAJOR/v$TOMCAT_VERSION/bin/apache-tomcat-$TOMCAT_VERSION.tar.gz"

## ......
+

我们可以在构建时通过 docker build 的 –build-arg 选项来设置参数变量

+
    +
  • docker build --build-arg TOMCAT_MAJOR=8 --build-arg TOMCAT_VERSION=8.0.53 -t tomcat:8.0 ./tomcat
  • +
+

环境变量

环境变量也是用来定义参数的东西,与 ARG 指令相类似,环境变量的定义是通过 ENV 这个指令来完成的。

+
1
2
3
4
5
6
7
8
9
10
FROM debian:stretch-slim

## ......

ENV TOMCAT_MAJOR 8
ENV TOMCAT_VERSION 8.0.53

## ......

RUN wget -O tomcat.tar.gz "https://www.apache.org/dyn/closer.cgi?action=download&filename=tomcat/tomcat-$TOMCAT_MAJOR/v$TOMCAT_VERSION/bin/apache-tomcat-$TOMCAT_VERSION.tar.gz"
+

环境变量与参数变量的区别:

+
    +
  • 环境变量不仅能够影响构建,还能够影响基于此镜像创建的容器。
      +
    • 环境变量设置的实质,其实就是定义操作系统环境变量,所以在运行的容器里,一样拥有这些变量,而容器中运行的程序也能够得到这些变量的值。
    • +
    +
  • +
  • 环境变量的值不是在构建指令中传入的,而是在 Dockerfile 中编写的,所以如果我们要修改环境变量的值,我们需要到 Dockerfile 修改
  • +
  • 在创建容器时使用 -e 或是 --env 选项,可以对环境变量的值进行修改或定义新的环境变量。
      +
    • docker run -e MYSQL_ROOT_PASSWORD=my-secret-pw -d mysql:5.7
    • +
    +
  • +
  • Dockerfile 中的 ENV 指令所定义的变量,永远会覆盖 ARG 所定义的变量
  • +
+

合并命令

看似连续的镜像构建过程,其实是由多个小段组成。

+
    +
  • 每当一条能够形成对文件系统改动的指令在被执行前,Docker 先会基于上条命令的结果启动一个容器,在容器中运行这条指令的内容
  • +
  • 之后将结果打包成一个镜像层,如此反复,最终形成镜像。
  • +
+

+
    +
  • 镜像是由多个镜像层叠加而得,而这些镜像层其实就是在我们 Dockerfile 中每条指令所生成的。
  • +
  • 将命令合并到一条指令中不但减少了镜像层的数量,也减少了镜像构建过程中反复创建容器的次数,提高了镜像构建的速度。
  • +
+

构建缓存

Docker 判断镜像层与之前的镜像间不存在变化的两个维度:

+
    +
  1. 所基于的镜像层是否一样
  2. +
  3. 用于生成镜像层的指令的内容是否一样
  4. +
+

我们在条件允许的前提下,更建议将不容易发生变化的搭建过程放到 Dockerfile 的前部,充分利用构建缓存提高镜像构建的速度。

+

另外一些时候,我们可能不希望 Docker 在构建镜像时使用构建缓存,这时我们可以通过 –no-cache 选项来禁用它。

+
    +
  • docker build --no-cache ./webapp
  • +
+

搭配 ENTRYPOINT 和 CMD

两个指令的区别在于,ENTRYPOINT 指令的优先级高于 CMD 指令。

+
    +
  • 当 ENTRYPOINT 和 CMD 同时在镜像中被指定时,CMD 里的内容会作为 ENTRYPOINT 的参数,两者拼接之后,才是最终执行的命令。
  • +
+

ENTRYPOINT 和 CMD 设计的目的不同:

+
    +
  • ENTRYPOINT 指令主要用于对容器进行一些初始化
  • +
  • CMD 指令则用于真正定义容器中主程序的启动命令
  • +
+

容器启动时覆盖启动命令也只是覆盖 CMD 中定义的内容,不会影响 ENTRYPOINT 中的内容。

+

使用脚本文件来作为 ENTRYPOINT 的内容是常见的做法,因为对容器运行初始化的命令相对较多,全部直接放置在 ENTRYPOINT 后会特别复杂:

+
1
2
3
4
5
6
7
8
9
## ......

COPY docker-entrypoint.sh /usr/local/bin/

ENTRYPOINT ["docker-entrypoint.sh"]

## ......

CMD ["redis-server"]
+

Redis 中的 ENTRYPOINT 脚本:

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#!/bin/sh
set -e

# first arg is `-f` or `--some-option`
# or first arg is `something.conf`
if [ "${1#-}" != "$1" ] || [ "${1%.conf}" != "$1" ]; then
set -- redis-server "$@"
fi

# allow the container to be started with `--user`
if [ "$1" = 'redis-server' -a "$(id -u)" = '0' ]; then
find . \! -user redis -exec chown redis '{}' +
exec gosu redis "$0" "$@"
fi

exec "$@"
+

在很多镜像的 ENTRYPOINT 脚本里,我们都会看到 exec "$@" 命令,其作用其实很简单,就是运行一个程序,而运行命令就是 ENTRYPOINT 脚本的参数

+
    +
  • 由于 ENTRYPOINT 脚本的参数就是 CMD 指令中的内容,所以实际执行的就是 CMD 里的命令
  • +
  • 所以说,虽然 Docker 对容器启动命令的结合机制为 CMD 作为 ENTRYPOINT 的参数,合并后执行 ENTRYPOINT 中的定义,但实际在我们使用中,我们还会在 ENTRYPOINT 的脚本里代理到 CMD 命令上
  • +
+

另外一篇 Dockerfile 最佳实践的文章:https://www.practicemp.com/2018/10/docker-best-practices-for-writing-dockerfiles.html

+

使用 Docker Hub 中的镜像

选择镜像与程序版本

对于一些复杂的应用,除了版本外,还存在很多的变量,镜像的维护者们也喜欢将这些变量一同组合到镜像的 Tag 里,所以我们在使用镜像前,一定要先了解不同 Tag 对应的不同内容。

+

+
    +
  • 通常来说,镜像的维护者会在镜像介绍中展示出镜像所有的 Tag,如果没有,我们也能够从页面上的 Tags 导航里进入到镜像标签列表页面。
  • +
  • 在 OpenJDK 镜像的 Tag 列表里,我们可以看到同样版本号的镜像就存在多种标签。在这些不同的标签上,除了定义 OpenJDK 的版本,还有操作系统,软件提供者等信息。
  • +
  • 镜像维护者为我们提供这么多的标签进行选择,其实方便了我们在不同场景下选择不同环境实现细节时,都能直接用到这个镜像,而不需要再单独编写 Dockerfile 并构建。
  • +
+

Alpine 镜像

镜像标签中的 Alpine 指的是这个镜像内的文件系统内容,是基于 Alpine Linux 这个操作系统的。

+
    +
  • Alpine Linux 是一个相当精简的操作系统,而基于它的 Docker 镜像可以仅有数 MB 的尺寸。
  • +
+

Alpine 镜像的缺点就在于它实在过于精简

+
    +
  • 在 Alpine 中缺少很多常见的工具和类库
      +
    • 以至于如果我们想基于软件 Alpine 标签的镜像进行二次构建,那搭建的过程会相当烦琐。
    • +
    +
  • +
  • 所以想要对软件镜像进行改造,并基于其构建新的镜像,那么 Alpine 镜像不是一个很好的选择
      +
    • 提倡基于 Ubuntu、Debian、CentOS 这类相对完整的系统镜像来构建
    • +
    +
  • +
+

使用 Docker Compose 管理容器

Docker Compose

+

如果说 Dockerfile 是将容器内运行环境的搭建固化下来,那么 Docker Compose 我们就可以理解为将多个容器运行的方式和配置固化下来。

+

启动和停止

最常使用的 Docker Compose 命令就是 docker-compose updocker-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 文件,而会以这个目录的名字作为组装的应用项目的名称。
      +
    • 可以通过选项 -f 来修改识别的 Docker Compose 配置文件,通过 -p 选项来定义项目名。
    • +
    • docker-compose -f ./compose/docker-compose.yml -p myapp up -d
    • +
    +
  • +
+

docker-compose up 相反,docker-compose down 命令用于停止所有的容器,并将它们删除,同时消除网络等配置内容

+
    +
  • docker-compose down
  • +
  • 也就是几乎将这个 Docker Compose 项目的所有影响从 Docker 中清除
  • +
+

指定镜像

在 Docker Compose 里,可以通过两种方式为服务指定所采用的镜像。

+
    +
  1. 通过 image 这个配置
      +
    • 给出能在镜像仓库中找到镜像的名称即可
    • +
    +
  2. +
  3. 直接采用 Dockerfile 来构建镜像
      +
    • 通过 build 这个配置我们能够定义构建的环境目录
    • +
    • 如果通过这种方式指定镜像,那么 Docker Compose 先会帮助我们执行镜像的构建,之后再通过这个镜像启动容器。
    • +
    +
  4. +
+

在配置文件里,我们还能用 Map 的形式来定义 build,在这种格式下,我们能够指定更多的镜像构建参数,例如 Dockerfile 的文件名,构建参数等等:

+
1
2
3
4
5
6
7
8
## ......
webapp:
build:
context: ./webapp
dockerfile: webapp-dockerfile
args:
- JAVA_VERSION=1.6
## ......
+

依赖声明

如果我们的服务间有非常强的依赖关系,就必须告知 Docker Compose 容器的先后启动顺序。

+
    +
  • 只有当被依赖的容器完全启动后,Docker Compose 才会创建和启动这个容器。
  • +
  • 定义依赖的方式很简单,只需要通过 depends_on 列出这个服务所有依赖的其他服务即可
  • +
  • Docker Compose 为我们启动项目的时候,会检查所有依赖,形成正确的启动顺序并按这个顺序来依次启动容器。
  • +
+

文件挂载

使用 volumes 配置可以像 docker CLI 里的 -v 选项一样来指定外部挂载和数据卷挂载。

+

在使用外部文件挂载的时候,我们可以直接指定相对目录进行挂载,这里的相对目录是指相对于 docker-compose.yml 文件的目录。

+
    +
  • 由于有相对目录这样的机制,可以将 docker-compose.yml 和所有相关的挂载文件放置到同一个文件夹下,形成一个完整的项目文件夹。
      +
    • 这样既可以很好的整理项目文件,也利于完整的进行项目迁移。
    • +
    +
  • +
  • 在开发时,推荐直接将代码挂载到容器里,而不是通过镜像构建的方式打包成镜像。
  • +
  • 在开发过程中,对于程序的配置等内容,也建议直接使用文件挂载的形式挂载到容器里,避免经常修改所带来的麻烦。
  • +
+

使用数据卷

如果我们要在项目中使用数据卷来存放特殊的数据,我们也可以让 Docker Compose 自动完成对数据卷的创建,而不需要我们单独进行操作。

+

在上面的例子里,独立于 servicesvolumes 配置就是用来声明数据卷的。定义数据卷最简单的方式仅需要提供数据卷的名称。

+

如果我们想把属于 Docker Compose 项目以外的数据卷引入进来直接使用,我们可以将数据卷定义为外部引入,通过 external 这个配置就能完成这个定义。

+
1
2
3
4
5
## ......
volumes:
mysql-data:
external: true
## ......
+

在加入 external 定义后,Docker Compose 在创建项目时不会直接创建数据卷,而是优先从 Docker Engine 中已有的数据卷里寻找并直接采用。

+

配置网络

在 Docker Compose 里,我们可以为整个应用系统设置一个或多个网络。

+

声明网络的配置同样独立于 services 存在,是位于根配置下的 networks 配置。

+

除了简单的声明网络名称,让 Docker Compose 自动按默认形式完成网络配置外,我们还可以显式的指定网络的参数。

+
1
2
3
4
5
6
7
8
networks:
frontend:
driver: bridge
ipam:
driver: default
config:
- subnet: 10.10.1.0/24
## ......
+

在这里,我们为网络定义了网络驱动的类型,并指定了子网的网段。

+

使用网络别名

网络别名的定义方式很简单,这里需要将之前简单的网络 List 定义结构修改成 Map 结构,以便在网络中加入更多的定义。

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
## ......
database:
networks:
backend:
aliases:
- backend.database
## ......
webapp:
networks:
backend:
aliases:
- backend.webapp
frontend:
aliases:
- frontend.webapp
## ......
+

在进行这样的配置后,便可以使用这里所设置的网络别名对其他容器进行访问了。

+

端口映射

ports 配置项,是用来定义端口映射的。可以利用它进行宿主机与容器端口的映射,这个配置与 docker CLI 中 -p 选项的使用方法是近似的。

+

需要注意的是,由于 YAML 格式对 xx:yy 这种格式的解析有特殊性,在设置小于 60 的值时,会被当成时间而不是字符串来处理,所以我们最好使用引号将端口映射的定义包裹起来,避免歧义。

+
    +
  • "8080:8080"
  • +
+

重启机制

restart 配置主要是用来控制容器的重启策略的。

+

restart 选项:

+ + + + + + + + + + + + + + + + + + + + + + + + + +
配置值说明
no不设重启机制
always总是重启
on-failure在异常退出时重启
unless-stopped除非由停止命令结束,其他情况都重启
+

应用于服务化开发

+

Overlay Network 能够跨越物理主机的限制,让多个处于不同 Docker daemon 实例中的容器连接到同一个网络,并且让这些容器感觉这个网络与其他类型的网络没有区别。

+
    +
  • 要搭建 Overlay Network 网络,我们就要用到 Docker Swarm 这个工具了。
  • +
+

Docker Swarm

Docker Swarm 是 Docker 内置的集群工具,它能够帮助我们更轻松地将服务部署到 Docker daemon 的集群之中。

+

+

在真实的服务部署里,我们通常是使用 Docker Compose 来定义集群,而通过 Docker Swarm 来部署集群。

+
    +
  • 对于 Docker Swarm 来说,每一个 Docker daemon 的实例都可以成为集群中的一个节点
  • +
  • 在 Docker daemon 加入到集群成为其中的一员后,集群的管理节点就能对它进行控制。
  • +
  • 我们要搭建的 Overlay 网络正是基于这样的集群实现的。
  • +
+

我们在任意一个 Docker 实例上都可以通过 docker swarm init 来初始化集群。

+
1
2
3
4
5
6
7
$ docker swarm init

Swarm initialized: current node (t4ydh2o5mwp5io2netepcauyl) is now a manager.

To add a worker to this swarm, run the following command:

docker swarm join --token SWMTKN-1-4dvxvx4n7magy5zh0g0de0xoues9azekw308jlv6hlvqwpriwy-cb43z26n5jbadk024tx0cqz5r 192.168.1.5:2377
+

在集群初始化后,这个 Docker 实例就自动成为了集群的管理节点,而其他 Docker 实例可以通过运行这里所打印的 docker swarm join 命令来加入集群。

+

加入到集群的节点默认为普通节点,如果要以管理节点的身份加入到集群中

+
    +
  • 可以通过 docker swarm join-token 命令来获得管理节点的加入命令。
  • +
+
1
2
3
4
$ docker swarm join-token manager
To add a manager to this swarm, run the following command:

docker swarm join --token SWMTKN-1-60am9y6axwot0angn1e5inxrpzrj5d6aa91gx72f8et94wztm1-7lz0dth35wywekjd1qn30jtes 192.168.1.5:2377
+

建立跨主机网络

通过 docker network create 命令来建立 Overlay 网络。

+

docker network create --driver overlay --attachable mesh

+
    +
  • 在创建 Overlay 网络时,我们要加入 --attachable 选项以便不同机器上的 Docker 容器能够正常使用到它。
  • +
  • 在创建了这个网络之后,我们可以在任何一个加入到集群的 Docker 实例上使用 docker network ls 查看一下其下的网络列表。
      +
    • 会发现这个网络定义已经同步到了所有集群中的节点上。
    • +
    +
  • +
+

将网络的 external 属性设置为 true,就可以让 Docker Compose 将其建立的容器都连接到这个不属于 Docker Compose 的项目上了。

+
1
2
3
networks:
mesh:
external: true
+

准备程序配置

我们常用下列几种方式来获得程序的配置文件:

+
    +
  • 借助配置文档直接编写
  • +
  • 下载程序源代码中的配置样例
  • +
  • 通过容器中的默认配置获得
  • +
+

借助配置文档直接编写

MySQL 文档中关于配置文件的参考:
https://dev.mysql.com/doc/refman/5.7/en/server-options.html

+
    +
  • 使用软件的文档来编写配置文件,其优势在于在编写的过程实际上也是我们熟悉软件的过程,通过配置加文档形式的阅读,你一定会从中收获很多。
  • +
  • 这种方法也有很大的劣势,即需要仔细阅读文档,劳神劳力,对于常规开发中的使用来说,成效比很低。
  • +
+

下载程序源代码中的配置样例

大部分软件,特别是开源软件都会直接给出一份示例配置文件作为参考。 我们可以直接拿到这份配置,达到我们的目的。

+
    +
  • 在 Redis 源代码中,就包含了一份默认的配置文件,我们可以直接拿来使用:https://github.com/antirez/redis/blob/3.2/redis.conf
  • +
  • 相对于通过配置文档获得配置,从配置示例里获得配置要来得更为简单容易。
  • +
+

通过容器中的默认配置获得

大多数 Docker 镜像为了实现自身能够直接启动为容器并马上提供服务,会把默认配置直接打包到镜像中,以便让程序能够直接读取。

+
    +
  • 所以说,我们可以直接从镜像里拿到这份配置,拷贝到宿主机里备用。
  • +
+

以 Tomcat 为例,说说如何从 Tomcat 镜像里拿到配置文件:

+
    +
  1. 要拿到 Tomcat 中的配置文件,我们需要先创建一个临时的 Tomcat 容器。
      +
    • docker run --rm -d --name temp-tomcat tomcat:8.5
    • +
    +
  2. +
  3. 对于 Tomcat 来说,在开发过程中我们可能会经常改动的配置主要是 server.xmlweb.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
    • +
    +
  4. +
  5. 完成上面的操作后清理我们创建的临时容器
      +
    • docker stop temp-tomcat
    • +
    • 由于我们在创建临时容器的时候增加了 --rm 选项,所以我们在这里只需要使用 docker stop 停止容器,就可以在停止容器的同时直接删除容器,实现直接清理的目的。
    • +
    +
  6. +
+

docker 和 docker-compose 命令手册:

+ +
+ + + + + + + +
+ + + + + + +
+ +
+ + + +
+ + + + + + + + +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2020/edit-docker-container-file/index.html b/2020/edit-docker-container-file/index.html new file mode 100644 index 0000000000..73f43acd41 --- /dev/null +++ b/2020/edit-docker-container-file/index.html @@ -0,0 +1,520 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 编辑 docker 容器中的文件 | 贾攀的流水账 + + + + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

贾攀的流水账

+ +
+

Panmax's Blog

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+ + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ 编辑 docker 容器中的文件 +

+ + +
+ + + + +
+ + +
+ +

写在前面

为什么要这样做?

实际上我们并不需要也不建议直接编辑容器中的文件。Docker 容器是不可变的工作单元,用于运行单个、特定的进程。镜像应该在没有任何干预的情况下够建和运行。

+

只有在开发期间,对 Docker 容器中的文件进行编辑可能才有些用处,这让我们在无需重新够建镜像的状态下验证我们的修改是否达到了预期的效果,可以达到节省时间、提高开发效率的目的,但是在完成验证后,应该删除添加到镜像中的多于软件包,并将验证后的结果持久化到镜像中。

+

另外需要提醒的一点是,当我们在一个运行着的容器中编辑一个文件后需要确保所依赖这个文件的进程收到了文件编辑的通知并进行了配置更新,如果没有类似的通知机制,需要手动重启这些进程使修改生效。

+

本文假设你所使用的容器中没有 vi 等文本编辑工具,我们以 openjdk:11 作为演示镜像:

+
1
2
3
4
➜ docker run -it openjdk:11 bash
root@d0fb3a0b527c:/# vi Lol.java
bash: vi: command not found
root@d0fb3a0b527c:/#
+

下面介绍五种常用方法:

方法1:使用挂载

准备 Dockerfile:

+
1
2
FROM openjdk:11
WORKDIR "/app"
+

编译镜像:

+
1
docker build -t lol .
+

最后,运行带有挂载的容器:

+
1
docker run --rm -it --name=lol -v $PWD/app-vol:/app lol bash
+

如果本地 $PWD/app-vol 目录不存在,会被自动创建。此后在 $PWD/app-vol 下的文件操作会映射在容器的 /app 目录下。

+

方法2:安装编辑器

1
2
3
4
docker run --rm -it --name=lol lol bash

root@4b72fbabb0af:/app# apt-get update
root@4b72fbabb0af:/app# apt-get -y install vim
+

如果需要重复使用,更好的做法是写在 Dockerfile 中:

+
1
2
3
4
FROM openjdk:11
RUN ["apt-get", "update"]
RUN ["apt-get", "-y", "install", "vim"]
WORKDIR "/app"
+

方法3:将文件拷贝到正在运行的容器中

1
docker cp Lol.java lol:/app
+

另一个与之类似的方法是将 docker exec 和 cat 结合使用,下边的命令同样把 Lol.java 文件复制到了正在运行的容器中:

+
1
docker exec -i lol sh -c 'cat > /app/Lol.java' < Lol.java
+

方法4:使用 Linux 工具

虽然容器中通常没有安装编辑工具,但是其他 Linux 工具,如:sed, awk, echo, cat, cut 等是具备的,可以派上用场。比如 sed 和 awk 可以编辑文件的适当位置,还可以将 echo, cat, cut 联合起来并借助强大的重定向流创建和编辑文件。正如前文所示,这些工具可以与 docker exec 命令结合使用,从而发挥更强大的威力。

+

方法5:使用远程 vim(或其他编辑器)

这种方法只是为了开拓思路,并不会在实际中使用。

+

修改 Dockerfile:

+
1
2
3
4
5
6
7
8
9
10
FROM openjdk:11
RUN ["apt-get", "update"]
RUN ["apt-get", "install", "-y", "openssh-server"]
RUN mkdir /var/run/sshd
RUN echo 'root:lollol0' | chpasswd
RUN sed -i 's/#PermitRootLogin prohibit-password/PermitRootLogin yes/' /etc/ssh/sshd_config
RUN ["/etc/init.d/ssh", "start"]
EXPOSE 22
WORKDIR "/app"
CMD ["/usr/sbin/sshd", "-D"]
+

因为我们要借助 scp 来远程进行文件编辑,所以需要安装 openssh-server 并开放其端口。

+

编译并运行:

+
1
2
docker build -t lol .
docker run --rm -p 2222:22 -d --name=lol lol
+

现在我们可以使用以下命令来编辑 Lol.java 文件了:

+
1
vim scp://root@localhost:2222//app/Lol.java
+

注:在 vi 中需要先执行 :set bt=acwrite 命令再去编辑文件,相关讨论见:https://github.com/vim/vim/issues/2329

+

编辑完成保存并退出后,可以使用下边的命令来验证文件确实被创建和保存了:

+
1
docker exec -it lol cat /app/Lol.java
+ +
+ + + + + + + +
+ + + + + + +
+ +
+ + + +
+ + + + + + + + +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2020/git-lose-weight/0.jpeg b/2020/git-lose-weight/0.jpeg new file mode 100644 index 0000000000..cb8124b95c Binary files /dev/null and b/2020/git-lose-weight/0.jpeg differ diff --git a/2020/git-lose-weight/1.png b/2020/git-lose-weight/1.png new file mode 100644 index 0000000000..bca10be696 Binary files /dev/null and b/2020/git-lose-weight/1.png differ diff --git a/2020/git-lose-weight/2.png b/2020/git-lose-weight/2.png new file mode 100644 index 0000000000..c0c6392ed3 Binary files /dev/null and b/2020/git-lose-weight/2.png differ diff --git a/2020/git-lose-weight/3.png b/2020/git-lose-weight/3.png new file mode 100644 index 0000000000..8fb7938f03 Binary files /dev/null and b/2020/git-lose-weight/3.png differ diff --git a/2020/git-lose-weight/index.html b/2020/git-lose-weight/index.html new file mode 100644 index 0000000000..93d44ade53 --- /dev/null +++ b/2020/git-lose-weight/index.html @@ -0,0 +1,559 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + GitLab 瘦身方法 | 贾攀的流水账 + + + + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

贾攀的流水账

+ +
+

Panmax's Blog

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+ + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ GitLab 瘦身方法 +

+ + +
+ + + + +
+ + +

+
+

由于项目代码中存放了一些大文件在 git 仓库中(比如训练后的模型数据),所以最近收到公司的通知,需要给 git 进行瘦身。

+

本文内容是摘自公司的通知。

+
+

提前告知

    +
  • 瘦身将从此库中永久删除此文件,且无法恢复。包括所有“分支”中的引用,所有“Tag”中的引用,连同提交此文件的log记录也一并清除。

    +
  • +
  • 请在操作之前将要永久删除的文件备份,并记录目录位置。待瘦身结束后,将此大文件以「LFS」的形式 commit 到此库中。 详见《GitLab lfs 使用》。

    +
  • +
+

基本原理

    +
  1. 先在本地对 git 库瘦身,再镜像推送到 GitLab 新创建的库。

    +
  2. +
  3. 待新库测试稳定后,通知管理员将旧的 git 库归档,组内使用新库,新/旧库 rename 互换。

    +
  4. +
+

操作步骤:(大库瘦身需要几个小时,请提前注销组员权限)

1. 需要瘦身的库 git clone –bare 到本地

1
git clone --bare https://git.server.com/group/name.git
+

2. 查看 git 库空间大小

1
du -sh ./name.git
+

3. 查看历史上哪些文件庞大(检查所有分支)

1
2
cd name.git
git verify-pack -v ./objects/pack/*.idx | sort -k 3 -n | tail -10
+

查询结果对应关系:<SHA-1> <类型> <size> <size-in-packfile> <offset-in-packfile>

+

如:

+
1
2
3
4
5
6
7
8
9
10
950dae43f100f6586884893eab3b258a09da1076 blob   173244608 172458659 28056
d969843d33706a6d1f0d2ef9576ce8baa95d6786 blob 188144087 188196487 204238271
dd99138acfdbfbe40ce8caed731fbe077f087a82 blob 225264868 208815706 184299
006dcd011a0a1d31a3066634befcda6c8fd60d0d blob 255858144 236934769 453966523
50703d1627abf7d3a1a4a6447ae5c001c2cdd263 blob 255858144 236936133 690901292
92dbd0d8f1eaf303a2deac80cfeb9fa7f3f864f1 blob 255858144 236936851 1640249530
9631c6903a73bda0d218e7cc19cfc513fbcf01a2 blob 255858144 236935865 1166376569
b5fe73bc6dcc740e28a4ee414483b0d712dc05fa blob 255858144 236936876 1877186381
b79e9f47640942b4e3fd035ced4140366b645376 blob 255858144 236937096 1403312434
fd6ca8cebdb65c89f2d392f3143ba3cdadfdbddd blob 257557808 238539144 927837425
+

4. 查看大文件名称,排名前 10,从小到大,检索 5G 库需要 1 分钟

1
git rev-list --objects --all | grep "$(git verify-pack -v ./objects/pack/*.idx | sort -k 3 -n | tail -10 | awk '{print$1}')"
+

5. 删除历史文件,删除5G库需要15分钟(此步永久删除,对所有分支 /tag/log 的删除操作)

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
    名会从标注输入送给后面的命令,用 cat 就表示保持 tag 名不变。
    紧跟着的 -- 表示分割符,最后的 --all 表示对所有的分支和 tag 都考虑在内。
  • +
+

6. 删除GIT缓存记录里的内容

1
rm -rf ./refs/original/
+

7. 对 git log 处理,任何时间运行 git reflog 命令可以查看当前的状态

1
git reflog expire --expire=now --all
+

8. 在进行 repack 前需要将所有对这些 commits 的引用去除

1
git repack -A -d
+

9. 执行 gc 压缩

1
git gc --aggressive --prune=now
+

--aggressive 最大限度的压缩,会比较缓慢

+

10. 检查完整性

1
git fsck --full --unreachable
+

11. 再次查看 .git 空间大小

1
du -sh ../name.git
+

联系 gitlab 管理员

    +
  1. 联系管理员创建新的 git 库
  2. +
  3. 将瘦身后的 git 库镜像推送到 gitlab
      +
    • git push --mirror https://git.server.com/group/name_new.git
    • +
    +
  4. +
  5. 测试使用新的库
  6. +
  7. 将旧库 rename 并归档,新库 rename 成旧库名字
  8. +
  9. 将大文件以 LFS 形式 commit 到新库中
  10. +
  11. 恢复新库的人员权限,通知大家使用
  12. +
+
+

GitLab LFS 使用方法

Linux 安装

git lfs 要求 git >= 1.8.2

+
1
yum install git-lfs -y
+

MacOS 安装

运行 brew install git-lfs 即可

+

Windows 安装

git 版本大于 2.12

+

关闭 Windows 的 ssl 校验

+
1
git config --global http.sslVerify false
+

+

申请 git lfs 仓库

走流程申请一个 aritfactory –git lfs 仓库

+

使用方法

告诉 lfs 需要管理的大文件

比如 3.pdf,运行命令 git lfs track 3.pdf,会产生 git lfs 管理文件 .gitattributes

+

支持通配符比如 git lfs track *.exe

+

添加 .lfsconfig 文件,指定 git lfs 文件存放位置

我申请的 git lfs 仓库叫做 git-lfs

+

登陆 aritfactory 后,如下操作:

+

+
    +
  1. 同时提交 .gitattributes.lfsconfig3.pdf,然后在 gitlab 中查看
  2. +
+

+ +
+ + + + + + + +
+ + + + + + +
+ +
+ + + +
+ + + + + + + + +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2020/go-field-scope-question/0.jpg b/2020/go-field-scope-question/0.jpg new file mode 100644 index 0000000000..0d2d291a47 Binary files /dev/null and b/2020/go-field-scope-question/0.jpg differ diff --git a/2020/go-field-scope-question/index.html b/2020/go-field-scope-question/index.html new file mode 100644 index 0000000000..346433264a --- /dev/null +++ b/2020/go-field-scope-question/index.html @@ -0,0 +1,499 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 由于粗心,Go 变量作用域导致的问题 | 贾攀的流水账 + + + + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

贾攀的流水账

+ +
+

Panmax's Blog

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+ + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ 由于粗心,Go 变量作用域导致的问题 +

+ + +
+ + + + +
+ + +
+ +

昨天写代码的时候因为变量作用域的问题被坑了好久,在这里记录一下,避免今后再犯。

+

先看下面这段代码,大致功能是为传进来的引用填充一个带有自增的 ID 的对象,同时这个 ID 中不能包含 4,自增 ID 是使用 Redis 的 incr 来维护的。

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
func fillNewUserLiveRightAttribute(ctx context.Context, userLiveRight *po.UserLiveRight, right *po.LiveRight) error {
if right.Type == util.LiveRightTypeMystery {
var incrId int64
// 跳过 ID 中包含 4 的情况
for {
incrId, err := cache.GetUserLiveRightCacheRepository().GetUserLiveRightIncrID(ctx, right.ID)
if err != nil {
return err
}

incrIdStr := strconv.FormatInt(incrId, 10)

if !strings.Contains(incrIdStr, "4") {
break
}
}
userLiveRight.Attribute = &po.UserLiveRightAttribute{ID: incrId}
}
return nil
}
+

但是之后发现填充进去的 ID 永远是 0,检查了一下 Redis 中 那个自增 ID 也确实存在。

+

这里当时还饶了一下远路,因为那个 Attribute 字段在数据库中使用 jsonb 存储的,所以我前期先检查了插入时执行的 SQL 语句,发现每次 Attribute 都是打印的 {},就以为是我自己没有赋上值。真实的原因是我指定了 Attribute 中的 ID 字段在转 Json 时启用 omitempty ,即:

+
1
2
3
type UserLiveRightAttribute struct {
ID int64 `json:"id,omitempty"`
}
+

使用 omitempty 可以告诉 Marshal 函数如果 field 的值是对应类型的 zero-value,那么序列化之后的 JSON object 中不包含此 字段,所以 ID=0 转 Json 后自然就没有这个字段了。

+

回到为啥上边的代码拿到的 ID 总是 0 的问题:因为 for 中赋值的 incrId 是在一个新的作用域内,只在 for 的花括号内有效,退出 for 后拿到的是最开始初始化 incrId 的 0 值,这里使用的 := 进行的赋值,因为 err 是个新字段,所以并没有提示错误。

+

修复方法很简单,err 也在作用域外声明,里边使用 = 来赋值。

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
func fillNewUserLiveRightAttribute(ctx context.Context, userLiveRight *po.UserLiveRight, right *po.LiveRight) error {
if right.Type == util.LiveRightTypeMystery {
var incrId int64
var err error
// 跳过 ID 中包含 4 的情况
for {
incrId, err = cache.GetUserLiveRightCacheRepository().GetUserLiveRightIncrID(ctx, right.ID)
if err != nil {
return err
}

incrIdStr := strconv.FormatInt(incrId, 10)

if !strings.Contains(incrIdStr, "4") {
break
}
}
userLiveRight.Attribute = &po.UserLiveRightAttribute{ID: incrId}
}
return nil
}
+ +
+ + + + + + + +
+ + + + + + +
+ +
+ + + +
+ + + + + + + + +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2020/go-make-slice-question/1.jpg b/2020/go-make-slice-question/1.jpg new file mode 100644 index 0000000000..989dbcbb6d Binary files /dev/null and b/2020/go-make-slice-question/1.jpg differ diff --git a/2020/go-make-slice-question/index.html b/2020/go-make-slice-question/index.html new file mode 100644 index 0000000000..c7b1e60a88 --- /dev/null +++ b/2020/go-make-slice-question/index.html @@ -0,0 +1,504 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + make slice 后 append 产生的问题 | 贾攀的流水账 + + + + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

贾攀的流水账

+ +
+

Panmax's Blog

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+ + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ make slice 后 append 产生的问题 +

+ + +
+ + + + +
+ + +
+ +

今天又踩到了一个 go 语言的坑,其实也不算坑,本质上还是自己对这门语言的不熟悉。

+

来看一下我犯的错误,直接上代码:

+
1
2
3
4
5
6
7
func Int64ToStrings(ids []int64) []string {
strs := make([]string, len(ids))
for i, id := range ids {
strs = append(strs, strconv.FormatInt(id, 10))
}
return strs
}
+

我的需求很简单,将一个 int64 的切片转为 string 类型的切片,写这段代码的时候想到可以预先分配 slice 的大小,所以写了开头的 make,后边就直接逐个将转为 string 的元素 append 进去了。

+

但是,make 时实际已经帮我完成了里边指定数量元素的初始化,即:

+
1
s = make([]string, 5) // s == []string{"", "", "", "", ""}
+

所以我之后再往里边 append 时是往最后一个空字符串后边追加元素。

+

修复后的代码如下:

+
1
2
3
4
5
6
7
func Int64ToStrings(ids []int64) []string {
strs := make([]string, len(ids))
for i, id := range ids {
strs[i] = strconv.FormatInt(id, 10)
}
return strs
}
+

或者在初始化时指定 slice 长度为0,容量为我们需要的长度:

+
1
2
3
4
5
6
7
func Int64ToStrings(ids []int64) []string {
strs := make([]string, 0, len(ids))
for i, id := range ids {
strs = append(strs, strconv.FormatInt(id, 10))
}
return strs
}
+ +
+ + + + + + + +
+ + + + + + +
+ +
+ + + +
+ + + + + + + + +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2020/go-value-for-mistake/1.jpg b/2020/go-value-for-mistake/1.jpg new file mode 100644 index 0000000000..4e21130482 Binary files /dev/null and b/2020/go-value-for-mistake/1.jpg differ diff --git a/2020/go-value-for-mistake/index.html b/2020/go-value-for-mistake/index.html new file mode 100644 index 0000000000..55fb1b1418 --- /dev/null +++ b/2020/go-value-for-mistake/index.html @@ -0,0 +1,509 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 使用 Go 语言时没有关注值传递和误用 for 循环导致的 bug | 贾攀的流水账 + + + + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

贾攀的流水账

+ +
+

Panmax's Blog

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+ + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ 使用 Go 语言时没有关注值传递和误用 for 循环导致的 bug +

+ + +
+ + + + +
+ + +
+ +

我们的业务代码中习惯使用 Map 维护一些 LocalCache,前两天发现自己维护的一个 LocalCache 数据有些不对:Cache 的 Key 为某个对象的ID,值为这个ID对应的 PO(即数据库中的对象),调试时发现所有的 Key 对应的值都是一样的,这是因为自己对一些细节没有关注到,还把 Java 那套东西搬来用导致的问题。

+

为了简化,我就不把业务代码搬上来了,写个简单的示例:

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
package main

import (
"encoding/json"
"fmt"
)

type Student struct {
ID int
Name string
Age int
}

func main() {

var students []Student
students = append(students, Student{
ID: 1,
Name: "张三",
Age: 18,
})
students = append(students, Student{
ID: 2,
Name: "李四",
Age: 19,
})
students = append(students, Student{
ID: 3,
Name: "王五",
Age: 20,
})

studentMap := make(map[int]*Student, len(students))
for _, student := range students {
studentMap[student.ID] = &student
}

bs, _ := json.Marshal(studentMap)
fmt.Println(string(bs))

}
+

上边代码输出如下:

+
1
{"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 的地址看一下:

+
1
2
3
4
for _, student := range students {
fmt.Printf("%p \n", &student)
studentMap[student.ID] = &student
}
+

输出为:

+
1
2
3
4
0xc0000a6040 
0xc0000a6040
0xc0000a6040
{"1":{"ID":3,"Name":"王五","Age":20},"2":{"ID":3,"Name":"王五","Age":20},"3":{"ID":3,"Name":"王五","Age":20}}
+

这种情况下我们应该用 students 中索引对应数据的指针,上边 for 循环修改如下:

+
1
2
3
4
for i, student := range students {
fmt.Printf("%p \n", &students[i])
studentMap[student.ID] = &students[i]
}
+

输出为:

+
1
2
3
4
0xc0000b8000 
0xc0000b8020
0xc0000b8040
{"1":{"ID":1,"Name":"张三","Age":18},"2":{"ID":2,"Name":"李四","Age":19},"3":{"ID":3,"Name":"王五","Age":20}}
+

上边的情况给 student 赋值也是有问题的:

+
1
2
3
4
5
6
for _, student := range students {
student.Name = "test"
}

bs, _ := json.Marshal(students)
fmt.Println(string(bs))
+

输出:

+
1
[{"ID":1,"Name":"张三","Age":18},{"ID":2,"Name":"李四","Age":19},{"ID":3,"Name":"王五","Age":20}]
+
+

Java 写习惯了就以为迭代时的 student 指向的是 students 中的地址。

+ +
+ + + + + + + +
+ + + + + + +
+ +
+ + + +
+ + + + + + + + +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2020/goland-protobuf-syntax-check/0.jpg b/2020/goland-protobuf-syntax-check/0.jpg new file mode 100644 index 0000000000..9c60eecc46 Binary files /dev/null and b/2020/goland-protobuf-syntax-check/0.jpg differ diff --git a/2020/goland-protobuf-syntax-check/1.png b/2020/goland-protobuf-syntax-check/1.png new file mode 100644 index 0000000000..101d94c0ae Binary files /dev/null and b/2020/goland-protobuf-syntax-check/1.png differ diff --git a/2020/goland-protobuf-syntax-check/index.html b/2020/goland-protobuf-syntax-check/index.html new file mode 100644 index 0000000000..91f7a4ff43 --- /dev/null +++ b/2020/goland-protobuf-syntax-check/index.html @@ -0,0 +1,500 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + GoLand Protobuf 语法检查 | 贾攀的流水账 + + + + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

贾攀的流水账

+ +
+

Panmax's Blog

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+ + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ GoLand Protobuf 语法检查 +

+ + +
+ + + + +
+ + +
+ +

公司的服务间调用使用的 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 后问题解决。

+
+

今天一下午光处理坑了,上线了一个限流功能,一直没有拿到打点数据,和治理组同事检查了所有地方都没有问题,最后治理组同事想起来,需要触发限流后才会打点,在监控中看到数据。

+ +
+ + + + + + + +
+ + + + + + +
+ +
+ + + +
+ + + + + + + + +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2020/homebrew-china-mirror/1.jpeg b/2020/homebrew-china-mirror/1.jpeg new file mode 100644 index 0000000000..e777716d49 Binary files /dev/null and b/2020/homebrew-china-mirror/1.jpeg differ diff --git a/2020/homebrew-china-mirror/index.html b/2020/homebrew-china-mirror/index.html new file mode 100644 index 0000000000..96b1baa617 --- /dev/null +++ b/2020/homebrew-china-mirror/index.html @@ -0,0 +1,526 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Homebrew 修改国内源 | 贾攀的流水账 + + + + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

贾攀的流水账

+ +
+

Panmax's Blog

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+ + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ Homebrew 修改国内源 +

+ + +
+ + + + +
+ + +

1

+

Homebrew 是 Mac 上最常用的软件包管理工具,可以简化 macOS 系统是的软件安装过程。但由于国内网络环境,每次更新时速度都不忍直视。为了提升速度和体验,建议修改为国内源。

+

目前有两个常用 Homebrew 源,分别是阿里镜像清华大学镜像。其中清华镜像在阿里镜像已有的 brewhomebrew-core 之外,还额外提供了 homebrew-cask 源。所以我采用的策略是:brewhomebrew-core 使用阿里的,homebrew-cask 使用清华的,原因是阿里在程序员心目中的地位是要高于清华的。

+

直接复制以下命令在终端运行即可:

+
1
2
3
git -C "$(brew --repo)" remote set-url origin https://mirrors.aliyun.com/homebrew/brew.git
git -C "$(brew --repo homebrew/core)" remote set-url origin https://mirrors.aliyun.com/homebrew/homebrew-core.git
git -C "$(brew --repo homebrew/cask)" remote set-url origin https://mirrors.tuna.tsinghua.edu.cn/git/homebrew/homebrew-cask.git
+

没有安装使用 homebrew-cask 的情况下,最后一条命令会报错,可以忽略。

+

之后执行 brew update 使配置生效并测试工作是否正常。

+

Homebrew 还提供了一个核心组件 Homebrew-bottles,可以提供一些包的二进制预编译版本,省去本地下载源码、编译源码的时间,提升安装效率,所以可以把 Homebrew-bottles 的源地址也进行替换,Homebrew-bottles 的地址是通过环境变量加载的,所以有两种修改方式:

+

临时生效:

+
1
export HOMEBREW_BOTTLE_DOMAIN=https://mirrors.aliyun.com/homebrew/homebrew-bottles
+

永久生效(以 zsh 为例):

+
1
2
echo 'export HOMEBREW_BOTTLE_DOMAIN=https://mirrors.aliyun.com/homebrew/homebrew-bottles' >> ~/.zshrc
source ~/.zshrc
+

Enjoy!


+

下边记录两个通过 homebrew 更新软件包后可能会出现的问题

更新 openssl 后新开命令行窗口报错

报错内容:

+
1
2
3
4
5
6
7
8
9
10
11
12
ERROR:root:code for hash md5 was not found.
Traceback (most recent call last):
File "/usr/local/Cellar/python@2/2.7.15_3/Frameworks/Python.framework/Versions/2.7/lib/python2.7/hashlib.py", line 147, in <module>
globals()[__func_name] = __get_hash(__func_name)
File "/usr/local/Cellar/python@2/2.7.15_3/Frameworks/Python.framework/Versions/2.7/lib/python2.7/hashlib.py", line 97, in __get_builtin_constructor
raise ValueError('unsupported hash type ' + name)
ValueError: unsupported hash type md5
ERROR:root:code for hash sha1 was not found.
……
File "/usr/local/Cellar/mercurial/4.9/lib/python2.7/site-packages/hgdemandimport/demandimportpy2.py", line 151, in __getattr__
return getattr(self._module, attr)
AttributeError: 'module' object has no attribute 'md5'
+

修复方法:

+

执行:ls /usr/local/Cellar/openssl,可以看到当前可用的 openssl 版本:

+
1
2
3
➜ ls /usr/local/Cellar/openssl

1.0.2o_1 1.0.2q
+

根据列出的版本,执行 brew switch openssl <版本号> 来指定版本(有可能在你本地只存在一个版本或和我这里有其他区别):

+
1
2
3
4
5
➜ brew switch openssl 1.0.2q

// 正常情况下会返回一下内容
Cleaning /usr/local/Cellar/openssl/1.0.2q
Opt link created for /usr/local/Cellar/openssl/1.0.2q
+

问题解决~

+

更新 MySQL 后出问题

Python 程序在连接 MySQL 时,报错:

+
1
2
3
4
5
6
7
8
9
10
11
12
13
Traceback (most recent call last):
File "/Users/jiapan/.virtualenvs/bossku/lib/python2.7/site-packages/flask/app.py", line 1997, in __call__
return self.wsgi_app(environ, start_response)
……
……
……
File "/Users/jiapan/.virtualenvs/bossku/lib/python2.7/site-packages/sqlalchemy/dialects/mysql/mysqldb.py", line 102, in dbapi
return __import__('MySQLdb')
File "/Users/jiapan/.virtualenvs/bossku/lib/python2.7/site-packages/MySQLdb/__init__.py", line 19, in <module>
import _mysql
ImportError: dlopen(/Users/jiapan/.virtualenvs/bossku/lib/python2.7/site-packages/_mysql.so, 2): Library not loaded: /usr/local/opt/mysql/lib/libmysqlclient.20.dylib
Referenced from: /Users/jiapan/.virtualenvs/bossku/lib/python2.7/site-packages/_mysql.so
Reason: image not found
+

你的报错可能和我这里稍有区别,主要是倒数第三行,我这里是 libmysqlclient.20.dylib,你那里可能是 libmysqlclient.18.dylib 或其他的,不过理论上都可以通过这个方法解决。

+

修复方法:

+

执行 ls /usr/local/lib | grep libmysqlclient,我这里可以看到如下内容:

+
1
2
3
libmysqlclient.21.dylib -> ../Cellar/mysql/8.0.19/lib/libmysqlclient.21.dylib
libmysqlclient.a -> ../Cellar/mysql/8.0.19/lib/libmysqlclient.a
libmysqlclient.dylib -> ../Cellar/mysql/8.0.19/lib/libmysqlclient.dylib
+

查看列表中有没有和报错中完全相同的文件,如果存在完全匹配的就直接建立对应软链到 /usr/local/opt/mysql/lib/,没有的话就用 libmysqlclient.dylib 代替。

+

我这里没有 libmysqlclient.20.dylib,所以我使用的命令如下:

+
1
ln -s /usr/local/lib/libmysqlclient.dylib /usr/local/opt/mysql/lib/libmysqlclient.20.dylib
+

问题解决~

+
+

参考:

+ + +
+ + + + + + + +
+ + + + + + +
+ +
+ + + +
+ + + + + + + + +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2020/last-day-in-qianxin/0.JPG b/2020/last-day-in-qianxin/0.JPG new file mode 100644 index 0000000000..add12f0107 Binary files /dev/null and b/2020/last-day-in-qianxin/0.JPG differ diff --git a/2020/last-day-in-qianxin/1.png b/2020/last-day-in-qianxin/1.png new file mode 100644 index 0000000000..530e3e45d8 Binary files /dev/null and b/2020/last-day-in-qianxin/1.png differ diff --git a/2020/last-day-in-qianxin/2.png b/2020/last-day-in-qianxin/2.png new file mode 100644 index 0000000000..eb80a9c394 Binary files /dev/null and b/2020/last-day-in-qianxin/2.png differ diff --git a/2020/last-day-in-qianxin/3.png b/2020/last-day-in-qianxin/3.png new file mode 100644 index 0000000000..9b9d7ba91b Binary files /dev/null and b/2020/last-day-in-qianxin/3.png differ diff --git a/2020/last-day-in-qianxin/index.html b/2020/last-day-in-qianxin/index.html new file mode 100644 index 0000000000..c8ec81adc8 --- /dev/null +++ b/2020/last-day-in-qianxin/index.html @@ -0,0 +1,515 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Last Day | 贾攀的流水账 + + + + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

贾攀的流水账

+ +
+

Panmax's Blog

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+ + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ Last Day +

+ + +
+ + + + +
+ + +
+ +

是的,今天是我在这家公司的最后一天,2017年4月1日-2020年9月29日,3年零6个月,在我目前的职业生涯中是最长的一次经历。在这中间已经有过几次想要离开,但是都以各种各样的理由说服了自己。任何公司都有自己的问题,所以我在这里不会对这家公司进行过多的评判。

+

下图分别是我入职时发的动态和离职时的离职证明。

+
+ +
+ +

在这段经历中,也得到了老板的器重,去年年底的时候被任命了「研发总监」,需要管理部门内40+人的研发团队(之前成都的负责人协助我做副手),说实话当时的压力非常的大,尤其是对另一个团队的业务并不是那么了解。好在今年4月份,集团调整了政策,各职能线只能有一个 L3,经过慎重的考虑,老板认为成都负责人对整体业务更熟悉、管理经验更丰富,所以最终任命了他为留下来的研发总监。

+

老板当时还跟我做了一些解释,担心我有什么想法。说实话,我对这事还是有些窃喜的,因为压力不至于那么大了,而且我目前不太有意愿投非常大的精力在管理上。私下里也有同事私聊我说你这咋还降了,我云淡风轻的回一句「不重要」。后来我就管理大约一半的研发,也就是做平台开发的这些,加上成都那边有两个小组长的帮助,我的压力少了很多。

+

上个月底(8月)的一个周末,我坐下来疏理了一下自己这几年的工作,发觉到自己的成长空间受到了限制、发展方向偏离了主流,于是下定了决心换一份工作了,于是又抽出了 2 小时的时间整理了一份简历出来。周一的时候考虑从哪家开始面起,在看翻看微信的时候,偶然间在一个技术交流群中看到了一条内推「探探」的消息,于是和对方加了好友,让他帮我推探探的直播部门。

+

我选择探探的原因,是我在去年学习 Go 期间参与了探探举办的 Gopher China 大会,当时探探给我留下了不错的印象,感受到探探地技术气氛不错。

+

探探 HR 效率很高,当天下午就收到了HR的反馈,约了第二天的面试,当天和两个面试官进行了沟通,晚上的时候又约了第三轮面试,当时告诉我是终面。

+

第三天上午面完后,HR 反馈说因为职级可以到技术专家,所以需要加一轮交叉面试,约了当天的下午,和四面面试官聊的也有一个小时,当天晚上同时进行了 HR 面。

+

第四天我上报了自己的期望薪资,剩下的就是等待反馈了。大概是第二周的周二HR给了我反馈,顺利拿到了 offer。在此之前我已经和老板沟通了我要走的打算,也和一些朋友进行了沟通,很多人都劝我去大厂,让我面面大厂还有朋友主动帮我内推大厂,但我这个人不太爱做选择,而且加上本身没有特别想去大厂的欲望再加上对探探的印象也算不错,于是没有投入太多的精力去进行后边的事情了。其实这里还有一个原因,是我自己并没有为这一次跳槽做太多的事前准备,没有准备面经、八股、刷算法啥的,找到一个自己还算满意的也就定了,希望自己这次选择的是一家「小而美」。

+
+

以上大概就是这次的一个跳槽经历。

+
+

自从入职这家公司后,从来还没有休过长假,每年的年假都被作废掉,于是在交接的这一个月中,请了一周的假带家人跑北疆玩了一圈,看了看祖国的大好河山,感受了一下什么叫地大物博,顶部图选自旅途中的一张照片。

+

说到探探还有一段渊源,我在5年前和一群有趣的人做过一个创业项目,和探探早期的模式非常像(如下图),但是后边因为各种原因失败了,但那次之后我内心深处还是想回到社交这个领域。

+
+ +

去年在参加 Gopher China 大会的时候脑海中也闪过一个想法,未来如果有机会去探探就好了。

+

于是5年后,我回来了。

+

Life is a circle.

+ +
+ + + + + + + +
+ + + + + + +
+ +
+ + + +
+ + + + + + + + +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2020/multi-repo-vs-mono-repo/1.jpeg b/2020/multi-repo-vs-mono-repo/1.jpeg new file mode 100644 index 0000000000..64fc162a1a Binary files /dev/null and b/2020/multi-repo-vs-mono-repo/1.jpeg differ diff --git a/2020/multi-repo-vs-mono-repo/2.jpg b/2020/multi-repo-vs-mono-repo/2.jpg new file mode 100644 index 0000000000..2eb36e4a30 Binary files /dev/null and b/2020/multi-repo-vs-mono-repo/2.jpg differ diff --git a/2020/multi-repo-vs-mono-repo/index.html b/2020/multi-repo-vs-mono-repo/index.html new file mode 100644 index 0000000000..bd3b062cd5 --- /dev/null +++ b/2020/multi-repo-vs-mono-repo/index.html @@ -0,0 +1,522 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 单体仓库与多仓库——两种源码组织模式介绍 | 贾攀的流水账 + + + + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

贾攀的流水账

+ +
+

Panmax's Blog

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+ + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ 单体仓库与多仓库——两种源码组织模式介绍 +

+ + +
+ + + + +
+ + +

+
+

我在去年和前年主导了公司两个产品后端的技术选型和整体架构,并分别尝试了两种源码组织模式:多仓库和单体仓库。对两种仓库的利弊也有了很大程度上的感受,基于这个前提对这两种模式做个总结。

+
+

阅读本文后你会明白:什么是单体仓库?为什么 Google 采用单体仓库?

+

单体应用和微服务应用

在介绍单体仓库和多仓库前,先来说说什么叫单体应用和微服务应用。

+

微服务相比单体应用最大的好处是可以独立的开发测试部署和扩展。单体应用一般采用单体仓库,但是微服务的代码仓库该如何组织呢?一定是每个服务一个仓库吗?

+

其实也不一定,针对微服务的代码组织,业界有两种主要的实践,一种是多仓库(multi-repo)也就是每个服务开一个源码仓库,另一种叫单体仓库(mono-repo)所有源码都在同一个仓库中,尽管整个应用采用的微服务架构。

+

+

多仓库

单体仓库和多仓库都是有利有弊的。

+

多仓库的好处是显而易见的:

    +
  1. 每一个服务都有一个独立的仓库,职责单一。
  2. +
  3. 代码量和复杂性受控,服务由不同的团队独立维护、边界清晰。
  4. +
  5. 单个服务也易于自治开发测试部署和扩展,不需要集中管理集中协调。
  6. +
+

多仓库存在的问题:

    +
  1. 项目代码不容易规范。每个团队容易各自为政,随意引入依赖,code review 无法集中开展,代码风格各不相同。
  2. +
  3. 项目集成和部署会比较麻烦。虽然每个项目服务易于集成和部署,但是整个应用集成和部署的时候由于仓库分散就需要集中的管理和协调。
  4. +
  5. 开发人员缺乏对整个项目的整体认知。开发人员一般只关心自己的服务代码,看不到项目整体,造成缺乏对项目整体架构和业务目标整体性的理解。
  6. +
  7. 项目间冗余代码多。每个服务一个服务一个仓库,势必造成团队在开发的时候走捷径,不断地重复造轮子而不是去优先重用其他团队开发的代码。
  8. +
+

单体仓库

单体仓库可以解决部分上边提到的问题。

+

单体仓库的好处:

    +
  1. 易于规范代码。所有的代码在一个仓库当中就可以标准化依赖管理,集中开展 code review,规范化代码的风格。
  2. +
  3. 易于集成和部署。所有的代码在一个仓库里面,配合自动化构建工具,可以做到一键构建、一键部署,一般不需要特别的集中管理和协调。
  4. +
  5. 易于理解项目整体。开发人员可以把整个项目加载到本地的 IDE 当中,进行 code review,也可以直接在本地部署调试,方便开发人员把握整体的技术架构和业务目标。
  6. +
  7. 易于重用。所有的代码都在一个仓库中,开发人员开发的时候比较容易发现和重用已有的代码,而不是去重复造轮子,开发人员(通过 IDE 的支持)容易对现有代码进行重构,可以抽取出一些公共的功能进一步提升代码的质量和复用度。
  8. +
+

在工业界,世界上采用单体仓库管理源码的公司并不少,如 Google、Facebook、Twitter 这些互联网巨头,包括通过去年B站泄露的源码也可以看出,B站也是用的单体仓库进行的管理。虽然这些公司系统庞大、服务众多,内部研发团队人数众多,但是依然采用了单体仓库并且都很成功。

+

单体仓库也是有弊端的,随着公司业务团队规模的变大,单一的代码库会变得越来越庞大复杂性也呈极度的上升,所以这些互联网巨头之所以能够玩转单体仓库,一般都有独立的代码管理和集成团队进行支持,也有配套的自动化构建工具来支持,如 Google 自研的面向单体仓库的构建工具 Bazel:https://bazel.build/ 和 Facebook 的 Buck:https://buck.build/

+

初创公司在早期服务不是特别多的情况下,采用单体仓库比较合适。

+

##总结:

+

微服务架构并不是主张所有的东西都要独立自治,至少代码仓库就可以集中管理,而且这也是业界的最佳实践之一。

+ +
+ + + + + + + +
+ + + + + + +
+ +
+ + + +
+ + + + + + + + +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2020/nginx-too-many-open-files/1.jpg b/2020/nginx-too-many-open-files/1.jpg new file mode 100644 index 0000000000..312b9d9e98 Binary files /dev/null and b/2020/nginx-too-many-open-files/1.jpg differ diff --git a/2020/nginx-too-many-open-files/index.html b/2020/nginx-too-many-open-files/index.html new file mode 100644 index 0000000000..f568d8fda0 --- /dev/null +++ b/2020/nginx-too-many-open-files/index.html @@ -0,0 +1,507 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 解决 CentOS 7 下 Nginx 报 Too many open files | 贾攀的流水账 + + + + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

贾攀的流水账

+ +
+

Panmax's Blog

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+ + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ 解决 CentOS 7 下 Nginx 报 Too many open files +

+ + +
+ + + + +
+ + +

前几天在对开发环境中的服务进行压测时 Nginx 出现 Too many open files 的错误,这里记录下解决方法。

+

1.jpg

+

检查文件句柄

先来通过两个命令检查下 master 进程 和 worker 进程的文件句柄限制。

+

在 Nginx 运行时,检查当前 master 进程的限制:

+
1
2
3
cat /proc/$(cat /var/run/nginx.pid)/limits|grep open.files

Max open files 1024 4096 files
+

检查 worker 进程:

+
1
2
3
4
5
6
ps --ppid $(cat /var/run/nginx.pid) -o %p|sed '1d'|xargs -I{} cat /proc/{}/limits|grep open.files

Max open files 1024 4096 files
Max open files 1024 4096 files
Max open files 1024 4096 files
Max open files 1024 4096 files
+

上边返回结果的第二列和第三列分别为软限制(soft limit)和硬限制(hard limit),下边我们来对其进行调整。

+

调整限制

    +
  1. /etc/sysctl.conf 中加上 fs.file-max = 70000
  2. +
  3. /etc/security/limits.conf 中加上 nginx soft nofile 10000nginx hard nofile 30000
  4. +
  5. 执行 sysctl -p 使配置生效
  6. +
  7. /etc/nginx/nginx.conf 中加上 worker_rlimit_nofile 30000;
  8. +
+

虽然 Nginx 可以通过 nginx -s reload 使配置生效,但这种方式并不会让全部进程都应用上新的配置,如果你在多核机器下,可以实验下:在执行这个操作后,通过检查 worker 进程句柄限制(方法见上文),还是有部分进程的句柄被限制为 S1024/H4096,即使试用 nginx -s quit 也不管用。解决方法是用 kill 命令杀掉 Nginx 后重新启动,这样所有的 Nginx 进程就都有了 S10000/H30000 的文件句柄限制。

+
1
2
pkill -9 nginx
systemctl start nginx
+

再次验证 worker 进程

+
1
2
3
4
5
6
ps --ppid $(cat /var/run/nginx.pid) -o %p|sed '1d'|xargs -I{} cat /proc/{}/limits|grep open.files

Max open files 30000 30000 files
Max open files 30000 30000 files
Max open files 30000 30000 files
Max open files 30000 30000 files
+

可以看到配置已在全部 worker 进程上生效。

+ +
+ + + + + + + +
+ + + + + + +
+ +
+ + + +
+ + + + + + + + +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2020/resolve-old-goland-delve-bug/0.jpg b/2020/resolve-old-goland-delve-bug/0.jpg new file mode 100644 index 0000000000..a0f865b67d Binary files /dev/null and b/2020/resolve-old-goland-delve-bug/0.jpg differ diff --git a/2020/resolve-old-goland-delve-bug/index.html b/2020/resolve-old-goland-delve-bug/index.html new file mode 100644 index 0000000000..7bce7ac6e7 --- /dev/null +++ b/2020/resolve-old-goland-delve-bug/index.html @@ -0,0 +1,501 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 解决低版本 GoLand 启动服务报 Version of Delve is too old for this version of Go | 贾攀的流水账 + + + + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

贾攀的流水账

+ +
+

Panmax's Blog

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+ + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ 解决低版本 GoLand 启动服务报 Version of Delve is too old for this version of Go +

+ + +
+ + + + +
+ + +
+ +

今天算是入职新公司的第一天,配置好开发环境后,尝试用 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

+

但是我的 ToolBox 在 Check for updates 时没有响应,所以需要通过其他方式进行了解决。

+

更新 dlv,并将 GoLand 中的 dlv 路径指向更新后的路径

1) go get -u github.com/go-delve/delve/cmd/dlv
2) 执行以下命令并将打印的路径复制下来:

+
1
2
3
4
➜ echo `go env | grep GOPATH | cut -d "\"" -f 2`/bin/dlv

# 以下是打印的结果,进行复制
/Users/jiapan/go/bin/dlv`
+

3) 在 GoLand 中 Help -> Edit Custom Properties(之前没编辑过会提示新建)
4) 新增一项 dlv.path={你复制的路径},比如我的:

+
1
dlv.path=/Users/jiapan/go/bin/dlv
+

再次启动服务,问题解决。

+
+

delve 是 go 语言的 debug 工具,delve 的意思是:钻研、探索,用这个来命名一个 debug 工具还是非常形象的。

+
+ +
+ + + + + + + +
+ + + + + + +
+ +
+ + + +
+ + + + + + + + +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2020/sync-async-blocking/index.html b/2020/sync-async-blocking/index.html new file mode 100644 index 0000000000..23fd88f0a8 --- /dev/null +++ b/2020/sync-async-blocking/index.html @@ -0,0 +1,512 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 再论《阻塞、非阻塞 I/O 与同步、异步 I/O》 | 贾攀的流水账 + + + + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

贾攀的流水账

+ +
+

Panmax's Blog

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+ + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ 再论《阻塞、非阻塞 I/O 与同步、异步 I/O》 +

+ + +
+ + + + +
+ + +

去年的时候以一篇比较尬的故事(同步、异步、阻塞、非阻塞那些事)的形式介绍了一下阻塞、非阻塞 I/O 与同步、异步 I/O的区别和联系,这次重新把知识点总结一下,这篇只留下干货,湿货继续看那篇故事。

+

从应用程序角度

根据应用程序是否阻塞自身运行,可以把 I/O 分为阻塞 I/O 和非阻塞 I/O。

+
    +
  • 所谓阻塞 I/O,是指应用程序在执行 I/O 操作后,如果没有获得响应,就会阻塞当前线程,不能执行其他任务。
  • +
  • 所谓非阻塞 I/O,是指应用程序在执行 I/O 操作后,不会阻塞当前的线程,可以继续执行其他的任务。
  • +
+

从系统角度

根据 I/O 响应的通知方式的不同,可以把文件 I/O 分为同步 I/O 和异步 I/O。

+
    +
  • 所谓同步 I/O,是指收到 I/O 请求后,系统不会立刻响应应用程序;等到处理完成,系统才会通过系统调用的方式,告诉应用程序 I/O 结果。
  • +
  • 所谓异步 I/O,是指收到 I/O 请求后,系统会先告诉应用程序 I/O 请求已经收到,随后再去异步处理;等处理完成后,系统再通过事件通知的方式,告诉应用程序结果。
  • +
+

总结

阻塞 / 非阻塞和同步 / 异步,其实就是两个不同角度的 I/O 划分方式。它们描述的对象也不同:

+
    +
  • 阻塞 / 非阻塞针对的是 I/O 调用者(即应用程序)
  • +
  • 同步 / 异步针对的是 I/O 执行者(即系统)
  • +
+

举例

比如在 Linux I/O 调用中:

+
    +
  • 系统调用 read 是同步读,所以,在没有得到磁盘数据前,read 不会响应应用程序。
  • +
  • aio_read 是异步读,系统收到 AIO 读请求后不等处理就返回了,而具体的 read 结果,再通过回调异步通知应用程序。
  • +
+

再如,在网络套接字的接口中:

+
    +
  • 使用 send() 直接向套接字发送数据时,如果套接字没有设置 O_NONBLOCK 标识,那么 send() 操作就会一直阻塞,当前线程也没法去做其他事情。
  • +
  • 当然,如果你用了 epoll,系统会告诉你这个套接字的状态,那就可以用非阻塞的方式使用。当这个套接字不可写的时候,你可以去做其他事情,比如读写其他套接字。
  • +
+ +
+ + + + + + + +
+ + + + + + +
+ +
+ + + +
+ + + + + + + + +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2020/test-http-in-https/index.html b/2020/test-http-in-https/index.html new file mode 100644 index 0000000000..e47f5a2c40 --- /dev/null +++ b/2020/test-http-in-https/index.html @@ -0,0 +1,485 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 测试 https 页面中嵌入 http 元素 | 贾攀的流水账 + + + + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

贾攀的流水账

+ +
+

Panmax's Blog

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+ + +
+
+
+ + +
+ + + + + + + + + + + + +
+ +
+ + + +
+ + + + + + + + +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2020/the-7-habits-of-highly-effective-people/1.png b/2020/the-7-habits-of-highly-effective-people/1.png new file mode 100644 index 0000000000..ea8dffe8bf Binary files /dev/null and b/2020/the-7-habits-of-highly-effective-people/1.png differ diff --git a/2020/the-7-habits-of-highly-effective-people/index.html b/2020/the-7-habits-of-highly-effective-people/index.html new file mode 100644 index 0000000000..69596f388c --- /dev/null +++ b/2020/the-7-habits-of-highly-effective-people/index.html @@ -0,0 +1,520 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 《高效能人士的七个习惯》 脑图 && 好句 | 贾攀的流水账 + + + + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

贾攀的流水账

+ +
+

Panmax's Blog

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+ + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ 《高效能人士的七个习惯》 脑图 && 好句 +

+ + +
+ + + + +
+ + +

+
+

好句

对自己要有耐心,因为自我成长是神圣的,同时也是脆弱的,是人生中最大规模的投资。

+

人的一生包含了许多成长和进步阶段,必须循序渐进,每一步都十分重要,并且需要时间,不能挑过。

+

承认自己的无知往往是求知的第一步。

+

教育孩子应该有充分的耐心让他们体会拥有的感觉,同时用足够的智慧告诉他们付出的价值,另外还要以身作则。

+

人们越是依赖立竿见影的解决办法,越是加剧了问题潜在的隐患。

+

如果不能持续投资以增进自己的产能,眼光就会受到局限,只能在现有的职位上踏步。p45

+

所有积极主动的人都深谙其道,不会把自己的行为归咎于环境、外界条件或他人的影响。p57

+

对力不能及之事处之泰然,对能够改变的则全力以赴。p65

+

对于已经无法返回的错误,积极主动的人不是懊恨不已,而是承认往日错误已属关注圈的事实,那是人力无法企及的范畴,既不能从头来过,也不能改变结果。p65

+

学会做照亮他人的蜡烛,而不是评判对错的法官;以身作则,而不是一心挑错;解决问题,而不是制造事端。p67

+

如果你一直认为问题「存在于外部」,那么请马上打住,因为这种想法本身就是问题。p67

+

太多人成功之后,反而感到空虚;得到名利之后,却发现牺牲了更可贵的东西。因此我们务必紧盯真正重要的愿景,然后勇往直前坚持到底,使生活充满意义。p69

+

管理是正确地做事,领导则是做正确的事。p74

+

一个人的应变能力取决于他对自己的本性、人生目标以及价值观不变信念。p78

+

有效管理是掌握重点式的管理,它把最重要的事放在第一位。由领导决定什么是重点后,再靠自制力来掌握重点,时刻把他们放在第一位,以免被感觉、情绪或冲动所左右。p96

+

对人不讲效率,对事才可如此。对人应讲效用,即某一行为是否有效。p109

+

管理者注重建立制度,然后汇集群力共同完成工作。p111

+

信任是促使人们进步的最大动力,因为信任能够让人们表现出自己最好的一面。p113

+

责任型授权是关于授权的全新思维方式,它改变了人际关系的性质:因为分得工作的人成为自己的老板,受自己内心良知的指引,努力兑现自己的承诺,达到既定目标。p114

+

当孩子感觉受重视的时候,亲子之间就建起了一座爱与信任的坚实桥梁。p126

+

经验表明,在家族式或者建立在友谊基础上的生意启动之前,最好先就「不能双赢就好聚好散」这一点达成协议,这样的繁荣才不会导致关系的破裂。p133

+

成熟就是在表达自己的情感和信念的同时又能体谅他人的想法和感受的能力。p135

+

领导所要做的就是放手,让有责任心、积极处事以及具有自我领导能力的人独立完成任务。p140

+

双赢协议是管理的核心内容。有了这样的一个协议,员工就可以在协议规定的范围内进行有效的自我管理,而经理就像是赛跑中的开路车一样,待一切顺利开展后悄悄退出,做好后勤工作。p141

+

你应该时刻想着先理解别人,这是你力所能及的。如果你把精力放在影响圈内,就能真正且深入地了解对方。你会获得准确的信息,能迅速抓住事件的核心,建立自己的情感账户,还能给对方提供给你有效合作所必须的「心里空气」。p157

+

与所见略同嗯人沟通,益处不大,要有分歧才有收获。p159

+

不要在意别人的无理行径,避开那些消极力量,发现并利用别人的优势,提高自己的认识,扩展自己的视野。p172

+

工作本身并不能带来经济上的安全感,具备良好的思考、学习、创造与适应能力,才能立于不败之地。p175

+

我们越擅长发觉别人的潜力,越能在配偶、子女、同事或雇员身上发挥自己的想象力,而不是记忆力。p183

+

良知是一种天赋,帮助我们判断自己是否背离了正确的原则,然后引导我们向这些原则靠拢。p186

+ +
+ + + + + + + +
+ + + + + + +
+ +
+ + + +
+ + + + + + + + +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2020/werkzeug-middleware-proxy-fix-ProxyFix-review/0.jpg b/2020/werkzeug-middleware-proxy-fix-ProxyFix-review/0.jpg new file mode 100644 index 0000000000..2421538eca Binary files /dev/null and b/2020/werkzeug-middleware-proxy-fix-ProxyFix-review/0.jpg differ diff --git a/2020/werkzeug-middleware-proxy-fix-ProxyFix-review/1.png b/2020/werkzeug-middleware-proxy-fix-ProxyFix-review/1.png new file mode 100644 index 0000000000..c519417ceb Binary files /dev/null and b/2020/werkzeug-middleware-proxy-fix-ProxyFix-review/1.png differ diff --git a/2020/werkzeug-middleware-proxy-fix-ProxyFix-review/2.png b/2020/werkzeug-middleware-proxy-fix-ProxyFix-review/2.png new file mode 100644 index 0000000000..bf9f53fc6b Binary files /dev/null and b/2020/werkzeug-middleware-proxy-fix-ProxyFix-review/2.png differ diff --git a/2020/werkzeug-middleware-proxy-fix-ProxyFix-review/3.png b/2020/werkzeug-middleware-proxy-fix-ProxyFix-review/3.png new file mode 100644 index 0000000000..c3f2669dda Binary files /dev/null and b/2020/werkzeug-middleware-proxy-fix-ProxyFix-review/3.png differ diff --git a/2020/werkzeug-middleware-proxy-fix-ProxyFix-review/4.png b/2020/werkzeug-middleware-proxy-fix-ProxyFix-review/4.png new file mode 100644 index 0000000000..ddcd4c9206 Binary files /dev/null and b/2020/werkzeug-middleware-proxy-fix-ProxyFix-review/4.png differ diff --git a/2020/werkzeug-middleware-proxy-fix-ProxyFix-review/5.png b/2020/werkzeug-middleware-proxy-fix-ProxyFix-review/5.png new file mode 100644 index 0000000000..c259fcb824 Binary files /dev/null and b/2020/werkzeug-middleware-proxy-fix-ProxyFix-review/5.png differ diff --git a/2020/werkzeug-middleware-proxy-fix-ProxyFix-review/index.html b/2020/werkzeug-middleware-proxy-fix-ProxyFix-review/index.html new file mode 100644 index 0000000000..e1b5977c47 --- /dev/null +++ b/2020/werkzeug-middleware-proxy-fix-ProxyFix-review/index.html @@ -0,0 +1,543 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + werkzeug.middleware.proxy_fix.ProxyFix 问题复盘 | 贾攀的流水账 + + + + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

贾攀的流水账

+ +
+

Panmax's Blog

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+ + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ werkzeug.middleware.proxy_fix.ProxyFix 问题复盘 +

+ + +
+ + + + +
+ + +

werkzeug.middleware.proxy_fix.ProxyFix 问题复盘

+

背景

很多人知道我在运营着一个 SaaS 站点:https://bossku.cn/,用来给中小商家提供进销存服务,对于没有特殊需求的商户来说基本是免费使用的,目前注册商家 5000+,平稳运行 5 年多了。为了降低成本,中间做了好几次迁移,最早是在阿里云,后来迁移到了新浪的SAE,后边因为腾讯云有活动,又迁移到了腾讯云。

+

这段时间由于疫情的原因在家办公,这周末时间比较充裕所以就看了看 Sentry 日志准备改几个 bug,改 bug 花了 15 分钟,踩坑踩了大半天,接下来我把这次遇到的坑进行一下复盘。

+

这个项目是刚毕业没多久开始写的,那个时候也不懂什么自动化运维的东西,每次改完代码都是手动把新代码更新到服务器上(项目用 Python 写的,所以线上服务器 git pull 一下再重启一下 gunicorn 就可以了)。

+

迁移到 SAE 后就完全不用考虑运维的事情了,项目目录下写好描述文件,把代码推到指定地址上就可以完成一次发布了(甚至连负载均衡、HA 这些都不用考虑,很省心)。用了两年 SAE 感觉成本还是有些高,毕竟这个项目并没有太多收入,正好又看到腾讯云的活动,算了一下如果迁移到腾讯云可以实现收支平衡甚至能有些小盈,于是大概两年前把项目迁移到了现在在用的腾讯云上,那个时候已经对自动化和容器化有意识了,在调研一些方案后,最后选择了把项目进行容器化,借助 DaoCloud 实现持续发布。

+

这个方案的实现流程很简单:

+
    +
  1. 首先在我的腾讯云主机上安装 DaoCloud 的监控插件。
  2. +
  3. 在 DaoCloud 上 hook 一个 git 项目,每当项目有更新后会根据 Dockerfile 的描述进行项目打包生成镜像。
  4. +
  5. 然后在页面上进行配置,镜像生成完成后自动在指定机器上进行部署。
  6. +
+

+

因为那时候 Github 的私有仓库还不是免费的,所以我用了国内的 coding 作为项目代码的托管仓库,完成迁移后,每过一段时间就改几个小 bug,小日子一直安逸地前进着。由于去年下半年以来工作比较紧张,就没有再去管过这个项目了,在这期间 coding 貌似被腾讯收购了,中间的很多流程发生了变动,每次登录后都会跳转到腾讯开发者的页面,我也并没有太关心。

+

问题

直到昨天我再去维护的时候,发现之前的配置的 git 地址失效了,DaoCloud 上的 coding 授权也失效了,再去关联也关联不上(吐槽一下,这么严重的问题这么长时间了都没发现吗),DaoCloud hook 不到我的代码更新,自然也不能完成后续的流程,所以我在第一时间把源码迁移到了 Github 的 private repo(话外语:如果之前 Github 的私有仓库是免费的我肯定也不会用国内的)。

+

由于 DaoCloud 无法修改项目所关联的 Git 地址,只能删掉重新创建新的项目来关联新的地址,都配置好后又出了幺蛾子, DaoCloud 的镜像仓库挂了,镜像打包后无法 push 到仓库,自然也就无法发布应用了,当时找了客服,客服没有回复(直到我写这篇 blog 的时候,一天过去了客服依然没有任何响应)。

+

+

+

下午的时候再去看,仓库恢复了,项目启动后却报错了:

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
[2020-02-08 20:50:15 +0000] [10] [ERROR] Exception in worker process
Traceback (most recent call last):
File "/usr/local/lib/python2.7/site-packages/gunicorn/arbiter.py", line 583, in spawn_worker
worker.init_process()
File "/usr/local/lib/python2.7/site-packages/gunicorn/workers/ggevent.py", line 203, in init_process
super(GeventWorker, self).init_process()
File "/usr/local/lib/python2.7/site-packages/gunicorn/workers/base.py", line 129, in init_process
self.load_wsgi()
File "/usr/local/lib/python2.7/site-packages/gunicorn/workers/base.py", line 138, in load_wsgi
self.wsgi = self.app.wsgi()
File "/usr/local/lib/python2.7/site-packages/gunicorn/app/base.py", line 67, in wsgi
self.callable = self.load()
File "/usr/local/lib/python2.7/site-packages/gunicorn/app/wsgiapp.py", line 52, in load
return self.load_wsgiapp()
File "/usr/local/lib/python2.7/site-packages/gunicorn/app/wsgiapp.py", line 41, in load_wsgiapp
return util.import_app(self.app_uri)
File "/usr/local/lib/python2.7/site-packages/gunicorn/util.py", line 350, in import_app
__import__(module)
File "/usr/local/lib/python2.7/site-packages/gevent/builtins.py", line 96, in __import__
result = _import(*args, **kwargs)
File "/code/wsgi.py", line 5, in <module>
from werkzeug.contrib.fixers import ProxyFix
File "/usr/local/lib/python2.7/site-packages/gevent/builtins.py", line 96, in __import__
result = _import(*args, **kwargs)
ImportError: No module named contrib.fixers
+

其实这个原因写的很清楚了,/code/wsgi.py 文件的第 5 行包导入失败。因为在处理前期问题时花费了很长时间,已经没有太多耐心了,而且在我本地直接启动是没有问题的,所以没有太关心项目代码的报错,只注意到了最后的:

+
1
ImportError: No module named contrib.fixers
+

解决

Google 后发现遇到这个问题的人很少,而且根据几个有限的回答进行修改尝试后也都无济于事,比如更新 pip 和 setuptools 等。

+

于是我就开始自己找问题。最开始怀疑的是 Python 源有问题,我一直使用的是阿里的源:

+
1
RUN 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 上搜索了这个包名,就在这时我看到一句话使我眼前一亮:

+

+

点进去后:

+

+

这个 ProxyFix 类已经在 werkzeug 1.0 版本中移除了,通过查看我的 requirement.txt 的文件:

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
celery==3.1.24
enum34==1.1.6
Flask==0.12.2
Flask-Caching==1.3.1
Flask-DebugToolbar==0.10.0
Flask-WTF==0.14.2
Flask-Login==0.4.0
Flask-SQLAlchemy==2.1
Flask-Script==2.0.5
Flask-Migrate==2.0.0
gunicorn==19.9.0
gevent==1.3.7
ipython==5.1.0
MySQL-python==1.2.5
redis==2.10.5
raven==6.3.0
msgpack-python==0.4.8
captcha==0.2.1
yunpian-python-sdk==1.0.0
flask-cors==3.0.7
+

可以看到我并没有指定要安装 Werkzeug 的版本,Werkzeug 是一个 WSGI 工具包。熟悉 Flask 的同学都知道,Flask 依赖了 Werkzeug,大部分情况下都只需要安装 Flask 就可以直接使用 Werkzeug 这个工具包了。

+

查看 Flask 的源码,setupy.py 部分内容如下:

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
setup(
name='Flask',
version=version,
url='http://github.com/pallets/flask/',
license='BSD',
author='Armin Ronacher',
author_email='armin.ronacher@active-4.com',
description='A microframework based on Werkzeug, Jinja2 '
'and good intentions',
……
install_requires=[
'Werkzeug>=0.7',
'Jinja2>=2.4',
'itsdangerous>=0.21',
'click>=2.0',
],
+

可以看到 Flask 声明了自己需要依赖 0.7 以上的 Werkzeug,这个时候答案已经浮出水面了。

+

为什么我本地可以启动?

因为我本地的依赖包是很久之前安装的,所以我本地的 Werkzeug 版本是 0.14.1 的:

+
1
2
➜ pip freeze | grep Werk
Werkzeug==0.14.1
+

为什么 DaoCloud 之前一直没问题?

因为 DaoCloud 打包时有缓存层,从第一次构建完后,pip install 那一层就被缓存了下来,所以后边的都是用的缓存,安装的包也都是老版本,但是我昨天把 DaoCloud 上原有项目进行了删除、创建了一个新项目,之前的那些缓存层也就失效了,重新 pip install 时自然会去安装最新版本的 Werkzeug,这时候就安装了 1.0 以上版本,所以我在代码里引用的 ProxyFix 就找不到了。

+

为什么我要用 ProxyFix?

我在部署时采用了 Nginx 作为反向代理,这时候需要重写一些 HTTP 头来让应用正常工作,如:

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
server {
listen 80;

server_name _;

access_log /var/log/nginx/access.log;
error_log /var/log/nginx/error.log;

location / {
proxy_pass http://127.0.0.1:8000/;
proxy_redirect off;

proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
}
+

Nginx 添加了一些请求头来辅助应用获取真实的请求来源,这个时候需要用到 ProxyFix 处理一下这些请求头来让 WSGi 去正确处理这个请求,否则 WSGI 就可能认为这个请求来自服务器而不是真实的客户端。

+

所以我在程序入口前加入了:

+
1
2
3
4
5
6
from werkzeug.contrib.fixers import ProxyFix

from bossku.app import create_app

app = create_app()
app.wsgi_app = ProxyFix(app.wsgi_app)
+

修复

修复这个问题有两种做法:

+
    +
  1. 根据官方文档修改源码:从 werkzeug.middleware.proxy_fix 导入 ProxyFix 类。
  2. +
  3. 手动指定 Werkzeug 的版本号。
  4. +
+

我选择了第二种方式,原因是我不清楚新版本的 Werkzeug 还会有哪些不兼容问题,于是我在 requirements.txt 中安装 Flask 前加入了一行 werkzeug==0.14.1,重新在本地 Docker build 并启动,一切正常,问题解决了。

+

总结

回顾整个问题的解决过程,还是因为自己太浮躁导致的,只想着快速把问题解决,而没有踏下心来看看每一行报错提示,更没有想着去官方文档中看看这个类库有没有变动,其实一开始也压根没想到是因为依赖的类库做了不兼容更新导致的。

+

通过解决这个问题,还让我知道了为什么很多地方推荐 requirements.txt 这个文件要通过在本地执行 pip freeze > requirements.txt 来生成(我通常也都是这样做的),这样生成的描述文件中依赖所依赖的那些包的版本号也会固定下来。但是在这个项目中我为了追求精简和美观,采取了手动维护这个文件:每新增一个依赖,就手动在这个文件内新增一行,这就导致了依赖所依赖的那些包的版本可能在一台新机器上重新安装时发生变动。

+ +
+ + + + + + + +
+ + + + + + +
+ +
+ + + +
+ + + + + + + + +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2021/A-brief-history-of-humankind/index.html b/2021/A-brief-history-of-humankind/index.html new file mode 100644 index 0000000000..2a9dcf656b --- /dev/null +++ b/2021/A-brief-history-of-humankind/index.html @@ -0,0 +1,1020 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 《人类简史》摘抄 | 贾攀的流水账 + + + + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

贾攀的流水账

+ +
+

Panmax's Blog

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+ + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ 《人类简史》摘抄 +

+ + +
+ + + + +
+ + +

我们从农业革命能学到的最重要一课,很可能就是物种演化上的成功并不代表个体的幸福。

+
+

就演化而言,牛可能是有史以来最成功的动物。但同时,它们也是地球上生活最悲惨的动物。

+
+

在农业革命之后,人类成了远比过去更以自我为中心的生物,与“自己家”紧密相连,但与周遭其他物种画出界线。

+
+

历史只告诉了我们极少数的人在做些什么,而其他绝大多数人的生活就是不停挑水耕田。

+
+

史上的场场战争和革命,多半起因都不是粮食短缺。

+
+

虚构故事的力量强过任何人的想象。

+
+

大多数的人类合作网络最后都成了压迫和剥削。

+
+

支持它们的社会规范既不是人类自然的天性本能,也不是人际的交流关系,而是他们都相信着共同的虚构神话故事。

+
+

人人生而平等,造物者赋予他们若干不可剥夺的权利,其中包括生命权、自由权和追求幸福的权利。

+
+

演化的基础是差异,而不是平等。每个人身上带的基因码都有些许不同,而且从出生以后就接受着不同的环境影响,发展出不同的特质,导致不同的生存概率。“生而平等”其实该是“演化各有不同”。

+
+

个体诞生的背后就只是盲目的演化过程,而没有任何目的。所以“造物者赋予”其实就只是“出生”。

+
+

“自由”就像是“平等”、“权利”和“有限公司”,不过是人类发明的概念,也只存在于人类的想象之中。

+
+

我们相信某种秩序,并非因为它是客观的现实,而是因为相信它可以让人提升合作效率、打造更美好的社会。

+
+

伏尔泰就曾说:“世界上本来就没有神,但可别告诉我的仆人,免得他半夜偷偷把我宰了。”

+
+

如果不是大多数中国人都相信仁义礼智信,儒家思想绝不可能持续了两千多年。如果不是大多数的美国总统和国会议员都相信人权,美国的民主也不可能持续了250年。如果不是广大的投资人和银行家都相信资本主义,现代经济体系连一天也不可能继续存在。

+
+

而要怎样才能让人相信这些秩序?

+
    +
  • 第一,对外的说法绝对要坚持它们千真万确
  • +
  • 第二,在教育上也要彻底贯彻同一套原则。
  • +
+
+

有三大原因,让人类不会发现组织自己生活的种种秩序其实是想象:

+
    +
  1. 想象建构的秩序深深与真实的世界结合。
  2. +
  3. 想象建构的秩序塑造了我们的欲望。
  4. +
  5. 想象建构的秩序存在于人和人之间思想的连接。
  6. +
+
+

旅游业真正卖的可不是机票和饭店房间,而是旅游中的经验。

+
+

想象建构的秩序并非个人主观的想象,而是存在于主体之间(inter-subjective),存在于千千万万人共同的想象之中。

+
+

“客观”、“主观”和“主体间”的不同:

+
    +
  • “客观”事物的存在,不受人类意识及信念影响。
  • +
  • “主观”事物的存在,靠的是某个单一个人的意识和信念
  • +
+
+

“主体间”事物的存在,靠的是许多个人主观意识之间的连接网络。

+
+

为了改变现有由想象建构出的秩序,就得先用想象建构出另一套秩序才行。

+
+

身为人类,我们不可能脱离想象所建构出的秩序。

+
+

智人的社会秩序是通过想象建构,维持秩序所需的关键信息无法单纯靠DNA复制就传给后代,需要通过各种努力,才能维持种种法律、习俗、程序、礼仪,否则社会秩序很快就会崩溃。

+
+

人类的大脑并不是个很好的储存设备,主要原因有三。

+
    +
  • 第一,大脑的容量有限。
  • +
  • 第二,人类总难免一死,而大脑也随之死亡。
  • +
  • 第三,也是最重要的一点,在于人类的大脑经过演化,只习惯储存和处理特定类型的信息
  • +
+
+

演化压力让人类的大脑善于储存大量关于动植物、地形和社会的信息。

+
+

然而在农业革命之后,社会开始变得格外复杂,另一种全新的信息类型也变得至关重要:数字。

+
+

虽然这些符号现在被称为“阿拉伯数字”,但其实是印度人发明的。

+
+

之所以现在我们会称“阿拉伯数字”,是因为阿拉伯人攻打印度时发现了这套实用的系统,再加以改良传到中东,进而传入欧洲。

+
+

文字是采用实体符号来储存信息的方式。

+
+

文字对人类历史所造成最重要的影响:它逐渐改变了人类思维和看待这个世界的方式。

+
+

人类创造出了由想象建构的秩序、发明了文字,以这两者补足我们基因中的不足。

+
+

历史的铁则告诉我们,每一种由想象建构出来的秩序,都绝不会承认自己出于想象和虚构,而会大谈自己是自然、必然的结果

+
+

根据著名的婆罗门教神话,诸神是以原人普罗沙(Purusa)的身体创造这个世界:他的眼睛化成太阳,他的大脑化成月亮,他的口化成了婆罗门(祭司),他的手化成了刹帝力(贵族、武士),他的大腿化成了吠舍(农民和商人等平民),而他的小腿则化成了首陀罗(仆人)。

+
+

国古代的《风俗通》也记载,女娲开天辟地的时候要造人,一开始用黄土仔细捏,但后来没有时间余力,便用绳子泡在泥里再拉起来,飞起的泥点也化成一个一个的人,于是“富贵者,黄土人;贫贱者,引绳人也”

+
+

这些阶级区别不过全都是人类想象的产品罢了。

+
+

就目前学者研究,还没有任何一个大型人类社会能真正免除歧视的情形。

+
+

人类要让社会有秩序的方法,就是会将成员分成各种想象出来的阶级

+
+

让某些人在法律上、政治上或社会上高人一等,从而规范了数百万人的关系。

+
+

有了阶级之后,陌生人不用浪费时间和精力真正了解彼此,也能知道该如何对待对方。

+
+

就算身处不同阶级的人发展出了完全一样的能力,因为他们面对的游戏规则不同,最后结果也可能天差地别。

+
+

这些阶级制度开始时多半只是因为历史上的偶发意外,但部分群体取得既得利益之后,世世代代不断加以延续改良,才形成现在的样子。

+
+

纵观历史,几乎所有社会都会以“污染”和“洁净”的概念来做出许多社会及政治上的区隔,而且各个统治阶级利用这些概念来维系其特权也是不遗余力。

+
+

非洲人在基因上的优势(免疫力)竟造成了他们在社会上的劣势:正因为他们比欧洲人更能适应热带气候,反让他们成了遭到欧洲主人蹂躏的奴隶

+
+

“黑人”成了一种印记,人们觉得他们天生就不可靠、懒惰,而且愚笨。

+
+

随着时间推移,这些偏见只会越来越深。正由于所有最好的工作都在白人手上,人们更容易相信黑人确实低人一等。

+
+

恶性循环:某个偶然历史事件,成了僵化的社会制度常规。

+
+

随着时间流逝,不公不义的歧视常常只是加剧而不是改善。富者越富,而贫者越贫。教育带来进一步的教育,而无知只会造成进一步的无知

+
+

不同的社会,想象出的阶级制度也就相当不同。

+
+

有某种阶级制度却是在所有已知的人类社会里都有着极高的重要性:性别的阶级。

+
+

现在人体的所有器官早在几亿年前就已经出现了原型,而现在所有器官都不只做着原型所做的事。

+
+

各种规定男人就该如何、女人就该怎样的法律、规范、权利和义务,反映的多半只是人类的想象,而不是生物天生的现实。

+
+

生物上,人类分为男性和女性。所谓男性(male),就是拥有一个X染色体和一个Y染色体,所谓女性(female)则是拥有两个X染色体。

+
+

要说某个人算不算“男人”(man)或“女人”(woman),讲的就是社会学而不是生物学的概念了。

+
+

强壮分了许多种,像是女人一般来说比男人更能抵抗饥饿、疾病和疲劳,而且也有许多女人能跑得比男人更快,挑得比男人更多。

+
+

人类历史显示,肌肉的力量和社会的权力还往往是呈反比。在大多数社会中,体力好的反而干的是下层的活。

+
+

在智人内部的权力链里,聪明才智及社交技巧也会比体力更重要。

+
+

常常军队的领导人从没当过一天兵,只因为他们是贵族、富人或受过教育,高级将领的荣耀也就落在他们头上。

+
+

战争可不是什么单纯的酒吧打架,需要非常复杂的组织、合作和安抚手段。真正胜利的关键,常常是能够同时安内攘外,并看穿他人思维(尤其是敌国的思维)。

+
+

父权制度其实并没有生物学上的基础,而只是基于毫无根据的虚构概念。

+
+

人类几乎从出生到死亡都被种种虚构的故事和概念围绕,让他们以特定的方式思考,以特定的标准行事,想要特定的东西,也遵守特定的规范。

+
+

虽然每种文化都有代表性的信仰、规范和价值,但会不断流动改变。只要环境或邻近的文化改变,文化就会有所改变及因应,文化内部也会自己形成一股改变的动力。

+
+

正如中世纪无法解决骑士精神和基督教的矛盾,现代社会也无法解决自由和平等的冲突。

+
+

就像两个不谐和音可以让音乐往前进,人类不同的想法、概念和价值观也能逼着我们思考、批评、重新评价。一切要求一致,反而让心灵呆滞。

+
+

一般认为认知失调是人类心理上的一种问题,但这其实是一项重要的特性,如果人真的无法同时拥有互相抵触的信念和价值观,很可能所有的文化都将无从建立,也无以为继。

+
+

几千年来,我们看到规模小而简单的各种文化逐渐融入较大、较复杂的文明中,于是世界上的大型文化数量逐渐减少,但规模及复杂程度远胜昨日。

+
+

合久必分只是一时,分久必合才是不变的大趋势。

+
+

世界上没有什么社会性动物会在意所属物种的整体权益。

+
+

钱让我们能够快速、方便地比较不同事物的价值(例如苹果、鞋子甚至离婚这件事),让我们能够轻松交换这些事物,也让我们容易累积财富。

+
+

“人人都想要”正是金钱最基本的特性。人人都想要钱,是因为其他人也都想要钱,所以有钱就几乎可以换到所有东西

+
+

理想的金钱类型不只能用来交换物品,还能用来累积财富。

+
+

正因为有了金钱概念,财富的转换、储存和运送都变得更容易也更便宜,后来才能发展出复杂的商业网络以及蓬勃的市场经济

+
+

不管是贝壳还是美元,它们的价值都只存在于我们共同的想象之中。

+
+

金钱并不是物质上的现实,而只是心理上的想象。所以,金钱的运作就是要把前者转变为后者。

+
+

金钱正是有史以来最普遍也最有效的互信系统。

+
+

真正要用的时候,白银和黄金只会做成首饰、皇冠以及各种象征地位的物品;换言之,都是在特定文化里社会地位高的人所拥有的奢侈品。它们的价值完全只是因为文化赋予而来。

+
+

大约在公元前640年,土耳其西部吕底亚(Lydia)王国的国王阿耶特斯(Alyattes)铸造出史上第一批硬币

+
+

硬币上的印记代表着某些政治权力,能够确保硬币的价值。

+
+

就算是在宗教上水火不容的基督徒和穆斯林,也可以在金钱制度上达成同样的信仰。原因就在于宗教信仰的重点是自己相信,但金钱信仰的重点是“别人相信”

+
+

所有人类创造的信念系统之中,只有金钱能够跨越几乎所有文化鸿沟,不会因为宗教、性别、种族、年龄或性取向而有所歧视。也多亏有了金钱制度,才让人就算互不相识、不清楚对方人品,也能携手合作。

+
+

金钱制度有两大原则:

+
    +
  1. 万物可换:钱就像是炼金术,可以让你把土地转为手下的忠诚,把正义转为健康,把暴力转为知识。
  2. +
  3. 万众相信:有了金钱作为媒介,任何两个人都能合作各种计划。
  4. +
+
+

金钱还有更黑暗的一面。虽然金钱能建立起陌生人之间共通的信任,但人们信任的不是人类、社群或某些神圣的价值观,而只是金钱本身以及背后那套没有人性的系统。

+
+

多数过去的文化,早晚都是遭到某些无情帝国军队的蹂躏,最后在历史上彻底遭到遗忘。

+
+

在21世纪,几乎所有人的祖先都曾经属于某个帝国。

+
+

帝国是一种政治秩序,有两项重要特征:

+
    +
  • 第一,帝国必须统治着许多不同的民族,各自拥有不同的文化认同和独立的领土。
  • +
  • 第二,帝国的特征是疆域可以灵活调整,而且可以几乎无限扩张。
  • +
+
+

这里要特别强调,帝国的定义就只在于文化多元性和疆界灵活性两项,至于起源、政府形式、领土范围或人口规模则并非重点。并不是一定要有军事征服才能有帝国。

+
+

帝国正是造成民族多样性大幅减少的主因之一。

+
+

帝国的标准配备,常常就包括战争、奴役、驱逐和种族屠杀。

+
+

帝国四处征服、掠夺财富之后,不只是拿来养活军队、兴建堡垒,同时也赞助了哲学、艺术、司法和公益。现在人类之所以有许多文化成就,常常背后靠的就是剥削战败者。

+
+

智人本能上就会将人类分成“我们”和“他们”。所谓的“我们”,有共同的语言、宗教和习俗,我们对彼此负责,但“他们”就不干我们的事。“

+
+

西方认为所谓公义的世界应该是由各个独立的民族国家组成,但古代中国的概念却正好相反,认为政治分裂的时代不仅动荡不安,而且公义不行。

+
+

至于现代许多的美国人,他们也认为美国必须负起道义责任,让第三世界国家同样享有民主和人权。

+
+

中国的帝国大计执行得更为成功彻底。中国地区原本有许许多多不同的族群和文化,全部统称为蛮族,但经过两千年之后,已经成功统合到中国文化,都成了中国的汉族(以公元前206年到公元220年的汉朝为名)。

+
+

现今的文化又有大多数都是帝国的遗绪。

+
+

历史就是无法简单分成好人和坏人两种。自己常常就是跟着走坏人的路。

+
+

但在金钱和帝国之外,宗教正是第三种让人类统一的力量

+
+

在历史上,宗教的重要性就在于让这些脆弱的架构有了超人类的合法性。

+
+

宗教是“一种人类规范及价值观的系统,建立在超人类的秩序之上”。

+
+

宗教认为世界有一种超人类的秩序,而且并非出于人类的想象或是协议。以这种超人类的秩序为基础,宗教会发展出它认为具有约束力的规范和价值观。

+
+

某个宗教如果想要将幅员广阔、族群各异的人群都收归旗下,就还必须具备另外两种特质。第一,它信奉的超人类秩序必须普世皆同,不论时空而永恒为真。第二,它还必须坚定地将这种信念传播给大众。换句话说,宗教必须同时具备“普世特质”和“推广特质”。

+
+

农业革命开始,宗教革命便随之而来。

+
+

农业革命最初的宗教意义,就是让动植物从与人类平等的生物,变成了人类的所有物。

+
+

很多古代神话其实就是一种法律契约,人类承诺要永远崇敬某些神灵,换取人类对其他动植物的控制权。

+
+

真正让多神论与一神论不同的观点,在于多神论认为主宰世界的最高权力不带有任何私心或偏见,因此对于人类各种世俗的欲望、担心和忧虑毫不在意。

+
+

二元论宗教信奉着善与恶这两种对立力量的存在。二元论与一神论不同之处在于,他们相信“恶”也是独立存在,既不是由代表“善”的神所创造,也不归神所掌管。二元论认为,整个宇宙就是这两股力量的战场,世间种种就是两方斗争的体现。

+
+

诺斯替教和摩尼教认为,善神创造了精神和灵魂,而恶神创造了物质和身体。根据这种观点,人就成了善的灵魂和恶的身体之间的战场。

+
+

基督徒大致上是信奉一神论的上帝,相信二元论的魔鬼,崇拜多神论的圣人,还相信泛神论的鬼魂。

+
+

像这样同时有着不同甚至矛盾的思想,而又结合各种不同来源的仪式和做法,宗教学上有一个特别的名称:综摄(syncretism)。很有可能,综摄才是全球最大的单一宗教。

+
+

佛陀的教诲一言以蔽之:痛苦来自欲望;要从痛苦中解脱,就要放下欲望;而要放下欲望,就必须训练心智,体验事物的本质。

+
+

自由人文主义追求的,是尽可能为个人争取更多自由;而社会人文主义追求的,则是让所有人都能平等。

+
+

纳粹并不是反人性。他们之所以同自由人文主义、人权和共产主义站在对立面,反而正是因为他们推崇人性,相信人类有巨大的潜力。

+
+

我们刚刚踏入第三个千禧年,演化人文主义的未来仍未可知。

+
+

越来越多科学家认为,决定人类行为的不是什么自由意志,而是荷尔蒙、基因和神经突触——我们和黑猩猩、狼和蚂蚁并无不同。

+
+

商业、帝国和全球性的宗教,最后终于将几乎每个智人都纳入了我们今天的全球世界。

+
+

历史的铁则就是:事后看来无可避免的事,在当时看来总是毫不明显。

+
+

混沌系统分成两级:

+
    +
  • 一级混沌指的是“不会因为预测而改变”。
  • +
  • 二级混沌系统,指的是“会受到预测的影响而改变”,因此就永远无法准确预测。
      +
    • 例如市场就属于二级混沌系统。
    • +
    • 历史是“二级”混沌系统
    • +
    • 政治也属于二级混沌系统
    • +
    +
  • +
+
+

究竟为什么要学历史?历史不像是物理学或经济学,目的不在于做出准确预测。我们之所以研究历史,不是为了要知道未来,而是要拓展视野,要了解现在的种种绝非“自然”,也并非无可避免。未来的可能性远超过我们的想象。

+
+

历史的选择绝不是为了人类的利益。

+
+

迷因学假设,就像是生物演化是基于“基因”这种有机信息单位的复制,文化演化则是基于“迷因”(meme)这种文化信息单位的复制。

+
+

模因,又译媒因、觅母、米姆、弥等。目前比较公认的定义是通过模仿在人与人之间传播的思想、行为或风格,通常是为了传达模因所代表的特定现象、主题或意义。

+
+
+

现代科学与先前的知识体系有三大不同之处:

+
    +
  1. 愿意承认自己的无知。
  2. +
  3. 以观察和数学为中心。
  4. +
  5. 取得新能力。
  6. +
+
+

科学革命并不是“知识的革命”,而是“无知的革命”。

+
+

对“知识”的考验,不在于究竟是否真实,而在于是否能让人类得到力量或权力。

+
+

许多的科学研究和科技发展,正是由军事所发起、资助及引导

+
+

就目前所知,火药的发明其实是一场意外,原本的目的是道士想炼出长生不老药来。

+
+

纵观历史,社会上有两种贫穷:

+
    +
  1. 社会性的贫穷,指的是某些人掌握了机会,却不愿意释出给他人;
  2. +
  3. 生物性的贫穷,指的是因为缺乏食物和住所,而使人的生存受到威胁。
  4. +
+
+

许多社会现在的问题是营养过剩,胖死比饿死的概率更高。

+
+

人类所有看来无法解决的问题里,有一项最为令人烦恼、有趣且重要:死亡。

+
+

当时最聪明的人才,想的是如何给死亡赋予意义,而不是逃避死亡。

+
+

人之所以会死,可不是什么神的旨意,而是因为各种技术问题,像是心脏病,像是癌症,像是感染。而每个技术问题,都可以找到技术性的解决方案。

+
+

现在所有最优秀的人才可不是浪费时间为死亡赋予意义,而是忙着研究各种与疾病及老化相关的生理、荷尔蒙和基因系统。

+
+

科学革命的一大计划目标,就是要给予人类永恒的生命。

+
+

唯一一个让死亡仍然占据核心的现代意识形态就是民族主义。在那些绝望到极点但又同时充满诗意的时刻,民族主义就会向人承诺,就算你牺牲了生命,但你会永远活在国家整体的永恒记忆里。只不过,这项承诺实在太虚无缥缈,恐怕大多数民族主义者也不知道这究竟说的是什么意思。

+
+

科学活动并不是处于某个更高的道德和精神层面,而是也像其他的文化活动一样,受到经济、政治和宗教利益的影响。

+
+

现代科学之所以能在过去500年间取得如同奇迹般的成果,有很大程度必须归功于政府、企业、基金会和私人捐助者愿意为此投入数十亿美元的经费。

+
+

科学研究之所以能得到经费,多半是因为有人认为这些研究有助于达到某些政治、经济或宗教的目的。

+
+

真正控制科学发展进度表的,也很少是科学家。

+
+

科学并无力决定自己的优先级,也无法决定如何使用其发现。

+
+

科学研究一定得和某些宗教或意识形态联手,才有蓬勃发展的可能。意识形态能够让研究所耗的成本合理化。

+
+

在过去500年间,科学、帝国和资本之间的回馈循环无疑正是推动历史演进的主要引擎。

+
+

虽然我们常常不愿意承认,但现在全球所有人的穿着、想法和品位几乎就都是欧洲人的穿着、想法和品位。

+
+

中国和波斯其实并不缺乏制作蒸汽机的科技(当时要照抄或是购买都完全不成问题),他们缺少的是西方的价值观、故事、司法系统和社会政治结构,这些在西方花了数个世纪才形成及成熟,就算想要照抄,也无法在一夕之间内化。

+
+

欧洲帝国主义之所以要前往遥远的彼岸,除了为了新领土,也是为了新知识。

+
+

在15、16世纪,欧洲人的世界地图开始出现大片空白。从这点可以看出科学心态的发展,以及欧洲帝国主义的动机。

+
+

误以为发现美洲的人是亚美利哥·韦斯普奇,因此为了向他致敬,这片大陆就被命名为“America”(美洲)。

+
+

绝大多数的大帝国向外侵略只着眼于邻近地区,之所以最后幅员广大,只是因为帝国不断向邻近地区扩张而已。

+
+

虽然偶尔会有某个雄心勃勃的统治者或冒险家,展开长途的征讨或探险,但通常都是顺着早已成形的帝国道路或商业路线。。

+
+

郑和下西洋得以证明,当时欧洲并未占有科技上的优势。真正让欧洲人胜出的,是他们无与伦比而又贪得无厌、不断希望探索和征服的野心。

+
+

所有的非欧洲政权中,第一个派出军事远征队前往美洲的是日本。

+
+

现代科学和现代帝国背后的动力都是一种不满足,觉得在远方一定还有什么重要的事物,等着他们去探索、去掌握。

+
+

科学能够从思想上让帝国合理化。

+
+

正因为帝国与科学密切合作,就让它们有了如此强大的力量,能让整个世界大为改观;也是因为如此,我们很难简单断言它们究竟是善是恶。正是帝国创造了我们所认识的世界,而且,其中还包含我们用以判断世界的意识形态。

+
+

最早的梵语母语民族是在大约3000年前、从中亚入侵印度,他们自称为“雅利亚”(Arya)。而最早的波斯语母语者则自称为“艾利亚”(Airiia)。

+
+

对今日许多精英分子而言,要比较判断不同人群的优劣,几乎讲的总是历史上的文化差异,而不再是种族上的生物差异。

+
+

不论是科学还是帝国,它们能够迅速崛起,背后都还潜藏着一股特别重要的力量:资本主义。

+
+

真正让银行(以及整个经济)得以存活甚至大发利市的,其实是我们对未来的信任。“信任”就是世上绝大多数金钱的唯一后盾。

+
+

人类发展出“信用”这种金钱概念,代表着目前还不存在、只存在于想象中的货品。

+
+

现代经济的奇妙循环:

+
    +
  • 正是这种信任创造了信贷;
  • +
  • 而信贷带来了实实在在的经济成长;
  • +
  • 正因为有成长,我们就更信任未来,也就愿意提供更多的信贷
  • +
+
+

民间企业的获利正是社会整体财富和繁荣的基础。

+
+

所谓的“资本主义”(Capitalism),认为“资本”(capital)与“财富”(wealth)有所不同。

+
    +
  • 资本指的是投入生产的各种金钱、物品和资源。
  • +
  • 财富指的则是那些埋在地下或是浪费在非生产性活动的金钱、物品和资源。
  • +
+
+

资本主义的影响范围逐渐超越了单纯的经济领域,现在它还成了一套伦理,告诉我们该有怎样的行为,该如何教育孩子,甚至该如何思考问题。

+
+

资本主义认为经济可以无穷无尽地发展下去,但这和我们日常生活观察到的宇宙现象完全背道而驰。

+
+

印钞票的是银行和政府,但最后埋单的是科学家。

+
+

欧洲人征服世界的过程中,所需资金来源从税收逐渐转为信贷,而且也逐渐改由资本家主导,一切的目标就是要让投资取得最高的报酬。

+
+

为了掌控哈德孙河这个重要商业通道,西印度公司在河口的一座小岛上开拓了一个殖民地,名为“新阿姆斯特丹”(New Amsterdam)。这个殖民地不断遭受美国原住民威胁,英国人也多次入侵,最后在1664年落入英国手中。英国人将这个城市改名“纽约”(New York,即“新约克”,约克为英国郡名)。当时西印度公司曾在殖民地筑起一道墙,用来抵御英国人和美国原住民,这道墙的位置现在成了世界上最著名的街道:华尔街(Wall Street,直译为“墙街”)。

+
+

在1717年,密西西比河下游河谷其实大约只有沼泽和鳄鱼,但密西西比公司却是撒着漫天大谎,把这个地方描述得金银遍地、无限商机。许多法国贵族、商人和城市里那些冷漠的中产阶级都信了这套谎言,于是密西西比公司股价一飞冲天。公司上市的股价是每股500里弗(livre)。1719年8月1日,股价涨到每股2750里弗。8月30日,股价已经飙升到每股4100里弗;9月4日升上每股5000里弗。等到12月2日,密西西比公司的股价每股超过10000里弗大关。当时,整个巴黎街头洋溢着一种幸福感。民众卖掉了自己所有的财产,借了大笔的金钱,只为了能够购买密西西比公司的股票。每个人都相信自己找到了快速致富的捷径。

+

密西西比泡沫可以说是史上最惨烈的一次金融崩溃。法国王室的金融体系一直没能真正走出这场重大的打击。

+
+

至于打下印度次大陆的,同样也不是英国官方,而是英国东印度公司的佣兵。这家公司的成就甚至比荷兰东印度公司更加辉煌

+

一直要到1858年,英国王室才将印度及英国东印度公司的军队收编国有

+
+

这世界上根本不可能有完全不受政治影响的市场。毕竟,经济最重要的资源就是“信任”,而信任这种东西总是得面对种种的坑蒙拐骗。光靠着市场本身,并无法避免诈欺、窃盗和暴力的行为。这些事得由政治系统下手,立法禁止欺诈,并用警察、法庭和监狱来执行法律。

+
+

论听来十分完美,但实际上却是漏洞百出。如果真的是完全自由的市场,没有国王或神职人员来监督,贪婪的资本家就能够通过垄断或串通来打击劳工

+
+

如果真的是完全自由的市场,没有国王或神职人员来监督,贪婪的资本家就能够通过垄断或串通来打击劳工。

+

这是自由市场资本主义美中不足之处。它无法保证利润会以公平的方式取得或是以公平的方式分配。

+
+

人类的历史从来不是洁白无邪,随着现代经济成长,全球各地还有无数的大小罪恶和灾难正在上演。

+

就像农业革命一样,所谓的现代经济成长也可能只是个巨大的骗局。虽然人类和全球经济看来都在继续成长,但更多的人却活在饥饿和困乏之中。

+
+

每次即将因为能源或原料短缺而使经济成长趋缓的时候,就会有资金投入科学研究,解决这项问题。这种做法屡屡奏效,有时候让人更有效利用现有资源,有时候找出了全新的能源和材料。

+
+

过去可能会有人认为,像这样大规模使用资源,很快就会耗尽所有能源和原料,很快只能靠着回收垃圾撑下去了。然而,实际状况却正好相反。在1700年,全球运输工具使用的原料多半是木材和铁,但今天我们却有各式各样的新材料任君挑选,像是塑料、橡胶、铝和钛,这一切我们的祖先都完全一无所知。另外,1700年的马车主要是由木匠和铁匠手工人力制作,但在现在的丰田车厂和波音公司工厂里,我们靠的是燃油引擎和核电厂来推动生产。类似的革命在几乎所有产业领域无处不在。我们将它称为“工业革命”

+
+

人类历史在过去一直是由两大周期来主导:植物的生长周期,以及太阳能的变化周期。

+
+

工业革命的核心,其实就是能源转换的革命。

+
+

我们能使用的能源其实无穷无尽。讲得更精确,唯一的限制只在于我们的无知。

+
+

在地心引力下将一颗小苹果抬升一米,所需的能量就是一焦耳;

+
+

工业革命最重要的一点,其实在于它就是第二次的农业革命。

+
+

正是因为农业释放出了数十亿的人力,由工厂和办公室吸纳,才开始像雪崩一样有各种新产品倾泻而出。

+
+

消费主义的美德就是消费更多的产品和服务,鼓励所有人应该善待自己、宠爱自己,就算因为过度消费而慢慢走上绝路,也是在所不惜。

+
+

购物已成为人类最喜爱的消遣,而且消费性产品也成了家人、朋友、配偶之间不可或缺的中介。各种宗教节日(例如圣诞节)都已经成了购物节。

+
+

肥胖这件事,可以说是消费主义的双重胜利。

+
    +
  • 一方面,如果大家吃得太少,就会导致经济萎缩,这可不妙;
  • +
  • 另一方面,大家吃多了之后,就得购买减肥产品,再次促进经济成长。
  • +
+
+

资本主义和消费主义的伦理可以说是一枚硬币的正反两面,将这两种秩序合而为一。

+
    +
  • 有钱人的最高指导原则是——“投资!”
  • +
  • 而我们这些其他人的最高指导原则则是——“购买!”
  • +
+
+

人类能用的资源其实不断增加,而且这个趋势很可能还会继续。

+
+

与中世纪农民和鞋匠相比,现代工业对太阳或季节可说是完全不在乎,更重视的是要追求精确和一致。

+
+

1847年,英国各家火车业者齐聚一堂,研拟同意统一协调所有火车时刻表,一概以格林尼治天文台的时间为准,而不再遵循利物浦、曼彻斯特、格拉斯哥或任何其他城市的当地时间。在火车业者开了头之后,越来越多机构跟进这股风潮。最后在1880年,英国政府迈出了前所未有的一步,立法规定全英国的时刻表都必须以格林尼治时间为准。

+
+

直到现在,新闻广播开头的第一条仍然是现在时间,就算战争爆发也得放在后面再报。

+
+

一般人每天会看上几十次时间,原因就在于现代似乎一切都得按时完成。

+
+

很多时候,王国和帝国就像是收着保护费的黑道集团。国王就是黑道大哥,收了保护费就得罩着自己的人民,不受附近其他黑道集团或当地小混混骚扰。除此之外,其实也没什么功用。

+
+

年轻人越来越不需要听从长辈的意见,而一旦孩子的人生出了任何问题,似乎看来总是可以怪在父母头上。

+
+

现代所兴起的两大想象社群,就是“民族”和“消费大众”。

+
    +
  • 所谓民族,是国家的想象社群。
  • +
  • 而所谓消费大众,则是市场的想象社群。
  • +
+
+

消费主义和民族主义可说是夙夜匪懈,努力说服我们自己和其他数百万人是一伙的,认为我们有共同的过去、共同的利益以及共同的未来。

+
+

民族竭尽全力,希望能掩盖自己属于想象的这件事。大多数民族都会声称自己的形成是自然而然、天长地久,说自己是在最初的原生时代,由这片祖国土地和人民的鲜血紧密结合而成。但这通常就是个夸大其词的说法。虽然民族确实有悠久的源头,但因为早期“国家”的角色并不那么重要,所以民族的概念也无关痛痒。

+
+

现有的民族多半是到了工业革命后才出现。

+
+

狄更斯写到法国大革命,就说“这是最好的年代,也是最坏的年代”

+
+

虽然可能会有某些小规模边界冲突,但现在除非发生了某个世界末日等级的事件,否则几乎不可能再次爆发传统的全面战争。

+
+

如果说有个最高诺贝尔和平奖,应该把奖颁给罗伯特·奥本海默以及和他一起研发出原子弹的同事。有了核武器之后,超级大国之间如果再开战,无异等于集体自杀。

+
+

现在有四大因素形成了一个良性循环。

+
    +
  • 核子末日的威胁促进了和平主义;
  • +
  • 和平主义大行其道,于是战争退散、贸易兴旺;
  • +
  • 贸易成长,也就让和平的利润更高,而战争的成本也更高。
  • +
+
+

现在正面临着全球帝国的形成。而这个帝国与之前的帝国也十分类似,会努力维持其疆域内的和平。正因为全球帝国的疆域就是全世界,所以世界和平也就能得到有效的维持。

+
+

就算是都市中产阶级,过着舒适的生活,生活中却再也没有什么比得上狩猎采集者猎到长毛象那种兴奋和纯粹的快乐。

+
+

然智人确实取得了空前的成就,或许值得沾沾自喜,但代价就是赔上几乎所有其他动物的命运。

+
+

金钱确实会带来快乐,但是有一定限度,超过限度之后的效果就不那么明显。

+
+

另一项有趣的发现是疾病会短期降低人的幸福感,但除非病情不断恶化,或是症状带有持续、让人无力的疼痛,否则疾病并不会造成长期的不快。

+
+

对快乐与否的影响,家庭和社群要比金钱和健康来得重要。

+
+

多项重复研究发现,婚姻美好与感觉快乐,以及婚姻不协调与感觉痛苦,分别都呈现高度相关。

+
+

快乐并不在于任何像是财富、健康甚至社群之类的客观条件,而在于客观条件和主观期望之间是否相符。

+
+

重要的是要知足,而不是一直想要得到更多。

+
+

在我们试着猜测或想象其他人有多快乐的时候(可能是现在或过去的人),我们总是想要设身处地去想想自己在那个情况下会如何感受。但这么一来,我们是把自己的期望放到了别人的物质条件上,结果当然就会失准。

+
+

如果说快乐要由期望来决定,那么我们社会的两大支柱(大众媒体和广告业)很有可能正在不知不觉地让全球越来越不开心。

+
+

有没有可能,第三世界国家之所以会对生活不满,不只是因为贫穷、疾病、腐败和政治压迫,也是因为他们看到了第一世界国家的生活标准?

+
+

纵观历史,穷人和受压迫者之所以还能自我安慰,就是因为死亡是唯一完全公平的事。

+
+

人类演化的结果,就是不会太快乐,也不会太痛苦。我们会短暂感受到快感,但不会永远持续。迟早快感会消退,让我们再次感受到痛苦。

+
+

演化就把快感当成奖赏,鼓励男性和女性发生性行为、将自己的基因传下去。如果性交没有高潮,大概很多男性就不会那么热衷。但同时,演化也确保高潮得迅速退去。如果性高潮永续不退,可以想象男性会非常开心,但连觅食的动力都没了,最后死于饥饿,而且也不会有兴趣再去找下一位能够繁衍后代的女性。

+
+

人类的生化机制就像是个恒温空调系统。

+
+

已婚的人比单身和离婚的人更快乐,但这不一定代表是婚姻带来了快乐,也有可能是快乐带来了婚姻。

+
+

那些生化机制天生开朗的人,一般来说都会是快乐和满足的。而这样的人会是比较理想的另一半,所以他们结婚的概率也比较高。

+
+

快乐不只是“愉快的时刻多于痛苦的时刻”这么简单。相反,快乐要看的是某人生命的整体;生命整体有意义、有价值,就能得到快乐。

+
+

只要有了活下去的理由,几乎什么都能够忍受。

+
+

从我们所知的纯粹科学角度来看,人类的生命本来就完全没有意义。人类只是在没有特定目标的演化过程中,盲目产生的结果。

+
+

我们对生活所赋予的任何意义,其实都只是错觉。

+
+

所谓的快乐,很可能只是让个人对意义的错觉和现行的集体错觉达成同步而已。只要我自己的想法能和身边的人的想法达成一致,我就能说服自己、觉得自己的生命有意义,而且也能从这个信念中得到快乐。

+
+

奉若圭臬:比喻把某些言论或事当成自己的准则。

+
+

自由主义政治的基本想法,是认为选民个人最知道好坏,我们没有必要由政府老大哥来告诉人民何者为善、何者为恶。

+
+

佛教认为,快乐既不是主观感受到愉悦,也不是主观觉得生命有意义,反而是在于放下追求主观感受这件事。

+
+

人想要离苦得乐,就必须了解自己所有的主观感受都只是一瞬间的波动,而且别再追求某种感受。

+
+

苦真正的来源不在于感受本身,而是对感受的不断追求。

+
+

在所有目前进行的研究当中,最革命性的就是要建构一个直接的大脑–计算机双向接口,让计算机能够读取人脑的电子信号,并且同时输回人脑能够了解的电子信号

+
+

我们这个现代晚期的世界,是有史以来第一次认为所有人类应享有基本上的平等,然而我们可能正准备要打造出一个最不平等的社会。

+
+

我们真正应该认真以对的,是在于下一段历史改变不仅是关于科技和组织的改变,更是人类意识与身份认同的根本改变。

+
+

拥有神的能力,但是不负责任、贪得无厌,而且连想要什么都不知道。天下危险,莫此为甚。

+
+ +
+ + + + + + + +
+ + + + + + +
+ +
+ + + +
+ + + + + + + + +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2021/HackersPainters/index.html b/2021/HackersPainters/index.html new file mode 100644 index 0000000000..8fd942d033 --- /dev/null +++ b/2021/HackersPainters/index.html @@ -0,0 +1,743 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 《黑客与画家》摘抄 | 贾攀的流水账 + + + + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

贾攀的流水账

+ +
+

Panmax's Blog

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+ + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ 《黑客与画家》摘抄 +

+ + +
+ + + + +
+ + +

译者序

自由软件基金会创始人理查德·斯托尔曼说:“出于兴趣而解决某个难题,不管它有没有用,这就是黑客。”

+

黑客的六条价值观:

+
    +
  1. 使用计算机以及所有有助于了解这个世界本质的事物都不应受到任何限制。任何事情都应该亲手尝试。
  2. +
  3. 信息应该全部免费。
  4. +
  5. 不信任权威,提倡去中心化。
  6. +
  7. 判断一名黑客的水平应该看他的技术能力,而不是看他的学历、年龄或地位等其他标准。
  8. +
  9. 你可以用计算机创造美和艺术。
  10. +
  11. 计算机使生活更美好。
  12. +
+

前言

人们区分程序员甚至不是看他们写了什么程序,而是看他们使用什么语言。

+

1. 为什么书呆子不受欢迎

受父母的影响,书呆子被教导追求正确答案,而受欢迎的小孩被教导讨人喜欢。

+

青少年在心理上还没有摆脱儿童状态,许多人都会残忍地对待他人。他们折磨书呆子的原因就像拔掉一条蜘蛛腿一样,觉得很好玩。在一个人产生良知之前,折磨就是一种娱乐。

+

在任何社会等级制度中,那些对自己没自信的人就会通过虐待他们眼中的下等人来突显自己的身份。我已经意识到,正是因为这个原因,在美国社会中底层白人是对待黑人最残酷的群体。

+

不受欢迎是一种传染病,虽然善良的孩子不会去欺负书呆子,但是为了保护自己,也依然会与书呆子保持距离。难怪聪明的小孩读中学时往往是不快乐的。

+

公立学校的老师很像监狱的狱卒。看管监狱的人主要关心的是犯人都待在自己应该待的位置。然后,让犯人有东西吃,尽可能不要发生斗殴和伤害事件,这就可以了。除此以外,他们一件事也不愿多管,没必要自找麻烦。所以,他们就听任犯人内部形成各种各样的小集团。

+

真实世界的关键并非在于它是由成年人组成的,而在于它的庞大规模使得你做的每件事都能产生真正意义上的效果。

+

表面上,学校的使命是教育儿童。事实上,学校的真正目的是把儿童都关在同一个地方,以便大人们白天可以腾出手来把事情做完。

+

人类喜欢工作,在世界上大多数地方,你的工作就是你的身份证明。

+

以前的青少年似乎也更尊敬成年人,因为成年人都是看得见的专家,会传授他们所要学习的技能。如今的大多数青少年,对他们的家长在遥远的办公室所从事的工作几乎一无所知。他们看不到学校作业与未来走上社会后从事的工作有何联系。

+

校园生活的两大恐怖之处——残忍和无聊

+

校园生活的真正问题是空虚。

+

2. 黑客与画家

黑客与画家的共同之处,在于他们都是创作者。与作曲家、建筑师、作家一样,黑客和画家都是试图创作出优秀的作品

+

黑客的最髙境界是创造规格。

+

创造优美事物的方式往往不是从头做起,而是在现有成果的基础上做一些小小的调整,或者将已有的观点用比较新的方式组合起来。

+

黑客搞懂“计算理论”(theory of computation)的必要性,与画家搞懂颜料化学成分的必要性差不多大。

+

大学里教给我的编程方法都是错的。你把整个程序想清楚的时间点,应该是在编写代码的同时,而不是在编写代码之前,这与作家、画家和建筑师的做法完全一样。

+

编程语言首要的特性应该是允许动态扩展(malleable)。编程语言是用来帮助思考程序的,而不是用来表达你已经想好的程序。它应该是一支铅笔,而不是一支钢笔。

+

大学和实验室强迫黑客成为科学家,企业强迫黑客成为工程师。

+

程序员被当作技工,职责就是将产品经理的“构想”(如果这个词是这么用的话)翻译成代码……。大公司这样安排的原因是为了减少结果的标准差……。但是当你排斥差异的时候,你不仅将失败的可能性排除在外,也将获得高利润的可能性排除在外。

+

开发优秀软件的方法之一就是自己创业。

+

自己创业的两个问题:

+
    +
  • 一个是自己开公司的话,必须处理许许多多与开发软件完全无关的事情。
  • +
  • 另一个问题是赚钱的软件往往不是好玩的软件,两者的重叠度不髙。
  • +
+

如果你想赚钱,你可能不得不去干那些很麻烦很讨厌的事情,因为这些事情没人愿意义务来干。

+

我们面试程序员的时候,主要关注的事情就是业余时间他们写了什么软件。因为如果你不爱一件事,你不可能把它做得真正优秀,要是你很热爱编程,你就不可避免地会开发你自己的项目。

+

黑客的出发点是原创,最终得到一个优美的结果;而科学家的出发点是别人优美的结果,最终得到原创性。

+

你不能盼望先有一个完美的规格设计,然后再动手编程,这样想是不现实的。如果你预先承认规格设计是不完美的,在编程的时候,就可以根据需要当场修改规格,最终会有一个更好的结果。

+

最容易修改的语言就是简短的语言。

+

坚持一丝不苟,就能取得优秀的成果。因为那些看不见的细节累加起来,就变得可见了。

+

需要合作,但是不要“合”得过头……。正确的合作方法是将项目分割成严格定义的模块,每一个模块由一个人明确负责。模块与模块之间的接口经过精心设计,如果可能的话,最好把文档说明写得像编程语言规范那样清晰。

+

通黑客与优秀黑客的所有区别之中,会不会“换位思考”可能是最重要的单个因素。

+

判断一个人是否具备“换位思考”的能力有一个好方法,那就是看他怎样向没有技术背景的人解释技术问题。

+

如果我只能让别人记住一句关于编程的名言,那么这句名言就是《计算机程序的结构与解释》一书的卷首语:程序写出来是给人看的,附带能在机器上运行。

+

3. 不能说的话

所谓“流行”(传统观念也是一种流行),本质上就是自己看不见自己的样子。

+

历史的常态似乎就是,任何一个年代的人们,都会对一些荒谬的东西深信不疑。

+

最令人暴跳如雷的言论,就是被认为说出了真相的言论。

+

最先从你头脑中跳出来的想法,往往就是最困扰你、很可能为真的想法。你已经注意到它们,但还没有认真思考过。

+

如果某个观点在大部分时空都是不受禁止的,只有我们这个社会才把它当作禁忌,那么很可能是我们出错了。

+

孩子眼里的世界是不真实的,是一个被灌输进他们头脑的假想世界。将来当孩子长大以后接触社会,就会发现小时候以为真实的事情,在现实世界中是荒唐可笑的。

+

找出不能说的话的四种方式:

+
    +
  1. 判断言论的真伪
  2. +
  3. 关注“异端邪说”
  4. +
  5. 回顾过去
  6. +
  7. 寻找那些一本正经的卫道者,看看他们到底在捍卫者什么
  8. +
+

流行的时尚产生于某个有影响力的人物,他突发奇想,接着其他人纷纷模仿。

+

但是,流行的道德观念不是这样,它们往往不是偶然产生的,而是被刻意创造出来的。如果有些观点我们不能说出口,原因很可能是某些团体不允许我们说。

+

如果一个团体强大到无比自信,它根本不会在乎别人的抨击。美国人或者英国人对外国媒体的诋毁就毫不在意。

+

大多数的斗争,不管它们实际上争的是什么,都会以思想斗争的形式表现出来。

+

优秀作品往往来自于其他人忽视的想法,而最被忽视的想法就是那些被禁止的思想观点。

+

智力越高的人,越愿意去思考那些惊世骇俗的思想观点。这不仅仅因为聪明人本身很积极地寻找传统观念的漏洞,还因为传统观念对他们的束缚力很小,很容易摆脱。从他们的衣着上你就可以看出这一点:不受传统观念束缚的人,往往也不会穿流行的衣服

+

做一个异端是有回报的,不仅是在科学领域,在任何有竞争的地方,只要你能看到别人看不到或不敢看的东西,你就有很大的优势。

+

训练自己去想那些不能想的事情,你获得的好处会超过所得到的想法本身。

+

与笨蛋辩论,你也会变成笨蛋。

+

自由思考比畅所欲言更重要……。在思想和言论之间划一条明确的界线。在心里无所不想,但是不一定要说出来……。你的思想是一个地下组织,绝不要把那里发生的事情一股脑说给外人听。

+

讨论一个观点会产生更多的观点,不讨论就什么观点也没有。

+

如果可能的话,你最好找一些信得过的知己,只与他们畅所欲言、无所不谈。这样不仅可以获得新观点,还可以用来选择朋友。能够一起谈论“异端邪说”并且不会因此气急败坏的人,就是你最应该认识的朋友。

+

人们喜欢讨论的许多问题实际上都是很复杂的,马上说出你的想法对你并没有什么好处。

+

如果你的思想很保守,你自己不会知道,而且你很可能还会持有相反的看法。

+

如果一个命题不是错的,却被加上各种标签,进行压制和批判,那就有问题。

+

4. 良好的坏习惯

在程序员眼里,“黑客”指的是优秀程序员……。对于程序员来说,“黑客”这个词的字面意思主要就是“精通”,也就是他可以随心所欲地支配计算机。

+

警方总是从犯罪动机开始调查。常见的犯罪动机不外乎毒品、金钱、性、仇恨等。满足智力上的好奇心并不在FBI的犯罪动机清单之上。

+

在计算机工业的历史上,新技术往往是由外部人员开发的,而且所占的比例可能要高于内部人员。

+

一个人们拥有言论自由和行动自由的社会,往往最有可能采纳最优方案,而不是采纳最有权势的人提出的方案。专制国家会变成腐败国家,腐败国家会变成贫穷国家,贫穷国家会变成弱小国家。

+

5. 另一条路

“你的电脑”这个概念正慢慢成为过去时,取而代之的是“你的数据”。

+

如果用户自己的硬盘坏了,他们不会发狂,因为不能去责怪别人;如果一家公司丢失了他们的数据,他们会怀着超乎寻常的怒火,冲着这家公司发飙。

+

对于开发者来说,互联网软件与桌面软件最显著的区别就是,前者不是一个单独的代码块。它是许多不同种类程序的集合,而不是一个单独的巨大的二进制文件。设计桌面软件就像设计一幢大楼,而设计互联网软件就像设计一座城市:你不仅需要设计建筑物,还要设计道路、路标、公用设施、警察局、消防队,并且制定城市发展规划和紧急事件的应对方案。

+

不同的语言适合不同的任务,你应该根据不同场合,挑选最合适的工具。

+

这只是公关伎俩啦,我们知道媒体喜欢听到版本号。如果你发布一个大的版本更新(版本号的第一位数发生变动),它们就会以大篇幅报道。

+

互联网软件的另一个技术优势在于,你能再现大部分的bug。

+

人数越来越多,开会讨论各个部分如何协同工作所需的时间越来越长,无法预见的互相影响越多越大,产生的bug也越多越多。幸运的是,这个过程的逆向也成立:人数越来越少,软件开发的效率将指数式增长。

+
+

软件项目是交互关系复杂的工作,需要大量的沟通成本,人力的增加会使沟通成本急剧上升,反而无法达到缩短工期的目的。

+
+

互联网软件不仅把开发者与他的代码更紧密地联系在了一起,而且把开发者与他的用户也更紧密联系在了一起。

+

一定数量的盗版对软件公司是有好处的。不管你的软件定价多少,有些用户永远都不会购买。如果这样的用户使用盗版,你并没有任何损失。事实上,你反而赚到了,因为你的软件现在多了一个用户,市场影响力就更大了一些,而这个用户可能毕业以后就会出钱购买你的软件。

+

只要有可能,商业性公司就会采用一种叫做“价格歧视”(price discrimination)的定价方法,也就是针对不同的客户给出不同的报价,使得利润最大化。软件的定价特别适合采用价格歧视,因为软件的边际成本接近于零。

+
+

“边际成本”是一个经济学概念,指下一个单位产品的生产成本。软件的边际成本就是复制代码的成本,所以接近零。这意味着,对软件公司来说,增加一个用户几乎没有增加生产成本。它与价格歧视的关系在于,边际成本越低,厂商的定价空间就越大,它可以针对特定消费者定出很低的价格,从而达到扩大销售、利润最大化的目的。——译者注

+
+

如果某样商品购买起来很困难,人们就会改变主意,放弃购买。反过来也成立,如果某样东西易于购买,你就会多买一点。

+

大公司付出的高价之中,很大一部分是商家为了让大公司买下这个商品而付出的费用。

+

我预计微软会推出某种服务器和桌面电脑的混合产品,让它的桌面操作系统专门与由它控制的服务器协同工作。

+

桌面软件迫使用户变成系统管理员,互联网软件则是迫使程序员变成系统管理员:用户的压力变小了,程序员的压力变大了。

+

管理企业其实很简单,只要记住两点就可以了:做出用户喜欢的产品,保证开支小于收入。

+

如何做出用户喜欢的产品,下面是一些通用规则:

+
    +
  • 从制造简洁的产品开始着手,首先要保证你自己愿意使用。
  • +
  • 迅速地做出1.0版,并且不断以改进,整个过程中密切倾听用户的反馈。
  • +
+

如果竞争对手的产品很糟糕,你也不要自鸣得意。比较软件的标准应该是看对手的软件将来会有什么功能,而不是现在有什么功能。

+

6. 如何创造财富

如果你想致富,应该怎么做?我认为最好的办法就是自己创业,或者加入创业公司。

+

创业公司其实就是解决了某个技术难题的小公司。

+

创业公司不是变魔术。它们无法改变创造财富的法则,它们只是代表了财富创造曲线远端上的一点。这里有一个守恒定律:如果你想赚100万美元,就不得不忍受相当于100万美元的痛苦。

+

创造有价值的东西就是创造财富。我们需要的东西就是财富。

+

财富才是你的目标,金钱不是。……金钱是财富的一种简便的表达方式:金钱有点像流动的财富,两者往往可以互相转化。

+

金钱是专业化的副产品。

+

金钱就是交换中介,它必须数量稀少,并且便于携带。

+

大多数生意的目的是为了创造财富,做出人们真正需要的东西。

+

目前还存在的最大的手工艺人群体就是程序员。

+

公司不过是一群人在一起工作,共同做出某种人们需要的东西。真正重要的是做出人们需要的东西,而不是加入某个公司。

+

大公司会使得每个员工的贡献平均化,这是一个问题。我觉得,大公司最大的困扰就是无法准确测量每个员工的贡献。

+

你在工作上投入的精力越多,就越能产生规模效应。

+

要致富,你需要两样东西:可测量性可放大性

+

任何一个通过自身努力而致富的个人,在他们身上应该都能同时发现可测量性和可放大性。

+

如果你有一个令你感到安全的工作,你是不会致富的,因为没有危险,就几乎等于没有可放大性。

+

乔布斯曾经说过,创业的成败取决于最早加入公司的那十个人。

+

在不考虑其他因素的情况下,一个非常能干的人待在大公司里可能对他本人是一件很糟的事情,因为他的表现被其他不能干的人拖累了。

+

创业公司为每个人提供了一条途径,同时获得可测量性和可放大性。

+

如果你有一个新点子去找VC,问他是否投资,他首先就会问你几个问题,其中之一就是其他人复制你的模式是否很困难。也就是说,你为竞争对手设置的壁垒有多高。

+

大公司不害怕打官司,这对它们是家常便饭。它们很清楚,打官司的成本高昂又很费时。

+

如果你有两个选择,就选较难的那个。

+

真正创业以后,你的竞争对手决定了你到底要有多辛苦,而他们做出的决定都是一样的:你能吃多少苦,我们就能吃多少苦。

+

创业公司如同蚊子,往往只有两种结局,要么赢得一切,要么彻底消失。

+

一家大到有能力收购其他公司的公司必然也是一家大到变得很保守的公司,而这些公司内部负责收购的人又比其他人更保守,因为他们多半是从商学院毕业的,没有经历过公司的创业期。他们宁愿花大钱做更安全的选择,所以向他们出售一家已经成功的创业公司要比出售还处在早期阶段的创业公司更容易,即使会让他们付出多得多的价码。

+

大多数时候,促成买方掏钱的最好办法不是让买家看到有获利的可能,而是让他们感到失去机会的恐惧

+

你以为买家在收购前会做很多研究,搞清楚你的公司到底值多少钱,其实根本不是这么回事。他们真正在意的只是你拥有的用户数量。

+

你开办创业公司不是单纯地为了解决问题,而是为了解决那些用户关心的问题。

+

创造人们需要的东西,也就是创造财富。

+

为什么欧洲在历史上变得如此强大?……答案(或者至少是近因)可能就是欧洲人接受了一个威力巨大的新观点:允许赚到大钱的人保住自己的财富。

+

只要懂得藏富于民,国家就会变得强大。让书呆子保住他们的血汗钱,你就会无敌于天下。

+

7. 关注贫富分化

有三个原因使得我们对赚钱另眼相看。

+
    +
  • 第一,我们从小被误导的对财富的看法;
  • +
  • 第二,历史上积累财富的方式大多名声不好;
  • +
  • 第三,担心收入差距拉大将对社会产生不利影响。
  • +
+

财富与金钱是两个概念。金钱只是用来交易财富的一种手段,财富才是有价值的东西,我们购买的商品和服务都属于财富。

+

由于每个人创造财富的能力和欲望强烈程度都不一样,所以每个人创造财富的数量很不平等。

+

每个人的技能不同,导致收入不同,这才是贫富分化的主要原因,正如逻辑学的“奥卡姆剃刀”原则所说,简单的解释就是最好的解释。

+

如果说某种工作的报酬过低,那就相当于说人们的需求不正确。

+

封建社会只有两个阶级:贵族与农奴(为贵族服务的人)。中产阶级是一个新的第三类团体,他们出现在城镇中,以制造业和贸易为生。

+

创造财富真正取代掠夺和贪污成为致富的最佳方式,并不是发生在中世纪,而是发生在工业革命时代。

+

双重误解:对一个已经过时的情况持有错误的看法。

+
    +
  • 举例:由于人类历史上主要的致富方式长期以来都是偷窃,所以我们依然对有钱人抱有一种怀疑态度。理想主义的大学生从小受到历史上知名作家的影响,长大后不知不觉保留了孩提时对财富的看法。
  • +
+

技术应该会引起收入差距的扩大,但是似乎能缩小其他差距。一百年前,富人过着与普通人截然不同的生活。……由于技术的发展,富人的生活与普通人的差距缩小了。

+

技术无法使其变得更便宜的唯一东西,就是品牌。

+

只要存在对某种商品的需求,技术就会发挥作用,将这种商品的价格变得很低,从而可以大量销售。

+

无论在物质上,还是在社会地位上,技术好像都缩小了富人与穷人之间的差距,而不是让这种差距扩大了。

+

一个社会需要有富人,这主要不是因为你需要富人的支出创造就业机会,而是因为他们在致富过程做出的事情。……如果你让他致富,他就会造出一台拖拉机,使你不再需要使用马匹耕田了。

+

8. 防止垃圾邮件的一种方法

每个用户应该都分别有自己的概率分布表,这是根据他收到的邮件对每一个词进行统计后得出的。这样做可以:

+
    +
  1. 使得过滤器更有效;
  2. +
  3. 让每个用户自己定义,什么是他眼中的垃圾邮件;
  4. +
  5. 使得垃圾邮件的发送者无法针对过滤器做出调整(这可能是最大的好处)。
  6. +
+

9. 设计者的品味

人类的思想就是没有经过整理的无数杂念的混合。

+

优秀设计的原则:

+
    +
  • 好设计是简单的设计。……当你被迫把东西做得很简单时,你就被迫直接面对真正的问题。当你不能用表面的装饰交差时,你就不得不做好真正的本质部分。
  • +
  • 好设计是永不过时的设计。……如果解决方法是丑陋的,那就肯定还有更好的解决方法,只是还没有发现而已。……如果你希望自己的作品对未来的人们有吸引力,方法之一就是让你的作品对上几代人有吸引力。
  • +
  • 好设计是解决主要问题的设计。
  • +
  • 好设计是启发性的设计。……在软件业中,这条原则意味着,你应该为用户提供一些基本模块,使得他们可以随心所欲自由组合,就像玩乐高积木那样。
  • +
  • 好设计通常是有点趣味性的设计。……幽默感是强壮的一种表现,始终拥有幽默感就代表你对厄运一笑了之,而丧失幽默感则表示你被厄运深深伤到。
  • +
  • 好设计是艰苦的设计。……人们常常觉得野生动物非常优美,原因就是它们的生活非常艰苦,在外形上不可能有多余的部分了。
  • +
  • 好设计是看似容易的设计。……在大多数领域,看上去容易的事情,背后都需要大量的练习。练习的作用也许是训练你把刻意为之的事情变成一种自觉的行为。
  • +
  • 好设计是对称的设计。……对称有两种:重复性对称和递归性对称。……在软件中,能用递归解决的问题通常代表已经找到了最佳解法。
  • +
  • 好设计是模仿大自然的设计。……我能想象五十年后,小型的无人侦察飞机可以做得完全像鸟一样。
  • +
  • 好设计是一种再设计。……专家的做法是先完成一个早期原型,然后提出修改计划,最后把早期原型扔掉。……你应该培养对自己的不满。……犯错误是很正常的事情。你不要把犯错看成灾难,要勇于承认、勇于改正。……开源软件因为公开承认自己会有bug,反而使得代码的bug比较少。
  • +
  • 好设计是能够复制的设计。……把事情做对比原创更重要。
  • +
  • 好设计常常是奇特的设计。……唯一达到“奇特”的方法,就是追求做出好作品,完成之后再回过头看。
  • +
  • 好设计是成批出现的。……推动人才成批涌现的最大因素就是,让有天赋的人聚在一起,共同解决某个难题。互相激励比天赋更重要。
  • +
  • 好设计常常是大胆的设计。……今天的实验性错误就是明天的新理论。如果你想做出伟大的新成果,那就不能对常识与真理不相吻合之处视而不见,反而应该特别注意才对。……伟大成果的出现常常来源于某人看到一样东西后,心想我能做得比这更好。
  • +
+

10. 编程语言解析

编程语言的一个重要特点:一个操作所需的代码越多,就越难避免bug,也越难发现它们。

+

编译器处理的高级语言代码又叫做源码。它经过翻译以后产生的机器码就叫做目标码

+

开源软件就像一篇经受同行评议的论文。

+

程序员的时间要比计算机的时间昂贵得多,后者已经变得很便宜了,所以几乎不值得非常麻烦地用汇编语言开发软件。

+

如果你长期使用某种语言,你就会慢慢按照这种语言的思维模式进行思考。所以,后来当你遇到其他任何一种有重大差异的语言,即使那种语言本身并没有任何不对的地方,你也会觉得它极其难用。缺乏经验的程序员对于各种语言优缺点的判断经常被这种心态误导。

+

语言设计者之间的最大分歧也许就在于,有些人认为编程语言应该防止程序员干蠢事,另一些人则认为程序员应该可以用编程语言干一切他们想干的事。

+

事实上有两种程度的面向对象编程:某些语言允许你以这种风格编程,另一些语言则强迫你一定要这样编程。

+

允许你做某事的语言肯定不差于强迫你做某事的语言。所以,至少在这方面我们可以得到明确的结论:你应该使用允许你面向对象编程的语言。至于你最后到底用不用则是另外一个问题了。

+

11. 百年后的编程语言

无论何时,选择进化的主干可能都是最佳方案。

+

那些内核最小、最干净的编程语言才会存在于进化的主干上。

+

编程语言进化缓慢的原因在于它们并不是真正的技术。

+

随着技术的发展,每一代人都在做上一代人觉得很浪费的事情。

+

我觉得一些最好的软件就像论文一样,也就是说,当作者真正开始动手写这些软件的时候,他们其实不知道最后会写出什么结果。

+

浪费程序员的时间而不是浪费机器的时间才是真正的无效率。

+

另一种消耗硬件性能的方法就是,在应用软件与硬件之间设置很多的软件层。

+

除了某些特定的应用软件,一百年后,并行计算不会很流行。如果应用软件真的大量使用并行计算,这就属于过早优化了。

+

应用软件运行速度提升的关键在于有一个好的性能分析器帮助指导程序开发。

+

新语言更多地以开源项目的形式出现,而不是以研究性项目的形式出现。

+

新语言的设计者更多的是本身就需要使用它们的应用软件作者,而不是编译器作者。

+

设计新语言的方法之一就是直接写下你想写的程序,不管编译器是否存在,也不管有没有支持它的硬件。……随便什么,只要能让你最省力地写出来就行。

+

12. 拒绝平庸

真正非常严肃地把黑客作为人生目标的人,应该考虑学习Lisp。

+

Lisp没有得到广泛使用的原因就是因为编程语言不仅仅是技术,也是一种习惯性思维,非常难于改变。

+

程序员关心的那种强大也许很难正式定义,但是有一个办法可以解释,那就是有一些功能在一种语言中是内置的,但是在另一种语言中需要修改解释器才能做到,那么前者就比后者更强大。

+

编程语言的特点之一就是它会使得大多数使用它的人满足于现状,不想改用其他语言。……人类天性变化的速度大大慢于计算机硬件变化的速度,所以编程语言的发展通常比CPU的发展落后一二十年。

+

编程语言不一样,与其说它是技术,还不如说是程序员的思考模式。

+

13. 书呆子的复仇

各种编程语言的编程能力是不相同的。

+

编程语言现在的发展不过刚刚赶上1958年Lisp语言的水平。

+

使用一种不常见的语言会出现的问题我想到了三个:

+
    +
  1. 你的程序可能无法很好地与使用其他语言写的程序协同工作;
  2. +
  3. 你可能找不到很多函数库;
  4. +
  5. 你可能不容易雇到程序员
  6. +
+

把软件运行在服务器端就可以没有顾忌地使用最先进的技术。

+

到目前为止,大家公认少于10个人的团队最适合开发软件。

+

选择更强大的编程语言会减少所需要的开发人员数量。因为:

+
    +
  1. 如果你使用的语言很强大,可能会减少一些编程的工作量,也就不需要那么多黑客了;
  2. +
  3. 使用更高级语言的黑客可能比别的程序员更聪明
  4. +
+

如果你创业的话,千万不要为了取悦风险投资商或潜在并购方而设计你的产品。

+

衡量语言的编程能力的最简单方法可能就是看代码数量。……语言的编程能力越强大,写出来的程序就越短。

+

代码的数量很重要,因为开发一个程序所耗费的时间主要取决于程序的长度。

+

当团队规模超过某个门槛时,再增加人手只会带来净损失。

+

一种出色的工具到了真正优秀的黑客手里,可以发挥出更大的威力。

+

程序员使用某种语言能做到的事情是有极限的。

+

你的经理其实不关心公司是否真的能获得成功,他真正关心的是不承担决策失败的责任。所以对他个人来说,最安全的做法就是跟随大多数人的选择。……既然我选择的是“业界最佳实践”,如果不成功,项目失败了,那么你也无法指责我,因为做出选择的人不是我,而是整个“业界”。

+

编程语言的所谓“业界最佳实践”,实际上不会让你变成最佳,只会让你变得很平常。

+

如果你想在软件业获得成功,就使用你知道的最强大的语言,用它解决你知道的最难的问题,并且等待竞争对手的经理做出自甘平庸的选择。

+

14. 梦寐以求的编程语言

编程语言本来就是为了满足黑客的需要而产生的,当且仅当黑客喜欢一种语言时,这种语言才能成为合格的编程语言。

+

虽然语言的核心功能就像大海的深处,很少有变化,但是函数库和开发环境之类的东西就像大海的表面,一直在汹涌澎湃。

+

发展最早的20个用户的最好方法可能就是使用特洛伊木马:你让人们使用一种他们需要的应用程序,这个程序偏巧就是用某种新语言开发的。

+

一种语言必须是某一个流行的计算机系统的脚本语言(scripting language),才会变得流行。

+

黑客欣赏的一个特点就是简洁。

+

简洁性是静态类型语言的力所不及之处。……只要计算机可以自己推断出来的事情,都应该让计算机自己去推断。

+

语言设计者应该假定他们的目标用户是一个天才,会做出各种他们无法预知的举动,而不是假定目标用户是一个笨手笨脚的傻瓜,需要别人的保护才不会伤到自己。

+

对于制造工具的人来说,总是会有用户以违背你本意的方式使用你的工具。

+

所谓一次性程序,就是指为了完成某些很简单的临时性任务而在很短时间内写出来的程序。……开发大型程序的另一个方法就是从一次性程序开始,然后不断地改进。

+

未来50年中,编程语言的进步很大一部分与函数库有关。

+

函数库的设计基础与语言内核一样,都是一个小规模的正交运算符集合。函数库的使用应该符合程序员的直觉,让他可以猜得出哪个函数能满足自己的需要。

+

编程时提高代码运行速度的关键是使用好的性能分析器(profiler),而不是使用其他方法,比如精心选择一种静态类型的编程语言。

+

一种编程语言要想变得流行,最后一关就是要经受住时间的考验。……让别人相信一种新事物是需要时间的。

+

大多数人接触新事物时都学会了使用类似的过滤机制。甚至有时要听到别人提起十遍以上他们才会留意。这样做完全是合理的,因为大多数的热门新商品事后被证明都是浪费时间的噱头,没多久就消失得无影无踪

+

人们真正注意到你的时候,不是第一眼看到你站在那里,而是发现过了这么久你居然还在那里。

+

新技术被市场接纳的方式有两种,一种是自然成长式,另一种是大爆炸式。

+

最终来看,自然成长式会比大爆炸式产生更好的技术,能为创始人带来更多的财富。

+

著名散文家E.B.怀特说过,“最好的文字来自不停的修改”

+

设计一样东西,最重要的一点就是要经常“再设计”,编程尤其如此,再多的修改都不过分

+

为了写出优秀软件,你必须同时具备两种互相冲突的信念。一方面,你要像初生牛犊一样,对自己的能力信心万丈;另一方面,你又要像历经沧桑的老人一样,对自己的能力抱着怀疑态度。在你的大脑中,有一个声音说“千难万险只等闲”,还有一个声音却说“早岁哪知世事艰”。

+

你必须对解决难题的可能性保持乐观,同时对当前解法的合理性保持怀疑。

+

黑客心目中梦寐以求的语言:这种语言干净简练,具有最高层次的抽象和互动性,而且很容易装备,可以只用很少的代码就解决常见的问题

+

15. 设计与研究

设计与研究的区别看来就在于,前者追求“好”(good),后者追求“新”(new)。

+

艺术的各个领域有着巨大的差别,但是我觉得任何一个领域的最佳作品都不可能由对用户言听计从的人做出来。

+

让用户满意并不等于迎合用户的一切要求。用户不了解所有可能的选择,也经常弄错自己真正想要的东西。做一个好的设计师就像做一个好医生一样。你不能头痛医头,脚痛医脚。病人告诉你症状,你必须找出他生病的真正原因,然后针对病因进行治疗。

+

除非设定目标用户,否则一种设计的好坏根本无从谈起。

+

如果你正在设计某种新东西,就应该尽快拿出原型,听取用户的意见。

+

软件功能的增加并不必然带来质量的提高。

+ +
+ + + + + + + +
+ + + + + + +
+ +
+ + + +
+ + + + + + + + +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2021/Monkey-Business-excerpt/index.html b/2021/Monkey-Business-excerpt/index.html new file mode 100644 index 0000000000..aa48742ee7 --- /dev/null +++ b/2021/Monkey-Business-excerpt/index.html @@ -0,0 +1,553 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 《别让猴子跳回背上》摘抄 | 贾攀的流水账 + + + + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

贾攀的流水账

+ +
+

Panmax's Blog

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+ + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ 《别让猴子跳回背上》摘抄 +

+ + +
+ + + + +
+ + +

在遇到困难或问题时,员工总会寻找各种理由来证明不是自己的问题,然后将责任推到其他人或事上。道理也并不复杂,那就是人的本性中始终都在重复的一个永恒的主题:规避风险。

+

在我们的传统教育中,缺乏一种培养孩子独立承担和解决问题的意识。

+

追求成功的领导要视管理者能否有效掌控源源不绝的“背上猴子”(monkey-on-the-back)。

+

管理者的贡献来自于他们的判断力与影响力,而非他们所投入的个人时间与埋头苦干

+

管理者的绩效表现则是许多人群策群力的结果,这些人包括组织内部与外部的人,管理者惟有通过判断与影响才能加以控制。

+

管理者必须借着巧妙运用时间管理的内容与时机,尽可能增加自己的可支配时间(这些时间必须用于完成必要的判断)。

+

管理者的时间管理包括4大要素

+
    +
  • 老板占用的时间
  • +
  • 组织占用的时间
  • +
  • 自己占用的时间
  • +
  • 外界占用的时间
  • +
+

老板与组织所指派的任务有惩罚执法在背后撑腰,如何恰当地运用自己的时间便成了最主要的考虑因素。

+

“猴子”就是双方谈话结束后的下一个步骤。猴子不是问题、项目、计划或机会;猴子只是解决问题、进行项目计划或是投入机会的下一个步骤、下一个措施、下一个行动步骤。

+

项目是包含一个以上的阶段流程。

+

每一只猴子都会有两边的人马介入——一方负责解决,另一方则是监督。

+

展开下一个步骤的人就有猴子。

+

老板花钱聘请管理者,便是要他们负责确定正确的人在正确的时间完成正确的事情。

+

下属占用的时间从猴子成功地从下属的背上跳到主管背上那一刻展开,除非猴子能回到照顾喂养它的正确饲养人身上,否则下属将永远占用你的时间。

+

任何能控制下属占用时间的人,就能够增加他们的可支配时间,让他们能够处理优先的工作或私人事务。

+

接受这只猴子的同时,你也自甘成为下属的下属。

+

管理者会累积无数的猴子等待照顾——在原本就忙碌异常的上班时间,其原因出在他们一开始就不明了猴子是如何往上爬到他们的背上。

+

“不服从”是管理与被管理者(上司与下属)关系里面一个相当重要的因素:没有这套规则,你就失去了一个重要的依据。

+

用“我们”来开头,轻而易举地让你进入这种思考模式,认为这是大家共同的问题。换言之,他安排猴子一开始就踩着你们两个人的背往上爬——一只脚踩在你的背上,另一只脚则在他的背上。

+

有两种方法可让你避免去背负别人的猴子。一种方式是训练猴子不要抬错脚,但更好的方法是,一开始便不要让它们把脚放在你的背上。

+

当团队成员对你说:“领导,我们有问题。”此人犯了越俎代疱的错误。你的下属没有立场替所有团队成员发言而说出:“我们有问题’这样的话。

+

下属向管理者报告时,惟一的正确发言方式就是:“我有问题。”如果他说的是:“我们有问题。”那么,他就是越俎代庖。

+

无论问题是什么,下属永远是承接下一个步骤的那一方。

+

如果你的管理工作进度落后,你愈赶得上进度,反而会更加遥遥落后。

+

在决定猴子属于谁之前,根本就不要让猴子跳到你的背上。如果这是下属的猴子,那么,猴子就是他们的下一个步骤。

+

不要弄丢东西的不二法则便是不要归档——把猴子交给下属去归档。

+

任何时候,我帮你解决你的问题时,你的问题绝不能变成是我的问题。

+

我必须重申一遍,要求下属出现,要求他在会议上有工作成果报告,这是相当合理的要求。

+

先安排讨论时间是为了减少延误的可能性。下属会重视你要检查的东西,而不是你期待的每件事。

+

挫折才是真正要人命。工作过量从来不会要你的命。你绝不可能用工作过量来害死一个人。

+

在组织中,我们需要的是独当一面,而非事事依赖上司的员工,能够自动自发者——采取必要的行动完成任务。

+

如果你期待员工在一个相辅相成的团队中,能够独当一面,千万不要帮他们做他们分内的事。当下属前来寻求你的协助时,通常他们要的不是协助,他们找的是杀人武器上印有你的指纹。

+

你应该帮帮自己和下属的忙。下一次你给他们其他猴子时,先约定好两人开会讨论“事情进行得如何”的时间。这个日期不需要是任务或项目完成的日期。

+

这意味着下属应该到主管的办公室去喂食猴子;主管不应该自己去寻找濒临饿死边缘的猴子。这样会让下属紧张兮兮,在此情况下,下属通常无法全力以赴。

+

组织实务上的基本原则便是,资深管理者不该在未知会直属部下之前,便绕过下属直接对后者的下属宣布指示(明显的例外是,攸关生死的情况)。

+

你不可能以扛下下属责任的方式来教导员工尽责。不过,我还是要鼓励他尽责,同时采取他迫使我去做的下一个步骤。

+

一个基本的领导原则,即职责总是以时间为优先,而非准备就绪。

+

「我们没有出任何问题,如果我必须提出看法的话,我们从来没有出过问题;问题不是出在你身上,便是出在我身上,但绝对不是我们的问题。所以第一件事,我们应该先弄清楚代名词,看看这是谁的问题。如果是你的问题,我很乐意协助你处理。但如果问题出在我身上,我希望你也会协助我。但它不是我们的问题。现在问题是什么?」

+

不要用纸、电子邮件,它们无法传递或创造彼此之间的了解。对话是惟一能够增进双方了解的工具。

+

带着你的下属一起进行,通常这是一种很好的管理做法,这么做,猴子将不会乱窜。

+

猴子一旦迷途,它会冲动地往上爬。它只想跑到上面,而不是回家。

+

光对结果有所承诺,其成就不会比新年愿望高出多少,而且还会留下恶劣的记录。

+

强调目标而忽略行动的人,根本就是无视于因果关系的科学原则。他们认为目标等于原因,而结局就是结果。

+

对目标有所承诺,不见得可以有效产生圆满的结果。只有针对完成目标采取的行动,才会有效达成结果。

+

管理者要正确针对自己的目标提出承诺,包含未来预定完成的时间表。

+

陈述下一个步骤时,应该以可量化的语句来表达行动,这样执行必要措施时的模棱两可才会降低,而且表现才能改进。

+

建立对员工的信赖时,最大的障碍是来自于你恐惧员工可以独当一面。

+

尽可能在下属处理的范围内,给与对等的责任与行动自由。让他们独立工作,但在他们需要你协助喂食时,务必要在他们身边。

+

只要你将猴子交付给某人,务必要排好追踪的会议时间表。这可让你将不预期的干预降至最低,掌控每天的行程。这也是管理猴子和确定它们跟好主人的惟一方法。

+

喂养猴子的六大规则

+
    +
  1. 要么喂养它们,要么射杀它们,千万不要让它们被活活饿死。
  2. +
  3. 只要你找到需要喂养的猴子,你的下属就要找出时间喂养它们,但千万不要过量。
  4. +
  5. 按照喂食进度表上的时间和地点喂养猴子是下属的责任,主管不必再沿途追逐即将饿死的猴子,胡乱地喂食。
  6. +
  7. 如果有冲突发生,预定喂食猴子的时间可在任何一方的提议下做出更改,但不被视为延误;事情毫无进展不能作为重新安排喂食时间的借口。
  8. +
  9. 无论何时,应尽可能面对面地喂养猴子,或者使用电话,绝对不要使用信件。备忘录、电子邮件、传真和报告可以适用于喂食过程,但不能替代面对面的对话。
  10. +
  11. 超过好几页的备忘录、电子邮件、传真和报告应该在一页的摘要中写清楚,以便展开即时的对话。
  12. +
+ +
+ + + + + + + +
+ + + + + + +
+ +
+ + + +
+ + + + + + + + +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2021/When-Panic-Attacks/index.html b/2021/When-Panic-Attacks/index.html new file mode 100644 index 0000000000..916cbbf0a0 --- /dev/null +++ b/2021/When-Panic-Attacks/index.html @@ -0,0 +1,1042 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 《伯恩斯焦虑自助疗法》摘抄(未整理) | 贾攀的流水账 + + + + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

贾攀的流水账

+ +
+

Panmax's Blog

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+ + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ 《伯恩斯焦虑自助疗法》摘抄(未整理) +

+ + +
+ + + + +
+ + +

伯恩斯焦虑自助疗法

    +
  • 焦虑有许多不同的形式,

    +
  • +
  • 焦虑的成因,

    +
  • +
  • 每当我们感到焦虑或害怕的时候,其实都是我们自己在杞人忧天。

    +
  • +
  • 当你改变自己的思考方式的时候,你的感受也会随之改变。

    +
  • +
  • 简单来说,焦虑是由我们的想法,或者说是认知导致的。

    +
  • +
  • 该理论认为,每一种想法或认知都可以创造出一种特定的感受。

    +
  • +
  • 每当我们感到焦虑或害怕的时候,都是我们自己在杞人忧天。

    +
  • +
  • 当你开始感到焦虑的时候,你的消极想法和情绪开始相互作用,形成了一个不断加深的恶性循环。这些消极想法会导致焦虑和恐惧,而焦虑和恐惧又会让你的想法更加消极。

    +
  • +
  • 当你感到焦虑时,心中的很多想法都并非现实。

    +
  • +
  • 健康的焦虑情绪源自对实际存在的危险的感知,

    +
  • +
  • 神经性焦虑并不是由真实的威胁导致的。导致神经性焦虑的想法往往都是扭曲的、不合逻辑的。

    +
  • +
  • 如果法”(What-If Technique)治疗。

    +
  • +
  • 他也发现自己其实无限放大了自己在别人眼中的重要性,却忽略了身边的很多律师其实都是以自我为中心的“自恋狂”。

    +
  • +
  • 接受悖论(acceptance paradox)。

    +
  • +
  • “软弱”实际上是他最强的力量,而他引以为傲的“力量”恰恰一直是他最大的软肋。

    +
  • +
  • 试图隐藏的软弱、焦虑和对自己的怀疑恰恰是他和其他人连接的一根红线。

    +
  • +
  • 佛教教导我们痛苦不是来自现实,而是来自我们对现实的判断。

    +
  • +
  • 当你改变思维方式时,你就可以改变你的感受。

    +
  • +
  • 暴露模型

    +
  • +
  • 这种模型认为,当你焦虑时,你其实是在极力逃避你害怕的事情。

    +
  • +
  • 名为“洪水法”(Flooding)的治疗方法。在恐慌之时,我们不要刻意逃避,而是故意让自己暴露在害怕的事物面前,让自己充满焦虑。

    +
  • +
  • 向焦虑妥协,

    +
  • +
  • 情感隐藏模型认为“善良”(niceness)是焦虑的主要原因。

    +
  • +
  • 当你焦虑的时候,你几乎总是在刻意逃避困扰你的问题,但是你并没有意识到这一点。你之所以会把困扰自己的问题从意识中推出来,是因为你想要变得善良,不想让任何人因此感到沮丧或不安。

    +
  • +
  • 在这种模型之下,只要你将自己的情绪原原本本地展现出来,你的焦虑感自然会一扫而空。

    +
  • +
  • 了三种对抗焦虑的有效方式。认知法帮助我们识破那些让我们焦虑抑郁的消极想法。暴露法帮我们直面过去一直逃避的恐惧。情感隐藏法则帮助我们找到我们精心隐藏在内心深处的冲突或情感。

    +
  • +
  • 人,生而不同。

    +
  • +
  • 焦虑情绪源自对危险的感知。如果你一直告诉自己马上就会有不好的事情发生,你就会感到焦虑。

    +
  • +
  • 与杞人忧天的焦虑情绪不同,如果你感到抑郁,你会觉得悲剧已经发生了。

    +
  • +
  • 如果你感到抑郁,就一定会感到焦虑。如果你感到焦虑,你也许也会感到一丝抑郁。

    +
  • +
  • 抑郁会带来很大的痛苦,因为抑郁的情绪会剥夺你的自尊。而且,在抑郁的状态下,你也会更加容易觉得没有希望,会很容易认为自己的痛苦是永不休止的。

    +
  • +
  • 抑郁是这个世界上最古老、也是最残酷的骗子,因为你会骗自己去相信很多根本不真实的东西。抑郁情绪会让你认为自己很糟糕,认为自己应该做得更好,还会感觉自己从来不曾开心、满足,也不会拥有创造力,无法和他人建立亲密的关系。

    +
  • +
  • 很多神经病学家都认为,抑郁症和焦虑症的成因是大脑分泌的血清素不足,而躁狂症(极其欣快兴奋的状态)的成因则是大脑分泌的血清素过多

    +
  • +
  • 人脑和电脑的区别在于,人脑每天都会产生新的脑细胞和新的电信号回路。所以,每天早上我们醒来的时候,从字面意义上说,我们都是一个“全新的人”,因为我们的脑细胞在过去的24小时里已经全部更新一遍了。

    +
  • +
  • 医生们自己每天也不停地听到这种化学物质失衡理论。而这种理论的传播动力更多地是来自其背后的制药公司,

    +
  • +
  • 实际上,我们到现在为止,甚至都不知道大脑是怎么创造出意识的,更别提

    +
  • +
  • 我们对自己的期待有的时候会给我们的思维方式、感受以及行为方式带来意料之外的影响。

    +
  • +
  • 希望”是最有效的抗抑郁药。

    +
  • +
  • 你可以为抑郁

    +
  • +
  • 最近的研究证明,所有的处方类抗抑郁药其实除了安慰剂效应之外真的都没有其他的治疗效果。

    +
  • +
  • 抗抑郁药所谓的功效之中,大概有75%到80%都归功于安慰剂效应。

    +
  • +
  • 他们营销药物的需求和科学研究之间出现了矛盾。

    +
  • +
  • 认知行为疗法也是目前在美国实验最广的心理治疗方法,同时也是在临床治疗中使用最多的疗法。

    +
  • +
  • 于抑郁症和焦虑症的治疗来说,无论是从长期来看还是短期来看,认知行为疗法都比药物治疗更加有效。

    +
  • +
  • 都应该至少一周做一次“简明情绪量表”的测试(见本书第29页),以随时监测自己是否有康复的迹象。

    +
  • +
  • 从全球范围来看,焦虑和抑郁是两种最普通、最常见的心理健康问题,这两种情绪会让人觉得非常痛苦。

    +
  • +
  • 主动在生活中运用它们才行。 为此,你必须要做到以下三件事: 1. 你必须放弃焦虑和抑郁的某些隐藏的好处,这可能会给你造成损失。 2. 你必须敢于直面心中最大的恐惧,这需要你拥有极大的勇气和决心。 3. 你必须做一些笔头上的练习,这要求你必须脚踏实地地做出积极的努力。

    +
  • +
  • 列出所有让你抓狂的感受、想法或者习惯的优缺点。然后就这些列出来的优点和缺点做一个权衡,再来综合考虑到底要不要改变。

    +
  • +
  • 他认为焦虑感可以让他始终保持警惕,从而免受未来的其他潜在的伤害。这其实是所有焦虑患者都共有的一种想法。我把这称为“魔法思维”(magical thinking)。

    +
  • +
  • 真正能让你提高效率的焦虑情绪很少很少,更多情况下,焦虑会让你的效率越来越低。

    +
  • +
  • 如果你想要战胜你的焦虑,你就必须直面你心中的怪兽,战胜你心中最深处的恐惧。

    +
  • +
  • 并不是暴露本身让你不再恐惧,而是在暴露治疗的过程中出现了某一刻,这一刻你突然意识到,让你恐惧的想法其实并不是事实。

    +
  • +
  • “每日情绪日志”的基本理念就是:只要你改变了自己的想法,你就可以改变自己的感受。

    +
  • +
  • 填写“每日情绪日志”可以分为五个步骤: 第一步 写下令你难受或不安的事件。

    +
  • +
  • 第二步 圈出你的“情绪”。

    +
  • +
  • 第三步 记录消极想法。

    +
  • +
  • 表6-1 每日情绪日志

    +
  • +
  • 表6-2 认知扭曲对照表

    +
  • +
  • 在你感到焦虑和沮丧的瞬间就可能发现你的所有问题。当你开始改变自己的思考和感受的方式的时候,你就可以找到解决你所有问题的方法了。

    +
  • +
  • 正确识别出消极想法中的扭曲认知与其说是科学,不如说是一门艺术,所以即使没有全部勾对,也无须担心。

    +
  • +
  • 当你在你的想法中发现这些扭曲认知的时候,你得先想一想首先需要用到哪个方法。

    +
  • +
  • 双重标准法本身利用的就是人类在处理事情时的天性。当我们沮丧的时候,我们就会觉得心烦意乱,抓狂不已。但是当我们和有同样情绪问题的朋友聊天的时候,又会变得格外客观冷静,并且有同情心。

    +
  • +
  • 6-5 玛莎的每日情绪日志(二)

    +
  • +
  • 如果你希望这个积极想法完完全全地改变你发自内心的感受,它必须满足两个条件。

    +
  • +
  • 这个积极想法一定得百分百真实,

    +
  • +
  • 积极想法需要让消极想法不攻自破。

    +
  • +
  • “每日情绪日志”最大的好处就是它能够反映出你独特的想法和感受。

    +
  • +
  • 在本书的最后我还为你准备了另一份“每日情绪日志”的空白模板。你还可以复印更多来

    +
  • +
  • 第一步 写下令你难受的事件 在每日情感日志的顶部,简短地描述一件让你感到不安或难受的事。

    +
  • +
  • 你感到焦虑和沮丧的瞬间就可能会暴露你所有的问题。

    +
  • +
  • 你可以一次只解决一个问题。

    +
  • +
  • 人们宁愿只谈论自己生活中的问题,而不愿意写下来。

    +
  • +
  • 如果你真的想改变自己的生活,你迟早得关注你自己感到不安或难受的这个特殊时刻。

    +
  • +
  • 如果只是谈话而没有任何逻辑章法,反而会有可能无休止地拖延病情,也不会给病人带来任何真正的改变。

    +
  • +
  • 第二步 圈出消极情绪 在你描述完令你难受不安的事件后,在表内的情绪词汇中圈出能够准确描述你此刻情绪的词语,并且给情绪的强烈程度打分,

    +
  • +
  • 识别和评估你的消极情绪很重要,因为特定种类的感受是由某些特定的消极想法引起的。

    +
  • +
  • 第三步 识别消极想法 当你感到不安时,记录你脑中闪现出的所有消极想法。

    +
  • +
  • 在“每日情绪日志”的“消极想法”一栏中列出你的消极想法,并评估你对每个想法的相信程度,

    +
  • +
  • 记录消极想法的过程做一些小提示。

    +
  • +
  • 第四步 识别出想法中的认知扭曲 在“认知扭曲”一列中记录每个消极想法中的扭曲认知。

    +
  • +
  • 第五步 想出积极想法 思考出一些更积极更现实的想法,让你的消极想法不攻自破。

    +
  • +
  • 认知行为疗法认为焦虑、抑郁和愤怒都是由当下的消极想法导致的。

    +
  • +
  • 自我攻击信念(

    +
  • +
  • 你的态度和价值观可以解释你的心理脆弱性。

    +
  • +
  • 自我攻击信念有两种基本类型:个人自我挫败和人际自我挫败。

    +
  • +
  • 个人自我挫败常常与自尊相关,

    +
  • +
  • 人际自我攻击信念可能更容易导致与其他人之间的冲突。

    +
  • +
  • 自我攻击信念其实始终存在,但消极想法只有在你感到不安时才会浮现在你的脑海中。

    +
  • +
  • 从“每日情绪日志”中选择一个消极想法,并在其下方画一个向下的箭头“↓”。

    +
  • +
  • 7-2 拉希德的向下箭头法

    +
  • +
  • 常见的自我攻击信念

    +
  • +
  • 在使用向下箭头法时,我们可以从“每日情绪日志”的消极想法开始。你选择哪一个消极想法都行,选择一个你感兴趣的就好。在它下面画一个向下的箭头“↓”并问自己:“如果这是真的,那对我来说意味着什么?为什么这会让我难过?”这时,一个新的消极想法将浮现在你的脑海中,你可以在箭头下把这个想法写下来。

    +
  • +
  • 最终找到自己最深层的担忧。

    +
  • +
  • 我们应如何改变自己的自我攻击信念呢?我认为这个过程可以分为三步。

    +
  • +
  • 进行成本效益分析。

    +
  • +
  • 修正自己的想法。

    +
  • +
  • 测试新的想法。

    +
  • +
  • 表8-1 行为完美主义:成本效益分析

    +
  • +
  • 另一个人的爱永远不会让我有价值,他们的拒绝也永远不会让我变得毫无价值。

    +
  • +
  • 如果法就可以帮助你发现引发焦虑的可怕幻想。

    +
  • +
  • 你在“每日情绪日志”的消极想法下画一个向下的箭头“↓”,并问自己这样的问题:“如果这是真的怎么办?会发生什么呢?最坏的情况会是怎样的呢?我心里最害怕的到底是什么?”

    +
  • +
  • 表9-1 克里斯汀:如果法

    +
  • +
  • 自虐式解决方案就是指你认为只要你惩罚自己,你就可以惩罚别人。

    +
  • +
  • 当我们感到沮丧时,我们会毫不留情地批判自己,仿佛想要将自己撕成碎片。但是当我们和有同样情绪问题的朋友聊天的时候,又会变得格外客观冷静。

    +
  • +
  • 可以试着问问自己:如果我的亲人或者朋友和我有着同样的问题,我会对他们说什么?我会对他或她说这么严厉的话吗?

    +
  • +
  • 大多数时候,被别人拒绝的痛苦都是来自我们自己的想法,而不是拒绝本身。有时,这些想法是极度扭曲的,会给我们带来很大的伤害。

    +
  • +
  • 但根据我的经验,自责、内疚和缺陷感通常都不能激励人,也不能帮助我们从错误中吸取教训。

    +
  • +
  • 只有当我们感到快乐、放松和自我接纳时,我们才无所不能。

    +
  • +
  • 当你使用基于真相的治疗法时,就可以像科学家一样,通过实验来测试自己的消极想法,看看这些想法是不是真的有现实依据、这些依据是否真实有效。

    +
  • +
  • 检查证据法、实验法、调查法和重新归因法。

    +
  • +
  • 核心思想就是:真相使你自由。

    +
  • +
  • 当你的消极想法中包含“妄下结论”这种扭曲认知时,检查证据法就会特别有用。

    +
  • +
  • 妄下结论”的两种常见形式——臆测未来和读心。“臆测未来”是指你自己对未来进行了一些可怕的预测,而这些预测没有任何事实依据。

    +
  • +
  • “情绪化推论”是很容易产生误导的,因为你的感受来自你的想法,而不是现实。

    +
  • +
  • 当你使用实验法时,你需要做一个实际的实验来测试消极想法或自我攻击信念是否真实,就像科学家测试他们提出的假设理论一样。

    +
  • +
  • 实验法是有史以来为治疗焦虑而开发的最强有力的方法。

    +
  • +
  • 实验法可以帮助我们治疗抑郁和焦虑,但它最大的效用是用来治疗惊恐发作。

    +
  • +
  • 惊恐发作是我们对无害的身体反应的过度解读引起的。

    +
  • +
  • 过度呼吸会导致血液中的氧气增加,并产生轻微的头晕,手指也会觉得刺痛。

    +
  • +
  • 人之所以会晕倒,是因为心跳减慢并且血压下降。这时心脏不能将足够的血液和氧气输送到大脑。而晕倒恰恰是身体自身的一种“关机”防御机制。

    +
  • +
  • 认知疗法背后的理念:当你改变思考方式的时候,你就可以改变你的感受。

    +
  • +
  • 真正发疯的人会认为全世界都是疯子,而自己却不是疯子。

    +
  • +
  • 命运的安排和你没有任何关系,你并没有错。你的问题不在于你到底是不是一个负担,而是你一直在责怪自己,并且不断地告诫自己不可以成为大家的负担。

    +
  • +
  • 每一个人活在世上都有成为负担的时候,这也是我们生而为人的一个特征呀。”

    +
  • +
  • 问一问身边的人,并找出答案,而不是对其他人的想法和感受自顾自地进行假设。

    +
  • +
  • “重新归因”的目标不是使失败合理化,而是用一种更现实的角度来了解发生的每一件事。

    +
  • +
  • “非黑即白”的思维模式会引发表现焦虑,你会认为自己的表现必须非常完美,否则自己就是一无是处的。

    +
  • +
  • 情绪变化的必要和充分条件吗?必要条件是,这个积极想法必须是百分百真实的。充分条件是,你必须要能够让自己相信消极想法是个彻头彻尾的谎言。

    +
  • +
  • 准备和过程中的努力都在你的掌控之中,但结果往往并非如此。

    +
  • +
  • 即使整件事没有按照我预想的方向发展,但我的处理方式仍然是正确的,在这样复杂的情况下,我已经做得很好了。

    +
  • +
  • 当你使用语义法时,你只需用更友善和更温和的语言来代替你在感到不安时使用的那些带有极强感情色彩和伤害性的语言。

    +
  • +
  • 当你焦虑或沮丧的时候,你很可能对自己使用“你应该”“你必须”“你不得不”一类的句式。

    +
  • +
  • 指向自己的“应该”句式会引起抑郁、焦虑、自卑、内疚和羞耻的感觉。

    +
  • +
  • 而指向他人的“应该”句式则会导致怨恨与愤怒的情绪。

    +
  • +
  • 乱贴标签和“应该”句式往往相伴而来。而语义法就可以帮助我们扭转这两种想法。

    +
  • +
  • 当你将“应该”句式指向整个世界时,你会感到沮丧。

    +
  • +
  • “应该”句式是非常难以摆脱的,因为这种句式会让人上瘾,并让人感到一种道德优越感。

    +
  • +
  • 大多数情绪化的痛苦都源于我们对自己和他人的“应该”和绝对主义要求。

    +
  • +
  • 当你使用语义法时,你就需要在考虑自己的问题时,用较少感情色彩和情感负荷的语言来代替原先使用的那些伤害性语言。

    +
  • +
  • 在日常会话中,“应该”一词也是有实际用途的,比如说:道德意义上的“应该”,法律意义上的“应该”以及自然界中普遍法则意义上的“应该”。

    +
  • +
  • 导致情绪困扰的“应该”句式通常不属于这三类中的任何一类。

    +
  • +
  • 表现焦虑源于对失败的恐惧。

    +
  • +
  • 以偏概全”可能会让你产生焦虑和抑郁的情绪,因为你会觉得你的自尊和骄傲岌岌可危。

    +
  • +
  • 当你使用“具体法”时,你会坚持现实并避免对自己做出过于概括的判断。

    +
  • +
  • 可以将自己的目光聚焦于某种特定的优势或劣势上。

    +
  • +
  • 请始终记住,我们的痛苦不是来自现实,而是来自我们对现实的判断。而且,这些判断很多时候都是错觉。世间万事万物本无成败或强弱之分,有区别的只是我们脑中的想法罢了

    +
  • +
  • 当你为自己辩驳时,你会创造一种战争一触即发的紧张状态,这会让批评者更有冲动想要再次攻击你。

    +
  • +
  • 我们要把以偏概全的非针对性偏见具体化。

    +
  • +
  • 自我监控法真的非常简单。你所要做的就是数一数你全天所有的负面想法。

    +
  • +
  • 每次只要你有一个消极的想法,你就主动按下计数器边缘的按钮,表盘上的数字就会加1。

    +
  • +
  • 当你放弃强迫性的习惯时,你的焦虑几乎总会持续数天,这就有点像是戒毒时的戒断反应。但如果你坚持下去,你的强迫性冲动通常会消失。

    +
  • +
  • 如果你想尝试使用“自我监控法”,请记住,在沮丧的想法减少之前通常需要一段时间,因此你应该计划坚持至少三周。

    +
  • +
  • “放任担忧法”是矛盾治疗法当中的一种。使用这种方法的时候,我们不去攻击自己的消极想法,而是顺其自然,并屈服于它们。

    +
  • +
  • 你每天可以自己安排一个或多个时段来放任自己感到忧虑、沮丧或内疚。在这些时间段,你可以尽可能地用消极的想法折磨自己,让自己最大程度地感到沮丧。剩下的时间,你就可以专注于积极并且富有成效的方式过你的生活了。你可以使用这种方法来克服引发焦虑或抑郁的想法。

    +
  • +
  • 笑声可以表达出很多言语无法直接表达的东西。当你在笑的时候,其实意味着你不再那么把自己当回事,你发现了一直以来困扰着你的恐惧、担忧和自我怀疑竟然如此荒谬。实际上,笑声传达了自我接纳和接受他人的信息。

    +
  • +
  • 其实我们也有三种方法可以有意地利用幽默来建立与病人之间的纽带,这三种方法分别是:害羞暴露练习法、悖论放大法和幽默想象法。

    +
  • +
  • 在做完蠢事之后,你会发现大多数人都不会看不起你,世界也并没有因为你做了一次蠢事就走到了尽头。

    +
  • +
  • 我们并不总是如此刻板,也不需要总是把自己太当回事。很多人其实都不排斥善意的小幽默,有的时候甚至奇怪一些也无所谓。因为大部分人的生活都是很无趣的,所以人们总是想要寻找笑料,点亮生活。

    +
  • +
  • 你可以相信并夸大自己的消极想法,而不是一味地反驳它们。使用这种方法就要求我们放弃和消极想法一辩高下,而是尽可能地将消极想法夸大,越夸张越好。

    +
  • +
  • 将你的焦虑放在一个更幽默的环境中,这样你在对待自己的不足和犯下的过错的时候,就不会过于内疚或自我怀疑,这无疑是焦虑的解毒剂。

    +
  • +
  • 幽默法的目标是帮助你看到你的恐惧中的荒谬。

    +
  • +
  • “声音外化法”通常需要两个人在场。另一个人可以是朋友,也可以是家庭成员或是治疗师。

    +
  • +
  • 扮演消极想法角色的人听起来像是攻击你的另一个人,但我们必须学会抛开现象看本质。其实这“另一个人”正是你自己内心的消极想法。你其实是在和自己进行战斗。

    +
  • +
  • 扮演消极想法角色的人记得使用第二人称“你”,相反,扮演积极想法角色的人要以第一人称“我”来说话。

    +
  • +
  • 你也可以自己使用这种方法,而不需要其他人。你只需在纸上写下两个声音的对话,就像你在本章中读到的那些对话一样。

    +
  • +
  • 接受悖论法是一种反向运作的精神治疗方法。在使用接受悖论法的时候,你不是在一味攻击自己的消极思想,而是在这些消极想法中找到一些正确的地方。你同意这些消极的想法,但是要以一种幽默、平和和学习的方式。

    +
  • +
  • 如果你突然发现其实这些消极想法都是不实的,它们立刻就会失去力量。

    +
  • +
  • 接受悖论的目的不是隐瞒或否认你的缺点或瑕疵,也不是让你甘于平庸的生活,而是要把你的缺点暴露在光天化日之中,这样你才能不带一丝羞耻感地接受它们。如果你发现自己确实有问题,你就可以努力改善它。如果这些问题恰好是你无法改变的,你就可以简单地接受它并继续你的生活。

    +
  • +
  • 而当你使用激励治疗法时,则会问:“这种消极的想法或感觉对我有利吗?这种心态有什么好处?这样做对我来说有怎样的影响?”

    +
  • +
  • 虽然焦虑、抑郁和愤怒可能会给我们带来剧烈的痛苦,但它们往往会为我们提供可以让人上瘾的隐藏奖励。

    +
  • +
  • 成本效益分析有五种不同的形式: 1. 认知成本效益分析:评估一个消极思想的优点和缺点,

    +
  • +
    1. +
    2. 态度成本效益分析:评估自我攻击信念的优点和缺点,
    3. +
    +
  • +
    1. +
    2. 情感成本效益分析:评估消极情绪的优缺点,
    3. +
    +
  • +
    1. +
    2. 行为(或习惯)成本效益分析:评估一个坏习惯的优点和缺点,
    3. +
    +
  • +
    1. +
    2. 关系成本效益分析:评估一种会让你的人际关系产生问题的态度的优点和缺点,
    3. +
    +
  • +
  • 直接成本效益分析,

    +
  • +
  • 首先,我们将想要改变的想法、信念、感觉或习惯写在空白成本效益分析表的顶部(

    +
  • +
  • 矛盾成本效益分析法利用了这样一个事实:消极的思维模式、情绪和习惯可能会让你非常痛苦,也可能会给你带来好处。

    +
  • +
  • 恶魔建议法是为克服不良习惯和成瘾而开发的最强大的方法之一。这种方法基于一个简单而有力的想法——具有诱惑力的积极想法使我们屈服于习惯和成瘾。

    +
  • +
  • 大多数有坏习惯的人都不想改变。习惯和成瘾是会带来好处的,达到情绪的高潮状态也是一件有趣的事。

    +
  • +
  • 在上一章中,我给大家介绍了两种可以帮助我们克服拖延的方法:矛盾成本效益分析法和恶魔建议法。在本章中,你还会学习到另外四种技巧,它们可以帮助我们打破拖延的循环,提高工作效率和创造力,这四种方法分别是: 1. 快乐预测法 2. 任务拆解法 3. 反拖延法 4. 问题解决法

    +
  • +
  • 下文有一张“快乐程度预测表”。在“活动”一栏中,你可以记录下各种可能带来愉悦、促进学习或个人成长的活动。

    +
  • +
  • 在“预测满意度”这一栏中,你要预测每个活动的满意度并打分,打分范围从0分(完全不满意)到100分(完全令人满意)。

    +
  • +
  • 很多人都会发现,你最快乐的时候可能恰恰就是你和自己独处的时候。这就可以说明,真正的幸福只来自与其他人相处的经历的想法其实并不准确。

    +
  • +
  • 你可以将复杂的任务分解为一系列可以在几分钟内完成的小步骤。然后你可以一次只进行一个步骤,而不是试图一次完成所有事情。

    +
  • +
  • 当你只是一步一步地完成任务时,你会经常感到更有动力,而不会去关心自己为什么拖延这件事。

    +
  • +
  • 大多数拖延症患者都认为动机是第一位的,而行动则次之。但那些成功人士都知道,真实情况其实恰恰相反,行动才是最重要的,动机次之。

    +
  • +
  • 如果你每次都得等到“觉得想要”做的时候才去处理那些不愉快的任务,你就会永远等待。

    +
  • +
  • 真正的问题不在于“我能完成这项任务吗?”而是“我愿意完成这项任务吗?”以及“完成这项任务会对我有什么价值?”。

    +
  • +
  • 拖延者常常认为他们有权拒绝所有困难或不愉快的任务。他们觉得生活应该总是轻松愉快、没有挫败感的。

    +
  • +
  • 从来没有一条规则规定我们的生活永远都是轻松且有收获的。某些任务可能永远都不会令人愉快。

    +
  • +
  • 因为拖延的实质其实就是“明日复明日”。

    +
  • +
  • 没有任何事能阻碍你,你不需要很多花哨的步骤来解决问题。真正的问题一直都只是:你到底愿不愿意做这件事。

    +
  • +
  • 行为疗法认为,人们可以学会快速、直接地修正那些会造成精神问题的感受和行为,而不仅仅是靠在分析师的沙发上进行自由联想或探索过去。

    +
  • +
  • 焦虑的病人可以通过直接接触他们担心的事情来战胜自己内心的恐惧。

    +
  • +
  • 让患者暴露在担心的事物之前通常是一种有效的治疗手段。

    +
  • +
  • 暴露疗法其实源于《西藏渡亡经》中的一个传说。

    +
  • +
  • 如果你想要彻底战胜你的焦虑,你就必须要直面你心中的怪兽,战胜你心中最深的恐惧。这一概念也是暴露疗法的基石。恐惧让焦虑持续存在,而暴露在恐惧面前就是治疗焦虑的不二法门。

    +
  • +
  • 逃避会助长你的恐惧,增加你的焦虑。

    +
  • +
  • 暴露治疗法有三种基本类型:经典暴露法、认知暴露法和人际暴露法。

    +
  • +
  • 典暴露法需要我们在现实中面对恐惧。这

    +
  • +
  • 当你征服恐惧时,会有一种兴奋的感觉,那会使你曾经害怕的东西成为你快乐的

    +
  • +
  • 屈服和接受自己的恐惧通常是成功的关键。

    +
  • +
  • 我的恐惧层次结构图

    +
  • +
  • 强迫指的是人们为防范危险而采取的任何重复的、迷信的行为。

    +
  • +
  • 反应预防是所有强迫性仪式的首选治疗方法。使用这种方法,你只需要拒绝屈服于强迫性的冲动。停止这类仪式后,你会暂时变得更焦虑,就像戒断反应一样。但在你坚持一段时间后,冲动最终会消失。

    +
  • +
  • 当你屈服于你最害怕的事情时,康复可能只需要几分钟的时间。

    +
  • +
  • 当令你恐惧的东西仅仅只作为一个生动的记忆或可怕的幻想存在于你的大脑中的

    +
  • +
  • 知暴露法包括认知洪水法、图像替换法、记忆重写法和恐惧幻想法。这

    +
  • +
  • 如果你想要彻底战胜你的焦虑,你就必须要直面你心中的怪兽,战胜你心中最深的恐惧。

    +
  • +
  • 如果你想使用图像替换法,那么当你感到焦虑的时候,你就可以试着调整脑内消极的图像和幻想,让你的思绪充满想象力。

    +
  • +
  • 当你使用恐惧幻想法时,你会进入一个噩梦般的世界,在这个世界中你最害怕的事情会成真。

    +
  • +
  • 现实中不存在敌对的批评者,这一切只是你自己内心最深处的恐惧的投射。你真的是在和自己做斗争。

    +
  • +
  • 认知疗法认为,是我们的想法创造出了所有的积极情绪和消极情绪。

    +
  • +
  • 感到害羞的人并不愚蠢。为什么他们会相信这些扭曲的信息?这是因为,消极想法在此时此刻变成了自我实现的预言,所以它们看起来才如此真实。

    +
  • +
  • 害羞之中的认知扭曲

    +
  • +
  • 你觉得自己是一个受害者,你永远不会想到整个场景都是你自己的扭曲思维的直接结果。是你在强迫对方以你害怕的方式对待你。

    +
  • +
  • 五种人际暴露法分别是:微笑打招呼练习、搭讪练习、拒绝练习、自我揭露法和大卫·莱特曼法。

    +
  • +
  • 如果你很容易害羞,你可以做同样的事情。你可以强迫自己微笑,每天向十个陌生人问好。通常你会发现人们比你预期的要友善得多。

    +
  • +
  • 如果某次搭讪有效的话,应该会有如下的效果:

    +
  • +
  • ·别人会感觉到你很特别,并且敬佩你。

    +
  • +
  • 搭讪的第一个秘诀是要记住这只是一场游戏。搭讪本身就是为了追求乐趣。但如果你认真对待它,你可能就会失败,因为这个世界上不存在魔法。很多人对自己的生活感到厌倦,希望能够偶尔分分心。

    +
  • +
  • 如果他们感觉到你是以一种非常轻松的方式在搭讪,而不是严肃、认真地进行对话,他们会更喜欢你。但如果他们觉得你很饥渴或是想追他们,他们就会拒绝你。

    +
  • +
  • 人们总是喜欢那些他们求而不得的东西,而从不想要唾手可得的东西。

    +
  • +
  • 成年人基本上都是小孩,他们只是长大了而已,并看起来有些严肃,但究其根本,我们这些成年人仍然想玩,而且想玩得开心。

    +
  • +
  • 大部分人的生活都是很无趣的,所以人们总是想要寻找笑料,

    +
  • +
  • 如果你害怕被拒绝,你就可以试着尽可能多地积累被拒绝的经验,这样你就会知道,即使被拒绝,这个世界也会照常运转。

    +
  • +
  • 习惯被拒绝是开展更激动人心的社交生活的第一步。

    +
  • +
  • 自我暴露法要求我们不再在社交场合隐藏自己的害羞或紧张感,而是公开披露它们。

    +
  • +
  • 你完全可以向外界展示你的羞怯,而不是试图隐藏它,然后让自己看起来很“正常”。

    +
  • +
  • 自我暴露法认为你因为害羞而产生的羞耻感才是你真正的敌人。如果没有这种羞耻感,害羞实际上可以成为一种资产,因为它可以让你看起来更加脆弱和有吸引力。

    +
  • +
  • 其实,大多数人都对谈论自己更感兴趣,给别人留下深刻印象的最好方法就是把“他人”放在聚光灯下。你可以让其他人谈他们自己,然后你带着敬意去听。这可以让你成为观众,而不是表演者,这就可以大大减轻你的压力。

    +
  • +
  • 使用有效沟通的五个秘诀,

    +
  • +
  • 解除武装法:即使对方的言论听起来非常荒谬,也要努力找到对方言论中可圈可点的部分,每个人都喜欢被肯定。

    +
  • +
  • 思想同理和感受同理:试着通过对方的眼睛看世界。

    +
  • +
  • 你可以换一种方式总结对方说过的话,然后再加以反馈,这样一来,对方就知道你在听,并且也了解到了你的想法。

    +
  • +
  • 质询法:提出简单的问题来吸引对方。

    +
  • +
  • EAR。在下面的图上,我们可以看到,EAR是三个词的首字母缩写,分别代表Empathy(同理心)、Assertiveness(肯定)和Respect(尊重)。

    +
  • +
  • 有效沟通的五个秘诀(EAR)

    +
  • +
  • 有效沟通的五个秘诀可以通过两种不同的方式帮助你在做公共演讲的时候摆脱焦虑。首先,因为你会发现你有一种神奇的方式处理人们在演讲期间对你说的任何话,所以焦虑自然而然就消失了。其次,如果某人确实提出了一个令人讨厌或困难的问题,并且你巧妙地使用了解除武装法和夸赞法,那么他们就会积极回应,因为他们会发现问问题是很安全的。这将让所有的观众情绪高涨。

    +
  • +
  • 大约75%的焦虑症患者都在隐藏自己的情绪和感觉。

    +
  • +
  • 只要我们把这些情绪问题拿到台面上来,焦虑很快就消失了,

    +
  • +
  • 大多数患有焦虑症的人都过于善良。我觉得,善良几乎是所有焦虑的原因。

    +
  • +
  • 你总是过于善良,而且你并不总是敢于展示你的真实感受。

    +
  • +
  • 这些感到焦虑的人甚至不知道自己的感受。

    +
  • +
  • 如果你感到焦虑,情感隐藏法绝对值得一试。这个方法有两个步骤: 1. 发现问题。

    +
  • +
  • 找到解决方案。

    +
  • +
  • 虑其实是你的身体在告诉你:“嘿,你对这件事感到不安,去查看一下吧。”

    +
  • +
  • 情感隐藏法涉及两个步骤: 1. 找出困扰你的问题或感觉。 2. 表达你的感受,并采取措施解决问题。

    +
  • +
  • 如果你很容易产生焦虑的情绪,你常常会无意识地忽略自己的感受,而被你忽视掉的感受会间接地出现,伪装成焦虑的样子。

    +
  • +
  • 当人们感到沮丧的时候,有些人会开始担心,有些人会产生恐惧症,有些人,比如特丽,会发生惊恐发作,还有一些人则可能会出现强迫症状。

    +
  • +
  • ·焦虑通常是对你的冲突或问题的象征性表现。这是你的大脑间接传达你的压抑感受的方式。

    +
  • +
  • 焦虑就像一个清醒的梦。焦虑的人就像艺术家和诗人一样,间接地用图像和隐喻来表达感情。

    +
  • +
  • 大多数人认为焦虑是一件坏事,不是好事。但我持相反的观点。没有人能一直感到幸福。我们都会不时地感到心碎和失望。

    +
  • +
  • 焦虑一定是人为的某些因素引起的,而焦虑背后真正的恐惧则是对真实情感和感受的恐惧。

    +
  • +
  • 战胜恐惧的40种方法

    +
  • +
  • 每日情绪日志”的五个步骤: 第一步:描述一件令人难受沮丧的事件,记录下任何一个让你觉得焦虑或沮丧的瞬间。 第二步:在表格中圈出符合你消极感受的词语,并按照从0(完全没有这样想)到100(完完全全是我的想法)的等级进行评分。 第三步:记录下你的消极想法,根据你对每个想法的相信程度在0~100之间进行打分。 第四步:找出每个消极想法中的扭曲认知。 第五步:用更积极、更现实的想法替换原有的消极想法。根据你对这些积极想法的相信程度对它们在0~100之间进行打分。现在,再次评估你对每个消极想法的相信程度。

    +
  • +
  • 如果康复圈中的消极想法让你感到焦虑,请确保你选择的方法中包含了三个种类的方法:认知治疗法、暴露治疗法和情感隐藏治疗法。这其实是一个很好的方法组合,你选择的方法中可以包括十二到十五种认知疗法、两种或三种暴露治疗法和情感隐藏治疗法。

    +
  • +
  • 平均而言,你必须尝试至少十到十五种方法,才能找到一个行之有效的识破消极想法的方法。

    +
  • +
  • 康复圈是为“每日情绪日志”提供动力的引擎。

    +
  • +
  • 在你想出一个能够满足情绪变化的必要和充分条件的积极想法之前,你的情况是不会有所改善的: ·必要条件:积极想法必须是百分百真实的。 ·充分条件:积极想法需要让消极想法不攻自破。

    +
  • +
  • 表22-15 基于消极想法中的认知扭曲选择方法

    +
  • +
  • 22-16 基于你正在克服的问题选择方法

    +
  • +
  • 战胜恐惧的40种方法

    +
  • +
  • 当你使用检查证据法时,你会问自己这样的问题:“有没有什么可靠的证据可以支持我的消极想法?我是怎么在第一时间得出这个结论的?”

    +
  • +
  • 没有公式或噱头可以盲目地应用于不同的问题或焦虑类型,相反,我给了你一些灵活、强大、个性化的方法,你可以用它们来克服困扰你的情绪问题。

    +
  • +
  • 如果你曾经因焦虑或抑郁而挣扎,你迟早会再次感到焦虑或沮丧。事实上,所有人的焦虑都会复发!

    +
  • +
  • 佛陀说,痛苦是人类的固有特征,这是不可避免的。没有人能够一直感到幸福,如果可能的话,一直开心其实也不会是件好事。如果我们一直很开心,我们的情绪就没有任何变化,也不存在任何挑战,生活很快就会变得无聊,因为我们总是感觉完全一样。古拉丁谚语说得好:饥饿是才最甜的酱汁。

    +
  • +
  • 复发时的认知扭曲

    +
  • +
  • 复发每日情绪日志

    +
  • +
  • 复发每日心情日志(续表)

    +
  • +
  • 要经常让自己面对心中的恐惧,这样你的信心就会增长。

    +
  • +
  • 焦虑或恐慌的感觉并不是一件坏事,而是一个重要的信号,表明有你需要注意的事情发生了。

    +
  • +
+ +
+ + + + + + + +
+ + + + + + +
+ +
+ + + +
+ + + + + + + + +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2021/altitude-extract/index.html b/2021/altitude-extract/index.html new file mode 100644 index 0000000000..840c4fd26f --- /dev/null +++ b/2021/altitude-extract/index.html @@ -0,0 +1,609 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 《格局》摘抄 | 贾攀的流水账 + + + + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

贾攀的流水账

+ +
+

Panmax's Blog

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+ + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ 《格局》摘抄 +

+ + +
+ + + + +
+ + +

人有多大的气度,就做多大的生意

+

格局大的人追求的是重复的成功和可叠加式的进步,格局小的人满足于自己某件事做得快、做得漂亮。

+

要做到高速率、可叠加式的进步,关键是做减法,懂得放弃

+

管理上级不是给上级分配任务,也不是不服从上级的安排,而是让上级了解我们的工作,并且在必要时及时寻求上级的帮助。对于这样具有高度主动性的员工,上级都喜欢。

+

凡事总有“两面”——好的一面和坏的一面,当大家一致觉得一件事只有好的一面时,并不代表它不存在坏的一面,很可能是大家认识不够深刻,没有看到一些盲点。而那些没有被发现的问题,一旦发生,后果可能极为严重,甚至是灾难性的。

+

对于那些人们都觉得好的事情,我会格外小心,因为我们可能忽视了它们的问题。

+

众利勿为,众争勿往

+

很多投资人以为抢一条所谓的“赛道”就能分一杯羹,岂不知众人相争,最后只有一个结果——相互碾轧致死。

+

当一种特长被很多人掌握之后,就不叫特长了。

+

为什么中国人硅谷**晋升得没有印度人快,原因有很多,其中一个小原因是,部分中国人在**分享利益**这件事上做得不好,不注重相互提携**。

+

我们的祖先在《礼记•大学》中这样告诫大家:“好而知其恶,恶而知其美者,天下鲜矣。”

+

对比较理性的人来讲,他们通常不问做错事是否有理由,而是确定当前是否做错了事。

+

我们要做的是超过他人的长处,而不是满足于超越别人的短处

+

所谓不认命,就是以为世界上所有事情自己都能控制,这是一种妄念,是对自己的迷信。

+

尽人事,听天命。

+

散户在股市上亏损的根本原因在于,把偶然的成功归结为自己努力的必然结果,把失败归咎于别人,对市场完全没有敬畏之心。

+

为什么要听天命呢?因为世界上稍微难点儿的事情都非常复杂,超出我们的有限认知,更超出我们的控制能力

+

承认天命的作用,我们在做人时就不会恃才傲物。但凡觉得自己了不起的人,通常都没有见过真正聪明能干的人。人只有到了人才荟萃的地方,才能体会到自己水平上的不足。

+

比才能更重要的是见识,而在见识之上还有运气。

+

人的命运是由大环境和自身做事情的方法决定的。

+

业余的水平再高也是业余的。

+

对绝大多数人来讲,一次好运气并不足以改变命运。

+

遇到任何倒霉的事情,一定要认命,不要总想着挽回损失,这样损失就会被限制在局部。

+

如果认识到自己只是一个普通人,自己的那点儿所得不过是上天的恩赐,得到了固然可喜,得不到也在情理之中,就愿意割舍,也就不会造成更大的损失。

+

人不会总有好运气,也不会永远走背运,但是不好的心态会让厄运不断被放大。

+

人在一个环境中待久了,难免产生思维定式

+

跳出思维定式的最好办法就是放下手中的工作,休息休息。

+

从忙乱中退一步,思考一下目的,能省掉多余的需求和行动,减少不必要的麻烦,让我们更快地接近目标。在诸多目标中,终极目标当属生活本身。

+

每一次重大科技进步的结果总是财富进一步向少数人集中,大部分人的生活压力更大了。

+

很多事情,我们连做它们的目的都没有想清楚,就在世俗力量的驱赶下随着奔涌不停的人潮匆匆去做了。

+

人不在于开始了多少件事,而在于完美地结束了多少件事。

+

对于人来讲,说得通俗点儿,多任务并行就是一心多用

+

如果一心多用,不仅不能多做事情,反而会因为来回切换任务而降低工作效率,还容易导致错误不断。

+

辛苦且回报低的专业能找到,但是轻松而回报高的专业几乎不存在。

+

速成的崇拜也是“瞎忙族”的一大特点。他们相信自己能找到别人找不到的捷径,而不是沉住气慢慢提升自己。

+

只要把做事的节奏慢下来,先动脑,再动手,把可做可不做的事情从任务清单上删除;在做事的过程中按部就班地把事情做好,不要开了很多头却不结尾;做完事情,审视一下自己的得失,评估一下效果,以备将来参考。

+

战术上的勤奋掩盖战略上的懒惰

+

当遇到困境时,我们首先应该慢下来,斩断厄运链。

+

世界上没有任何一个人重要到什么事情缺了他就不能运转了。

+

休息的本质是从外界获得信息和能量。

+

真正的成功者,真正有幸福生活的人,应该在现实生活中获得成功,获得最真实和最丰富的生活。

+

每一个人的具体生活是独一无二的,既不能由别人代替,也不可能等以后有时间再补上。

+

我们做的那些引以为豪的事情,其实远没有我们以为的那么重要。

+

幸福生活才是目的个人的成功不过是实现这个目的的途径和手段而已。

+

新加坡最大的好处是“省心”,一个人只要从小当好学生,然后上好学校,将来努力工作,就能挣到钱,并且赢得他人的尊重。相比之下,我们的努力往往未必能得到回报。这种不确定性会让人觉得看不到希望,幸福感自然不会高。

+

人这一辈子,大部分时候需要的不是去战斗、去征服、去比别人考得好,而是要对别人有用

+

《红楼梦》还有一个特点:它是一本关于女孩子的书。在《红楼梦》中,贾宝玉在某种程度上都被女性化了,这在中国的经典著作中很少见。男生若要读懂女生的心思,不妨读读它。

+

一个人一辈子的幸福在很大程度上取决于他(她)的婚姻

+

很多在美国上市的中国公司,上市后业务增长得不错,但是由于根本不关心投资人的利益,股价几乎不上涨,甚至低于刚上市时的水平。这些公司就是对投资人不好的公司。

+

巴菲特所谓的好公司有这样几个共同的特点:

+
    +
  • 第一,能够稳定发放股息
  • +
  • 第二,有多余的现金时会回购股票(这样可以推高股价)。
  • +
  • 第三,不断提高自己的利润率,而不是将大量的利润分给员工,或者管理层直接把利润拿走。
  • +
+

一个帮助过你的人,比一个你帮助过的人,更愿意帮助你。

+

一个人在选择工作单位时,应该把对自己好、能帮助自己成长的公司放在首位,而不是觉得某家公司很酷、很热门或者多给了一点儿薪水就选择它。

+

其实在所谓“命”的背后,起主导作用的是我们判断价值的方法。

+

至于生活的伴侣对自己好是比金钱、门第和外貌更持久的依靠

+

素质教育是以掌握一项技能为前提的。

+

我追求的是一种最好只有我能做,别人难以胜任的工作,也就是要体现出我的不可替代性

+

这是真正自由的人的想法,只有在金钱和地位面前丢弃掉奴性,保持自由人的心态,才能赢得对方的尊重

+

一些朋友问我如何判断一件事情是否有必要做,我的标准是,那些花了精力做的事情要尽可能对自己将来的进步有益。

+

永远待在舒适区,只会让人无法成长。每个人的成长,最终是在边界内最大程度上把事情做好。

+

一个人成长的过程,其实就是逐渐“杀死”心中那些超级英雄的过程。

+

孩子最终能走多远,不取决于父母给他们描绘的承诺,而更多地取决于他们自己在不停往前走方面有多大的意愿

+

对那些仅仅满足不失败的人来讲,失败的教训可以让他们避免犯同样的错误;但是对于想成功的人而言,失败的教训远没有成功的经验重要。一个经常失败的人会习惯性失败,相反,成功才是成功之母

+

从失败中固然可以学到经验教训,但是**效率实在太低了**。

+

虽然人很难做一件事情就成功一件,但总该尽量避免失败,这样才能少受挫折。

+

成就的多少至少取决于三个因素:做事情的速度或做事情的数量,每一件事的影响力,以及做事的成功率

+

对一个人来讲,如果一辈子非常努力地做了很多没有影响力的事情,还不如认认真真做好一件有一定影响力的事情。

+

一个优秀的专业人士在做事之前,会梳理出一个做事清单,按照重要性和影响力的量级排序,然后集中资源把最重要、影响力最大的事情先做完。

+

做事的多少最多不过是几倍的差异,但做事的质量以及随后带来的影响力可以达到量级之差。

+

成功不在于是否努力多做两件事,而在于能否跃迁到更高的量级。

+

提升量级不仅需要时间,还常常需要在关键时间点实现跳跃

+

不要醉心于重复做很多影响力微乎其微的事情,否则即使再努力,也难以有大成就。

+

所谓最具普遍意义的通向成功的方法论,从根本上说,就是搞清楚做事的边界或者极限,搞清楚做事的起点以及从起点通向边界的道路。

+

做事情最有效、最容易成功的办法,就是先将自己的基线提高,而不是从地下三层做起。

+

专业人士和业余爱好者的一个差别在于,是否了解极限的存在。

+

所谓工程化,就是依靠一套可循的,甚至相对固定的方法解决未知的问题。

+

失去的朋友大致有三类。

+
    +
  • 第一类是因为人生经历的变化而无法维系关系的
  • +
  • 第二类是因为交友不慎结交的假朋友,失去也不可惜。
  • +
  • 第三类则是因为彼此没有处理好朋友关系而失去的,事过之后回想起来,常常会让人怅然不已。
  • +
+

朋友关系有很多类型,常见的可以归为三类:合作型、依靠型和暧昧型

+

我们的世界并非那么灰暗,即便有挫折,也是暂时性的。

+

不论形势是好是坏,总有人对我们的生活进行悲观的解读。对未来可能发生的灾难有防范意识当然好,但是用悲观主义(包括怀疑主义)的心态做事,弊要远远大于利。因为这种心态让人惶惶不可终日,难以专注做自己该做的事情,最后变得一事无成。

+

人的过分自信以及由此造成的与现实之间的反差,是导致悲观主义的第一个原因,也是根本原因。

+

人过高估计自己的能力,在现实生活中却得不到想要的东西,才会产生悲观情绪。

+

一个人能否做成一件事,和是否有信心无关。

+

一个人不断往上走,眼界越来越开阔后,就越知道自己能力的局限,会越谦逊,越有敬畏之心,就不会再有不切实际的奢望了。

+

在中华文化圈内的国家和地区,经济腾飞阶段的第一代人,主要的财富来自在房地产上一次性的增值获利,而非工资收人。

+

焦虑,反映出人们对未来的怀疑;如果没有对不确定性的担心,就不会焦虑。

+

我们不仅无法回到过去,也不会习惯过去的生活,除了往前走,没有第二个选择。

+

乐观主义者往往不会杞人忧天,安下心来把事情做好,自然就能得到想要的结果。

+

为人处世,成功的第一要素就是走正道,不要总想着出奇制胜,特别是在未来非常光明的时候。

+

很多人一件事没有做好,就想着改变,好像一变就有机会了。且不说变化是否能给有这样想法的人带来机会,就算有,没有积累的人也把握不住机会

+

虽然盖茨扎克伯格退学后创业成功了,那是因为他们已经知道怎么挣钱,而不是退了学才去想挣钱的方法。

+

临渊羡鱼,不如退而结网。

+

未来的三个特点,即不对称性复杂性不确定性

+

是否利用了新技术不是核心,利用新技术实现提高效率降低成本的目的才是关键,因为降低成本、提高利润才是核心,才是不变的道理。

+

技术从来都是手段而不是目的,搞不清楚这一点,就会为了技术而研发技术。

+

洞察本质才能立于不败之地

+

事实上对大多数人来讲,更好的改变方式是学会计算机思维,将它用于自己熟悉的行业,扩大自己原有的优势。

+

在当今的商业世界里什么比较重要呢?对于商家来讲,最直接、最重要的标准是ARPU

+

一个公司在规模不大时,在关注度上和大公司进行全方位竞争是没有意义的,它更应该关心自己的核心用户,关心自己能给他们带来什么价值。

+

在当下这样一个风险投资资金过剩的年代,通过融资买关注是一件很容易的事情。花钱买用户的事情谁都会做,但是能提高ARPU值才是真本事。

+

互联网时代从来不缺乏免费的内容,最珍贵的资源是我们的时间。不要花太多工夫读那些免费、廉价,但是质量低的内容,读它们不仅浪费时间,甚至会误导我们。

+

无论是想得到关注,还是关注别人的,都需要记住一个关键词——优质

+

在信息可以随意复制的年代,创造信息不是什么难事,提供自己特有的、人们原先不知道的信息才有价值,重复别人的内容完全没有意义。

+

免费能够成功,是因为过去的一些东西有稀缺性,消费者不得不购买,这时免费就变得特别吸引人。当那些东西不再有稀缺性时,免费就没有意义了。

+

超越免费的第一条是制造一种稀缺性,而这需要产品、服务本身具有一种难以复制的特性。

+

时效性个性化可用性(易理解性)、可靠性黏性

+

终身学习目的就是让自己领先同辈人一步,以便成为具有时效性的人才,避免在低水平上竞争。

+

要求所有人都有一样的表现是工业时代的特征,因为只有那样才能保证行动一致,做出来的东西品质才能一致。

+

在任何时代,把事情解释清楚这个本领都可以变成一个很赚钱的生意

+

数据的积累可以让企业的护城河越来越深。

+

在信息时代,信息越透明、越对称,流动性越好,李嘉图定律导致的势差就会越大。

+

在信息时代,李嘉图定律带来的势差放大效应,会导致一个地区人员结构、产业结构的巨变。

+

随着信息流动性增强以及智能技术的提高,个别能力超强的人可以在技术的帮助下发挥巨大作用,行业里不再需要四流、五流的从业者了。

+

聘用人员时,不要贪便宜雇一大堆三流人士来充数,因为一堆三流的人聚在一起,有时带来的麻烦比他们能解决的问题还多。

+

在市场上,第二名永远无法拿到第一名的估值,第三名之后的价值几乎等于零。

+ +
+ + + + + + + +
+ + + + + + +
+ +
+ + + +
+ + + + + + + + +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2021/avoid-using-append-in-go/index.html b/2021/avoid-using-append-in-go/index.html new file mode 100644 index 0000000000..193a3c1be7 --- /dev/null +++ b/2021/avoid-using-append-in-go/index.html @@ -0,0 +1,507 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 避免在 Go 中使用 append | 贾攀的流水账 + + + + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

贾攀的流水账

+ +
+

Panmax's Blog

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+ + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ 避免在 Go 中使用 append +

+ + +
+ + + + +
+ + +

append 是我们向切片添加元素时的首选函数,但这可能不是最好用法。原因如下:

+

首先,我们创建两个函数,功能是将字符 “x” 填充进一个字符串切片。

+

WithAppend 调用 append 将 “x” 添加到一个字符串切片中

+
1
2
3
4
5
6
7
8
func WithAppend() []string {
var l []string
for i := 0; i < 100; i++ {
l = append(l, "x")
}

return l
}
+

WithAssignAlloc 通过用 make 来创建一个指定大小的字符串切片,之后赋值 “x” 给指定索引位而不是使用 append

+
1
2
3
4
5
6
7
8
func WithAssignAlloc() []string {
l := make([]string, 100)
for i := 0; i < 100; i++ {
l[i] = "x"
}

return l
}
+

这两个函数返回相同的结果,但其实现方式完全不同。

+

现在,让我们对这些函数进行基准测试。

+
1
2
3
4
5
6
7
8
9
10
11
12
func BenchmarkWithAppend(b *testing.B) {
b.ReportAllocs()
for i := 0; i < b.N; i++ {
WithAppend()
}
}
func BenchmarkWithAssignAlloc(b *testing.B) {
b.ReportAllocs()
for i := 0; i < b.N; i++ {
WithAssignAlloc()
}
}
+

结果如下:

+
1
2
3
4
BenchmarkWithAppend
BenchmarkWithAppend-8 863949 1322 ns/op 4080 B/op 8 allocs/op
BenchmarkWithAssignAlloc
BenchmarkWithAssignAlloc-8 2343424 523 ns/op 1792 B/op 1 allocs/op
+

WithAppend 的性能最差,而 WithAssignAlloc 的性能最好,这个结论应该可以说服你应该避免 append 了吧?

+

但先别急着走。

+

我们再写一个使用 append 的函数,并通过指定大小和容量来创建一个字符串切片。

+
1
2
3
4
5
6
7
func WithAppendAlloc() []string {
l := make([]string, 0, 100)
for i := 0; i < 100; i++ {
l = append(l, "x")
}
return l
}
+

再次运行基准测试。

+
1
2
3
4
5
6
BenchmarkWithAppend
BenchmarkWithAppend-8 863949 1322 ns/op 4080 B/op 8 allocs/op
BenchmarkWithAppendAlloc
BenchmarkWithAppendAlloc-8 2543119 514 ns/op 1792 B/op 1 allocs/op
BenchmarkWithAssignAlloc
BenchmarkWithAssignAlloc-8 2343424 523 ns/op 1792 B/op 1 allocs/op
+

现在我们在 WithAppendAllocWithAssignAlloc 上得到了同样好的性能。

+

为什么 WithAppend 性能很差?在使用 WithAppend 往切片中添加元素时,当切片的容量不足时,需要创建一个新的更大的切片来对切片进行扩容,这导致多次分配。

+
+

在你优化代码之前,应该通过基准测试来找到代码中的瓶颈。上面的例子过于简化,你可能并不总是知道应该提前分配切片的大小。

+

另外,过早地进行性能调整可能会矫枉过正。

+ +
+ + + + + + + +
+ + + + + + +
+ +
+ + + +
+ + + + + + + + +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2021/build-a-full-text-search-engine/book-index.png b/2021/build-a-full-text-search-engine/book-index.png new file mode 100644 index 0000000000..6c05e58f6b Binary files /dev/null and b/2021/build-a-full-text-search-engine/book-index.png differ diff --git a/2021/build-a-full-text-search-engine/index.html b/2021/build-a-full-text-search-engine/index.html new file mode 100644 index 0000000000..a9fabcc5d0 --- /dev/null +++ b/2021/build-a-full-text-search-engine/index.html @@ -0,0 +1,599 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 从零开始搭建一个全文检索引擎 | 贾攀的流水账 + + + + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

贾攀的流水账

+ +
+

Panmax's Blog

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+ + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ 从零开始搭建一个全文检索引擎 +

+ + +
+ + + + +
+ + +

全文检索是我们每天都使用的工具之一,在谷歌上搜索「golang 入门」或在淘宝上搜「智能音箱」,就会用到全文检索技术。

+

全文检索(FTS full text search)是一种在文档集合中搜索文本的技术。文档可以指网页、报纸上的文章、电子邮件或任何结构化文本。

+

今天我们将建立我们自己的 FTS 引擎。在这篇文章结束时,我们将实现一个能够在一毫秒内搜索数以百万计的文档的程序。我们从简单的搜索查询开始,比如:找出所有包含「cat」这个词的文档,然后我们将扩展这个引擎以支持更复杂的布尔查询。

+
+

注:目前最著名的 FTS 引擎是 Lucene(以及建立在它之上的 Elasticsearch 和 Solr)。

+
+

为什么是FTS

在我们开始写代码之前,你可能会问:「我们就不能用 grep 或者用一个循环来检查每个文档是否包含我所要找的词吗?」。

+

是的,可以这样做。但这并不是最好的解决方案。

+

语料库

我们将搜索英文维基百科的一部分摘要。最新的离线数据可以在 dumps.wikimedia.org 上找到。压缩包解压后的 XML 文件为 956MB(截止2021年05月17日),包含60多万个文档。

+

文档的例子:

+
1
2
3
<title>Wikipedia: Kit-Cat Klock</title>
<url>https://en.wikipedia.org/wiki/Kit-Cat_Klock</url>
<abstract>The Kit-Cat Klock is an art deco novelty wall clock shaped like a grinning cat with cartoon eyes that swivel in time with its pendulum tail.</abstract>
+

加载文件

首先,我们需要加载所有文件,这一步使用内置的 encoding/xml 包就就够了。

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
import (
"encoding/xml"
"os"
)

type document struct {
Title string `xml:"title"`
URL string `xml:"url"`
Text string `xml:"abstract"`
ID int
}

func loadDocuments(path string) ([]document, error) {
f, err := os.Open(path)
if err != nil {
return nil, err
}
defer f.Close()

dec := xml.NewDecoder(f)
dump := struct {
Documents []document `xml:"doc"`
}{}
if err := dec.Decode(&dump); err != nil {
return nil, err
}

docs := dump.Documents
for i := range docs {
docs[i].ID = i
}
return docs, nil
}
+

每个被加载的文档都被分配了一个唯一 ID。简单起见,第一个文档的 ID=0,第二个 ID=1,以此类推。

+

第一次尝试

搜索内容

现在我们已经把所有的文档都加载到了内存中了,我们可以试着找到所有关于 cat 的文档。

+

首先,让我们循环遍历所有的文档,检查它们是否包含 cat 这个子串。

+
1
2
3
4
5
6
7
8
9
func search(docs []document, term string) []document {
var r []document
for _, doc := range docs {
if strings.Contains(doc.Text, term) {
r = append(r, doc)
}
}
return r
}
+

在我的 Macbook Pro 上,搜索阶段耗时 68ms,还不错。

+

我们抽查结果中的几个文件,会发现这个函数匹配了 caterpillar(毛毛虫)和 category,但没有匹配大写字母 C 开头的 Cat,这不太符合我们的预期。

+

在继续前进之前,我们需要解决两件事:

+
    +
  • 使搜索不区分大小写(Cat 要匹配)。

    +
  • +
  • 在单词边界而不是在子字符串上匹配(caterpillarcategory 不匹配)。

    +
  • +
+

用正则表达式进行搜索

一种可以快速想到并实现这两个要求的方案是使用正则表达式。

+

在这里为 (?i)\bcat\b

+
    +
  • (?i)使正则表达式不区分大小写

    +
  • +
  • \b 匹配一个词的边界。

    +
  • +
+
1
2
3
4
5
6
7
8
9
10
func search(docs []document, term string) []document {
re := regexp.MustCompile(`(?i)\b` + term + `\b`) // 有安全风险,不要在生产环境中这样用
var r []document
for _, doc := range docs {
if re.MatchString(doc.Text) {
r = append(r, doc)
}
}
return r
}
+

这次搜索花了近 2 秒。正如我们所看到的,即使只有 60 万个文档,搜索也开始变得缓慢。虽然这种方法很容易实现,但它不能很好地扩展。随着数据集的增大,我们需要扫描的文档越来越多。这种算法的时间复杂度是线性的——需要扫描的文档数量等于文档总数。如果我们有 600 多万个文档,搜索将需要 20 秒。

+

倒排索引

为了使搜索查询更快,我们要对文本进行预处理,并提前建立一个索引。

+

FTS 的核心是一个叫做倒排索引的数据结构,倒排索引将文档中的每个单词与包含该单词的文档关联起来。

+

例子:

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
documents = {
1: "a donut on a glass plate",
2: "only the donut",
3: "listen to the drum machine",
}

index = {
"a": [1],
"donut": [1, 2],
"on": [1],
"glass": [1],
"plate": [1],
"only": [2],
"the": [2, 3],
"listen": [3],
"to": [3],
"drum": [3],
"machine": [3],
}
+

下面是倒排索引的现实世界的例子:一本书中的索引,其中每个术语都引用了一个页码。

+

+

文本解析

在我们开始建立索引之前,我们需要将原始文本分解成适合索引和搜索的单词(tokens)列表。

+

文本解析器由一个分词器和多个过滤器组成。

+

+

分词器(tokenizer)

分词是文本解析的第一步,它的工作是将文本转换成一个单词列表。我们本次的实现是在一个词的边界上分割文本,并删除标点符号。

+
1
2
3
4
5
6
func tokenize(text string) []string {
return strings.FieldsFunc(text, func(r rune) bool {
// Split on any character that is not a letter or a number.
return !unicode.IsLetter(r) && !unicode.IsNumber(r)
})
}
+
1
2
3
> tokenize("A donut on a glass plate. Only the donuts.")

["A", "donut", "on", "a", "glass", "plate", "Only", "the", "donuts"]
+

过滤器

大部数情况下,仅仅将文本转换为一个单词列表是不够的。为了使文本更容易被索引和搜索,我们还需要做额外的规范化处理。

+

小写字母

为了使搜索不区分大小写,小写过滤器将单词转换为小写。cAt、Cat 和 caT 被归一化为 cat。之后在我们查询索引时,也会将搜索词进行小写处理。这样就可以让搜索词 cAt 与文本 Cat 相匹配了。

+
1
2
3
4
5
6
7
func lowercaseFilter(tokens []string) []string {
r := make([]string, len(tokens))
for i, token := range tokens {
r[i] = strings.ToLower(token)
}
return r
}
+
1
2
3
> lowercaseFilter([]string{"A", "donut", "on", "a", "glass", "plate", "Only", "the", "donuts"})

["a", "donut", "on", "a", "glass", "plate", "only", "the", "donuts"]
+

排除常用词

几乎所有英语文本都包含常用的单词,如 a、I、the 或 be。这样的词被称为停词,我们要将它们删掉,因为几乎任何文档都会与这些停顿词相匹配。

+

没有「官方」的停词表,这里我们把 OEC 排名的前10的词进行排除。

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
var stopwords = map[string]struct{}{
"a": {}, "and": {}, "be": {}, "have": {}, "i": {},
"in": {}, "of": {}, "that": {}, "the": {}, "to": {},
}

func stopwordFilter(tokens []string) []string {
r := make([]string, 0, len(tokens))
for _, token := range tokens {
if _, ok := stopwords[token]; !ok {
r = append(r, token)
}
}
return r
}
+
1
2
3
> stopwordFilter([]string{"a", "donut", "on", "a", "glass", "plate", "only", "the", "donuts"})

["donut", "on", "glass", "plate", "only", "donuts"]
+

词干化

由于语法规则的原因,文档中可能包括同一个词的不同形式。词干化将单词还原为其基本形式。例如,fishing、fished 和 fishe r可以被还原为基本形式(词干)fish。

+

实现词干化是一项很大的任务,在本文中不进行涉及。我们将采用现有的一个模块。

+
1
2
3
4
5
6
7
8
9
import snowballeng "github.com/kljensen/snowball/english"

func stemmerFilter(tokens []string) []string {
r := make([]string, len(tokens))
for i, token := range tokens {
r[i] = snowballeng.Stem(token, false)
}
return r
}
+
1
2
3
> stemmerFilter([]string{"donut", "on", "glass", "plate", "only", "donuts"})

["donut", "on", "glass", "plate", "only", "donut"]
+
+

注:词干并不总是一个有效的词。例如,有些词干器可能会将 airline 简化为 airlin。

+
+

将解析器组合在一起

1
2
3
4
5
6
7
func analyze(text string) []string {
tokens := tokenize(text)
tokens = lowercaseFilter(tokens)
tokens = stopwordFilter(tokens)
tokens = stemmerFilter(tokens)
return tokens
}
+

分词器和过滤器将句子转换为一个单词列表:

+
1
2
3
> analyze("A donut on a glass plate. Only the donuts.")

["donut", "on", "glass", "plate", "only", "donut"]
+

这个列表已经做好了索引的主板内。

+
+

建立索引

回到倒排索引,它把文档中的每个词都映射到文档 ID 上。内置的 map 是存储该映射很好的选择。map 中的键为单词(字符串),值为文档 ID 的列表。

+
1
type index map[string][]int
+

建立索引的过程包括解析文档(调用前边的 analyze 函数)并将其 ID 添加到 map 中。

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
func (idx index) add(docs []document) {
for _, doc := range docs {
for _, token := range analyze(doc.Text) {
ids := idx[token]
if ids != nil && ids[len(ids)-1] == doc.ID {
// Don't add same ID twice.
continue
}
idx[token] = append(ids, doc.ID)
}
}
}

func main() {
idx := make(index)
idx.add([]document{{ID: 1, Text: "A donut on a glass plate. Only the donuts."}})
idx.add([]document{{ID: 2, Text: "donut is a donut"}})
fmt.Println(idx)
}
+

map 中的每个单词都指向包含该单词的文档 ID。

+
1
map[donut:[1 2] glass:[1] is:[2] on:[1] only:[1] plate:[1]]
+

查询

为了对索引进行查询,我们对查询词使用与索引的相同分词器和过滤器:

+
1
2
3
4
5
6
7
8
9
func (idx index) search(text string) [][]int {
var r [][]int
for _, token := range analyze(text) {
if ids, ok := idx[token]; ok {
r = append(r, ids)
}
}
return r
}
+
1
2
3
> idx.search("Small wild cat")

[[24, 173, 303, ...], [98, 173, 765, ...], [[24, 51, 173, ...]]
+

最后,我们可以找到所有提到 cat 的文件。搜索 60 多万个文档只花了不到一毫秒的时间。

+

有了倒排索引,搜索查询的时间复杂度与要搜索单词的数量成线性关系。在上面的例子中,解析完输入文本后,搜索只需要进行三次 map 查询。

+

布尔查询

上边的查询为每一个单词都返回了一个文档 ID 列表。当我们在搜索框中输入 small wild cat 时,我们通常期望找到的是一个同时包含 small、wild 和 cat 的结果列表。下一步是计算这些列表之间的集合交集,这样我们就可以得到一个与所有单词相匹配的文件列表。

+

+

我们的倒排索引中的 ID 是以升序插入的。由于 ID 是有序的,所以可以在线性时间内计算两个列表之间的交集。intersection 函数同时遍历两个列表,并收集同时存在于两个列表中的 ID。

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
func intersection(a []int, b []int) []int {
maxLen := len(a)
if len(b) > maxLen {
maxLen = len(b)
}
r := make([]int, 0, maxLen)
var i, j int
for i < len(a) && j < len(b) {
if a[i] < b[j] {
i++
} else if a[i] > b[j] {
j++
} else {
r = append(r, a[i])
i++
j++
}
}
return r
}
+

使用更新后的 search 方法解析给定的查询文本查找单词计算ID列表之间的集合交集

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
func (idx index) search(text string) []int {
var r []int
for _, token := range analyze(text) {
if ids, ok := idx[token]; ok {
if r == nil {
r = ids
} else {
r = intersection(r, ids)
}
} else {
// Token doesn't exist.
return nil
}
}
return r
}
+

维基百科的离线数据中只有两个文档同时与 small、wild 和 cat 相匹配。

+
1
2
3
4
> idx.search("Small wild cat")

130764 The wildcat is a species complex comprising two small wild cat species, the European wildcat (Felis silvestris) and the African wildcat (F. lybica).
131692 Catopuma is a genus containing two Asian small wild cat species, the Asian golden cat (C. temminckii) and the bay cat.
+

搜索到了我们所预期的结果!

+

总结

我们刚刚建立了一个全文检索引擎,尽管它很简单,但它可以成为更高级项目的基础。

+

我没有触及到太多可以显著提高性能和使引擎更友好的内容,下面是几个进一步改进的想法:

+
    +
  • 扩展布尔查询,支持 OR 和NOT

    +
  • +
  • 在磁盘上存储索引

    +
      +
    • 在每次应用重启时重建索引可能需要一段时间
    • +
    • 大型索引可能不适合放在内存中
    • +
    +
  • +
  • 尝试用内存和 CPU 高效的数据格式来存储文档 ID 集合

    + +
  • +
  • 支持对多个文档字段进行索引

    +
  • +
  • 按相关性对结果进行排序

    +
  • +
+ +
+ + + + + + + +
+ + + + + + +
+ +
+ + + +
+ + + + + + + + +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2021/build-a-full-text-search-engine/text-analysis.png b/2021/build-a-full-text-search-engine/text-analysis.png new file mode 100644 index 0000000000..d9afb9f287 Binary files /dev/null and b/2021/build-a-full-text-search-engine/text-analysis.png differ diff --git a/2021/build-a-full-text-search-engine/venn.png b/2021/build-a-full-text-search-engine/venn.png new file mode 100644 index 0000000000..a7943e3d87 Binary files /dev/null and b/2021/build-a-full-text-search-engine/venn.png differ diff --git a/2021/cache-common-abnormal/1.png b/2021/cache-common-abnormal/1.png new file mode 100644 index 0000000000..50fae84c85 Binary files /dev/null and b/2021/cache-common-abnormal/1.png differ diff --git a/2021/cache-common-abnormal/index.html b/2021/cache-common-abnormal/index.html new file mode 100644 index 0000000000..2977fc554e --- /dev/null +++ b/2021/cache-common-abnormal/index.html @@ -0,0 +1,489 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Redis 缓存常见异常处理 | 贾攀的流水账 + + + + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

贾攀的流水账

+ +
+

Panmax's Blog

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+ + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ Redis 缓存常见异常处理 +

+ + +
+ + + + +
+ + +

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
缓存不一致
先删除缓存,再更新数据库
问题: A 删除缓存后,更新 db 前,B 查询数据,缓存中没有到 db 中读到了旧数据,将旧数据设置到了缓存中
解决:在 A 更新完数据库值以后,让它先 sleep 一小段时间,再进行一次缓存删除操作。(延迟双删)
先更新数据库值,再删除缓存值
问题:A 更新完 db 还没删除缓存前,B 查询数据命中就缓存,获取到旧数据,这个问题无解,但对业务影响较小。
推荐这种方式
先删除缓存值再更新数据库,有可能导致请求因缓存缺失而访问数据库,给数据库带来压力
如果业务应用中读取数据库和写缓存的时间不好估算,那么,延迟双删中的等待时间就不好设置。

缓存血崩
原因
大量数据同时过期
缓存实例宕机
应对方案
给过期时间加随机数
服务降级、熔断、限流
主从集群

缓存击穿
原因:热点数据过期
应对方案
不给热点数据设置过期时间
将数据提升为本地缓存

缓存穿透
原因:缓存和数据库中都没有要访问的数据
误删数据
恶意攻击
应对方案
缓存空值或缺省值
使用布隆过滤器
前端合法性检查
+ +
+ + + + + + + +
+ + + + + + +
+ +
+ + + +
+ + + + + + + + +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2021/cdn-question/index.html b/2021/cdn-question/index.html new file mode 100644 index 0000000000..6b45a92103 --- /dev/null +++ b/2021/cdn-question/index.html @@ -0,0 +1,532 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 关于 CDN 的灵魂 16 问 | 贾攀的流水账 + + + + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

贾攀的流水账

+ +
+

Panmax's Blog

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+ + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ 关于 CDN 的灵魂 16 问 +

+ + +
+ + + + +
+ + +

如果你把你的站点部署在 CDN 的后面,网站的 IP 地址是 CDN 还是后端服务器?

是 CDN 的 IP

+

所有请求都会发到 CDN,然后 CDN 在需要的情况下向后端服务器发出请求。

+

CDN 是否只有一个数据中心用于缓存内容?

不是

+

CDN 会将内容缓存在不同地区的多个服务器上,这样你的用户无论在哪里都能迅速得到响应。

+

CDN 只会缓存 HTTP 响应的 body(比如图片)吗?

不是

+

它可以缓存整个 HTTP 响应,包括状态码和响应头。

+

所以,举例来说,如果你的服务器不小心返回了 404,CDN 将这个响应进行了缓存,那么即使服务器已经恢复,你的网站仍然可能是 404。

+

如果你不小心缓存了错误的内容,是否能清理掉?

是的

+

CDN 提供方通常有清除缓存的方法。不过有时需要几分钟才能完成(CDN 可能要去告诉世界各地的数百台服务器来清理它们的缓存)。

+

你可以选择从缓存中只删除特定文件或者删除所有缓存的文件。

+

CDN 如何知道它应该把 HTTP 响应放在缓存中?

依赖于客户端的请求

+

当 CDN 收到一个资源请求时,它会从你的服务器上请求资源,然后会把资源放在它的缓存中,这样下次就不用再到你的服务器上请求了。

+

CDN 可以缓存任何类型的 HTTP 的响应吗?

是的

+

如果你要求 CDN 进行缓存,它通常可以缓存任何你想要的 HTTP 响应,比如可以将响应头设置为: Cache-Control: public; max-age=3600

+

不过大多数 CDN 会对缓存内容的大小进行限制,所以你可能无法缓存一个电影。

+

CDN 是否可以在你的服务器宕机的情况下继续为你的站点提供服务?

也许

+

即使你的服务器没有运行,CDN 也可以继续提供缓存页面。

+

但是如果你告诉它只缓存一定时间(比如 1 小时),内容可能会在一段时间后过期,无法访问。而如果内容根本没有被缓存,CDN 也帮不了你。

+

如果你在 CDN 后面的网站使用 TLS,CDN 能读取你未加密的网站流量吗?

是的

+

如果你想让CDN缓存内容,它需要能够解密和读取。

+

通常人们处理这个问题的方法是,只把静态内容(如 CSS/JS/图片)放在 CDN 后面的域名上,而使用一个单独的域名来处理带有用户数据的请求。例如,https://github.githubassets.com/ 在 CDN 后面,但https://github.com 不是。

+

CDN 总是对资源进行缓存吗?

不是

+

如果你想,可以配置你的 CDN 不进行缓存,只是代理每个请求到你的后端服务器。

+

是否可以判断出某个网站使用了 CDN?

是的

+

你通常可以从 header 中找出答案:运行curl -I https://jiapan.me,看看我使用的是什么 CDN。

+

是否能判断出你收到的是一个被缓存的响应?

是的

+

CDN 通常会设置一个响应头,比如 x-cache: HIT,你可以用它来判断是缓存命中还是缓存失效。

+

及时没有缓存,CDN 是否可以使请求更快?

是的

+
    +
  1. CDN通常可以在离客户端更近的地方终止 TLS,这意味着 TLS 握手可以快很多。如果你的后端服务器离客户端很远,这可以节省一秒左右的时间。这样做的原因是,它经常会与后端服务器保持一个开放的 TLS 连接,所以它不必每次都重新建立一个新的连接。
  2. +
  3. 它可能比客户机有更快的路由连接到你的后端服务器。
  4. +
  5. CDN还可以通过更多的方式来提高性能!
  6. +
+

如果你的站点只支持 HTTP/1.1,CDN 可以接收 HTTP/2.0 的请求吗?

大部分情况下可以

+

许多CDN可以透明地将 HTTP/2 请求翻译成 HTTP/1 请求到你的后端服务器,所以你可以在不做任何工作的情况下获得 HTTP/2 的很多性能优势。

+

是否可以让 CDN 对响应只缓存一段时间(如10分钟)?

是的

+

您可以通过设置 Cache-Control 响应头来实现,比如 Cache-Control: max-age=600

+

是否允许资源只被浏览器缓存而不被 CDN 缓存?

是的

+

你可以通过设置 Cache-Control: private, max-age=3600 来实现。private 意味着内容只能存储在浏览器的缓存中,而不是 CDN 的缓存中。

+

如果你用同一个 URL 请求 CDN,但 header 不同,是否会得到相同的缓存响应?

视情况而定

+

默认情况下,会得到相同的响应。但如果服务器设置了 Vary: 头,那么 CDN 将为该头的每个值存储不同的缓存值。

+

例如,Vary: Accept-Encoding 将使 CDN 同时存储压缩和非压缩版本。

+ +
+ + + + + + + +
+ + + + + + +
+ +
+ + + +
+ + + + + + + + +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2021/cloudflare-workers-static-site/1.png b/2021/cloudflare-workers-static-site/1.png new file mode 100644 index 0000000000..ce4ad910c6 Binary files /dev/null and b/2021/cloudflare-workers-static-site/1.png differ diff --git a/2021/cloudflare-workers-static-site/2.png b/2021/cloudflare-workers-static-site/2.png new file mode 100644 index 0000000000..bfcf77029c Binary files /dev/null and b/2021/cloudflare-workers-static-site/2.png differ diff --git a/2021/cloudflare-workers-static-site/3.png b/2021/cloudflare-workers-static-site/3.png new file mode 100644 index 0000000000..0634657d7e Binary files /dev/null and b/2021/cloudflare-workers-static-site/3.png differ diff --git a/2021/cloudflare-workers-static-site/4.png b/2021/cloudflare-workers-static-site/4.png new file mode 100644 index 0000000000..5273dfa105 Binary files /dev/null and b/2021/cloudflare-workers-static-site/4.png differ diff --git a/2021/cloudflare-workers-static-site/5.png b/2021/cloudflare-workers-static-site/5.png new file mode 100644 index 0000000000..a1e097941c Binary files /dev/null and b/2021/cloudflare-workers-static-site/5.png differ diff --git a/2021/cloudflare-workers-static-site/6.png b/2021/cloudflare-workers-static-site/6.png new file mode 100644 index 0000000000..05e1710cce Binary files /dev/null and b/2021/cloudflare-workers-static-site/6.png differ diff --git a/2021/cloudflare-workers-static-site/7.png b/2021/cloudflare-workers-static-site/7.png new file mode 100644 index 0000000000..02e63e106d Binary files /dev/null and b/2021/cloudflare-workers-static-site/7.png differ diff --git a/2021/cloudflare-workers-static-site/8.png b/2021/cloudflare-workers-static-site/8.png new file mode 100644 index 0000000000..3b78eb959b Binary files /dev/null and b/2021/cloudflare-workers-static-site/8.png differ diff --git a/2021/cloudflare-workers-static-site/index.html b/2021/cloudflare-workers-static-site/index.html new file mode 100644 index 0000000000..e73473fb63 --- /dev/null +++ b/2021/cloudflare-workers-static-site/index.html @@ -0,0 +1,566 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 利用 Cloudflare Workers 托管静态站点 | 贾攀的流水账 + + + + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

贾攀的流水账

+ +
+

Panmax's Blog

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+ + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ 利用 Cloudflare Workers 托管静态站点 +

+ + +
+ + + + +
+ + +

当前部署方案的弊端

我们在选择静态站点(如博客、技术文档等)部署方案时会考虑以下几种情况:

+
    +
  • 访问速度
  • +
  • 绑定自定义域名
  • +
  • 便于部署
  • +
  • 费用
  • +
  • 自动配置 https
  • +
+

我目前使用的就是静态博客,托管在了 3 个地方,而且各有一些弊端:

+
    +
  1. Github Pages:国内的访问速度一般
  2. +
  3. 七牛云:会收取少量费用、无法绑定未在国内备案的域名、需要手动配置 https 证书
  4. +
  5. VPS + Cloudflare CDN:需提前购买 VPS、配置 Nginx,上手难度略大
  6. +
+

今天我们就利用 Cloudfalre Works 来部署一个满足上边所有条件的博客。

+

Cloudflare CDN

在使用 VPS + Cloudflare CDN 方案时,我们将博客的静态文件放在 VPS 上,并通过 Nginx 搭起一个静态站点,然后前置一个 Cloudflare CDN 来做静态资源加速和 https 的处理,即我们的 VPS 来作为静态文件的源站。

+

+

Cloudflare Workers

使用 Cloudflare Workers 方案可以无需准备 VPS。

+

+

Cloudflare Workers 本质上是一个边缘计算服务,举几个例子:

+
    +
  • 将不同类型的请求按路线发送到不同的源服务器。
  • +
  • 在边缘网络展开HTML模板,以降低原始带宽成本。
  • +
  • 将访问控制应用于缓存的内容。
  • +
  • 将一小部分用户重定向到开发用服务器。
  • +
  • 在两个完全不同的后端之间执行A / B测试。
  • +
  • 构建完全依赖Web API的“无服务器”应用程序。
  • +
  • ……
  • +
+

了解更多可以参考:https://blog.cloudflare.com/zh-cn/cloudflare-workers-unleashed-zh-cn/

+

生成静态站点

目前,生成静态站点的方案有很多,比如 HugoHexoJekyll 等,下边我以 Hugo 为例来生成一个站点,其他方案可以参考对应的官方文档。

+

安装 hugo 命令

1
2
3
4
brew install hugo

# 验证 hugo 是否安装成功
hugo version
+

生成新站点

1
hugo new site quickstart
+

执行这个命令后,会在执行的目录下创建出一个名为 quickstart 的目录,里边就是我们新站点的内容。

+

修改站点主题

下载主题:

+
1
2
3
cd quickstart
git init
git submodule add https://github.com/theNewDynamic/gohugo-theme-ananke.git themes/ananke
+

配置主题:

+
1
echo theme = \"ananke\" >> config.toml
+

创建一篇文章

1
hugo new posts/my-first-post.md
+

修改 content/posts 下的文章源文件,将 draft 改为 false,就可以正常发布了,正文随便点什么。

+
1
2
3
4
5
6
7
---
title: "My First Post"
date: 2021-10-04T21:45:54+08:00
draft: false
---

这是我们的第一篇文章
+

浏览效果

1
hugo server -D
+

+

此时我们可以访问:http://localhost:1313/ 看下效果。

+

生成静态文件

使用 hugo -D 命令生成静态文件用来发布到 Cloudflare Workers 上,hugo 生成的静态文件在项目目录的 public 下。

+

发布到 Cloudflare Workers

发布前需要先注册自己的 Cloudflare 账号,开通 Workers 服务,在 Workers 页面右侧可以修改自己的子域名,比如我的子域名为 panmax.workers.dev,即我发布的服务都是以 panmax.workers.dev 结尾,比如:https://hugo.panmax.workers.dev/

+

安装 wrangler 命令

wrangler 是 Cloudflare workers 为开发人员提供的 CLI 工具。使用 npm 进行安装:

+
1
npm i @cloudflare/wrangler -g
+

如果提示 node 版本太低,可以通过 nvm 来切换版本:

+
1
brew install nvmnvm install 12
+

初始化 cloudflare workers 项目

在刚才生成的 quickstart 目录下执行以下命令来初始化 cloudflare workers 项目,这个命令会在当前目录下生成 wrangler.toml 文件和 workers-site 目录。

+
1
wrangler init --site hugo
+

编辑 wrangler.toml

将 wrangler.toml 中的 bucket 改为我们静态目录的路径:

+
1
2
# 根据你的项目,将 bucket 改成生成静态文件的目录
site = {bucket = "./public",entry-point = "workers-site"}
+

其他参数暂时无需修改。

+

用户登录

使用 wrangler login 来完成登录,在弹出的页面中点击 Allow 即可。当命令行打印出 「✨ Successfully configured. 」就说明我们登录成功了,会在 home 下生成 .wrangler 目录,里边记录了我们的用户信息。

+
+

这个操作只需进行一次,后续发布时就不用再执行了。

+
+

发布

最后,使用 wrangler publish 即可将我们的静态站点发布到 cloudflare workers 上了。同时还会将我们站点的地址打印出来:

+

+

用浏览器访问这个地址就能看到效果了:

+

+

配置自定义域名

如果你在 cloudflare 上托管了自己的域名,还可以将自己的域名映射到 workers 上。

+

配置 CNAME

在你的 DNS 配置中新增一条 CNAME 规则,名称是你想关联的子域名,目标为 workers 为你提供给的域名。

+

比如,我要将 hugo.jiapan.me 关联到刚才发布的站点上,此时我的名称填写 hugo,目标填写 hugo.panmax.workers.dev

+

+

关联 workers

在域名管理页面上边的菜单中点击 workers,点击「添加路由」,还是以我刚才配置的域名为例,路由填写 hugo.jiapan.me/* ,Workers 选择 hugo,点击保存。

+

+

之后我们就可以使用自定义域名来访问我们的站点了:

+

+ +
+ + + + + + + +
+ + + + + + +
+ +
+ + + +
+ + + + + + + + +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2021/config-file-bug-review/1.png b/2021/config-file-bug-review/1.png new file mode 100644 index 0000000000..878e3fbb41 Binary files /dev/null and b/2021/config-file-bug-review/1.png differ diff --git a/2021/config-file-bug-review/2.png b/2021/config-file-bug-review/2.png new file mode 100644 index 0000000000..b9e35b6f7e Binary files /dev/null and b/2021/config-file-bug-review/2.png differ diff --git a/2021/config-file-bug-review/index.html b/2021/config-file-bug-review/index.html new file mode 100644 index 0000000000..559364a4ba --- /dev/null +++ b/2021/config-file-bug-review/index.html @@ -0,0 +1,520 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 配置不规范导致的 bug 复盘 | 贾攀的流水账 + + + + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

贾攀的流水账

+ +
+

Panmax's Blog

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+ + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ 配置不规范导致的 bug 复盘 +

+ + +
+ + + + +
+ + +

昨天晚上我们的推荐服务出现故障,排查到很晚。影响了50多个主播和用户的曝光卡的使用效果,虽然没有产生特别大的事故,但我觉得自己还是有必要做个复盘,毕竟有自己做的不好的地方。

+

时间轴

    +
  • 大概21:20分,服务开始有大量错误报警,推荐帧 v2接口和附近动态的 rpc 请求全部进入降级状态。

    +
  • +
  • 22:00 报错突然降成 0

    +
  • +
  • 22:10 又开始报错,23:10 分报错消失
  • +
+

处理经过

回顾一下我的排查过程,在报警前几分钟,我更新了本周要扶持的荣耀主播名单,这个名单是一周一换,每周二更新,正常情况下运营会在白天把名单给我,但今天运营晚上19点才给我,当时我在吃饭,吃完饭后因为处理另一个问题就把改配置的事给忘了,晚上到家后才配置上。

+

报错时没有想到会是配置的问题,因为这个配置我已经配置好多周了,都没有出过问题,而且是配置完后过了几分钟才开始报错的,看日志报的都是空指针异常,但是没有具体定位是那一行,起初以为是 live 对象缺少字段或者本身为空,加日志看了下并没有问题。

+

大概21:53,我想到有没有可能是配置的问题,所以把新增的配置删掉,发现问题并没有解决,到了22:00 的时候突然不报错了,这个时候因为是个整点时间,我怀疑是不是某个活动或者某个有脏数据的主播下播了?心想明天到了公司查下这个时间点下播的主播找找原因。

+

因为我前几分钟把荣耀主播的名单下掉了,这个名单需要在凌晨4点生效,所以我看既然没问题了就把配置恢复吧,恢复完配置文件几分钟后,刚要去洗漱就又开始报警。我和另一个同事决定继续加日志排查,一直搞到23点也没发现代码有问题,这时候我决定再下线刚才的配置,下完后没有恢复,不过到了23:10突然降成了0。又等到23:30发现没有报错我才去睡的,因为经历了这么长时间的惊心动魄,凌晨3点才睡着。

+

为了验证是不是配置文件导致,第二天早上7点我重新把这份配置上去,7:10又开始报错。删除配置后,7:20恢复。

+

故障分析

为什么会在整10分报错?

+

ai 所使用的配置文件在 hbase 中,为了提升效率会定期同步到 redis 一份,resource 类的配置文件我设置的是10分钟同步一次,所以会出现当有配置变更时,整10分钟才会生效。

+

为什么要10分钟才加载一次呢,因为我并不知道业务实际会用到哪些,索性把库中所有的配置都 load 了一遍,这会导致 redis 的抖动,不易太频繁。如下图所示

+

+

后边我改成了,当下游调用配置时,我会记录下来调用配置的 key,刷新配置时只刷新在用的配置,这样可以做到秒级或者分钟级刷新。

+

优化后效果如下:

+

+

配置错在了哪里?

+

配置中存在重复项,代码中解析这个配置后会转成一个 map,用到了 lambda 表达式

+
1
.collect(Collectors.toMap(Pair::getKey, Pair::getValue);
+

可以理解为,key 是主播的 ID,value 是要扶持的量,这段代码当有重复 key 时会报错,解决方法是传入第3个参数,告诉程序当 key 冲突时的 merge 逻辑,因为我这里不关心太具体保留哪个 value,可以简单实现:

+
1
.collect(Collectors.toMap(Pair::getKey, Pair::getValue,(value1, value2) -> value2));
+

后续优化

    +
  • 避免7点后(非上线时间)更改线上配置
  • +
  • 运营配置尽量做到 admin 和自动化
  • +
  • 服务出故障后优先想想最近有哪些改动(即使只修改了配置文件)
  • +
  • 配置刷新频率不宜过低
  • +
+ +
+ + + + + + + +
+ + + + + + +
+ +
+ + + +
+ + + + + + + + +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2021/docker-compose-hbase/index.html b/2021/docker-compose-hbase/index.html new file mode 100644 index 0000000000..d5c6b2a91b --- /dev/null +++ b/2021/docker-compose-hbase/index.html @@ -0,0 +1,501 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 利用 docker-compose,搭建本地 HBase 集群 | 贾攀的流水账 + + + + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

贾攀的流水账

+ +
+

Panmax's Blog

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+ + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ 利用 docker-compose,搭建本地 HBase 集群 +

+ + +
+ + + + +
+ + +

最近在重构公司的直播推荐服务,特征数据的存储使用的是 Hbase,但有个问题是我们的开发环境并没有搭建 HBase 集群,开发环境和生产环境网络又不通,这样本地调试就很不方便,所以我需要在本地搭一个 HBase 集群。

+

第一时间想到的就是使用 docker-compose

+

牛顿老师说过,轮子还是别人的圆,于是我在 gayhub 上找到了这个轮子:https://github.com/big-data-europe/docker-hbase

+

但是在搭建过程中发现了它的问题,有两个重要的端口没有在 docker-compose.yml 文件中开放,并且说明中没有提示要修改 hosts,再看了下这个仓库的更新时间,已经有 3 年没有更新,所以我也就不提 pr 了,而是将仓库进行了 fork 并修改了源码,可以直接 clone 我的仓库来搭建,流程如下:

+

1. 将代码 clone 到本地

1
2
git clone git@github.com:Panmax/docker-hbase.git
cd docker-hbase
+

2. /etc/hosts 中添加以下两项

1
2
0.0.0.0 hbase-master
0.0.0.0 hbase-region
+

3. 启动

1
docker-compose -f docker-compose-distributed-local.yml up
+

等待所有服务完全启动后,就可以让我们的程序通过监听在本地 2181 端口的 zookeeper 去发现并访问 hbase 了。

+

Hbase 数据录入

+

为了验证代码逻辑,我还需要写一些数据到 hbase 中,操作如下:

+

1. 进入容器

1
docker exec -it hbase-master bash
+

2. 进入 hbase 安装目录

1
cd /opt/hbase-1.2.6/
+

3. 运行 hbase shell

1
bin/hbase shell
+

然后就可以使用SQL语句进行操作了,例如:

+
1
2
3
4
5
6
7
8
9
> create 'mods_model_storage', 'f'
> put 'mods_model_storage','model1','f:model', 'model content1'
> put 'mods_model_storage','model2','f:model', 'model content2'

> scan 'mods_model_storage'
ROW COLUMN+CELL
model1 column=f:model, timestamp=1617605399565, value=model content1
model2 column=f:model, timestamp=1617605400576, value=model content2
2 row(s) in 0.0330 seconds
+ +
+ + + + + + + +
+ + + + + + +
+ +
+ + + +
+ + + + + + + + +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2021/domestic-server-speed-github/index.html b/2021/domestic-server-speed-github/index.html new file mode 100644 index 0000000000..c9a6c508e9 --- /dev/null +++ b/2021/domestic-server-speed-github/index.html @@ -0,0 +1,501 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 国内服务器访问 github 加速 | 贾攀的流水账 + + + + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

贾攀的流水账

+ +
+

Panmax's Blog

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+ + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ 国内服务器访问 github 加速 +

+ + +
+ + + + +
+ + +

众所周知的原因,国内访问 github 的速度非常受限,在个人电脑上还可以挂个代理之类的来提速,但是如果是在服务器上操作的话,配代理就没那么方便了。

+

前几天帮朋友在他的服务器上部署一个 github 上的开源项目,clone 的速度真的感人。

+
+ +

4k 左右的下载速度。

+

可以通过阿里提供的代理来解决,只需把 rep 地址中的 github.com 替换为 github.com.cnpmjs.org/ 就可以了。

+

比如,之前的地址是:https://github.com/gin-gonic/gin,替换后为:https://github.com.cnpmjs.org/gin-gonic/gin

+

效果如下:

+
+ +

虽然没有快的飞起,但是已经相当不错了。

+
+

P.S. 域名后边的 cnpmjs.org 这个地址是提供 npm 加速的,前端童鞋可以通过 cnpm 来实现前端构建的加速。

+ +
+ + + + + + + +
+ + + + + + +
+ +
+ + + +
+ + + + + + + + +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2021/domestic-server-speed-github/quick.png b/2021/domestic-server-speed-github/quick.png new file mode 100644 index 0000000000..fa783c6a95 Binary files /dev/null and b/2021/domestic-server-speed-github/quick.png differ diff --git a/2021/domestic-server-speed-github/slow.png b/2021/domestic-server-speed-github/slow.png new file mode 100644 index 0000000000..9b09070c98 Binary files /dev/null and b/2021/domestic-server-speed-github/slow.png differ diff --git a/2021/effective-go-read/index.html b/2021/effective-go-read/index.html new file mode 100644 index 0000000000..9eac703141 --- /dev/null +++ b/2021/effective-go-read/index.html @@ -0,0 +1,629 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Effective Go 查漏补缺 | 贾攀的流水账 + + + + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

贾攀的流水账

+ +
+

Panmax's Blog

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+ + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ Effective Go 查漏补缺 +

+ + +
+ + + + +
+ + +
+

前几天把 Effective Go 这本小书读了一下,里边有些比较生疏或者实用的知识点,在此记录。

+
+

命名

包应当以小写的单个单词来命名,且不应使用下划线或驼峰记法。

+

另一个约定就是包名应为其源码目录的基本名称。在 src/pkg/encoding/base64 中的包应作为 “encoding/base64” 导入,其包名应为 base64, 而非 encoding_base64encodingBase64

+
+

我们的代码中给包起别名时也应该遵循这个规则,即:livedomain “gitlab.xxx.com/backend/xxx-live/proto”,不应该是 live_domain

+
+

长命名并不会使其更具可读性。一份有用的说明文档通常比额外的长名更有价值。

+
+

避免 Java 那样的长命名

+
+

若你有个名为 owner (小写,未导出)的字段,其获取器应当名为 Owner(大写,可导出)而非 GetOwner。大写字母即为可导出的这种规定为区分方法和字段提供了便利。 若要提供设置器方法,SetOwner 是个不错的选择。两个命名看起来都很合理:

+
1
2
3
4
owner := obj.Owner()
if owner != user {
obj.SetOwner(user)
}
+
+

Go 中的 Set 方法无需以 Set 开头,只需实现一个大写开头的方法就可以了。(不过大部分常见下,变量可以直接用导出的)

+
+

按照约定,只包含一个方法的接口应当以该方法的名称加上 er 后缀来命名,如 Reader、Writer、 Formatter、CloseNotifier 等。

+
+

自己实现接口时也尽量遵循这个规范。

+
+

Go 中约定使用驼峰记法 MixedCaps 或 mixedCaps。

+
+

即便是常量也不例外,即:不应该写为 LIVE_USER_TABLE 而应该是 LiveUserTable。

+
+

分号

若在新行前的最后一个标记为标识符(包括 int 和 float64 这类的单词)、数值或字符串常量之类的基本字面或以下标记之一,词法分析器会使用一条简单的规则来自动插入分号,因此因此源码中基本就不用分号了。

+
1
break continue fallthrough return ++ -- ) }
+
+

所以

+
+
1
2
if a == 1 && 
b == 2
+
+

可以编译通过

+
+
1
2
if a == 1 
&& b == 2
+
+

不能编译通过,因为词法分析器会自动在 if a == 1 后边插入分号。

+
+

通常Go程序只在诸如 for 循环子句这样的地方使用分号,以此来将初始化器、条件及增量元素分开。如果你在一行中写多个语句,也需要用分号隔开。

+
+

for i := 0; i <= 10; i++

+
+

无论如何,你都不应将一个控制结构(if、for、switch 或 select)的左大括号放在下一行。如果这样做,就会在大括号前面插入一个分号,这可能引起不需要的效果。 你应该这样写

+
1
2
3
if i < f() {
g()
}
+

控制结构

Go 不再使用 do 或 while 循环,只有一个更通用的 for;switch 要更灵活一点;if 和 switch 像 for 一样可接受可选的初始化语句; 此外,还有一个包含类型选择和多路通信复用器的新控制结构:select。

+

Go 的 for 循环类似于 C,但却不尽相同。它统一了 for 和 while,不再有 do-while 了。它有三种形式,但只有一种需要分号。

+
1
2
3
4
5
6
7
8
// Like a C for
for init; condition; post { }

// Like a C while
for condition { }

// Like a C for(;;)
for { }
+
+

体现出 go 的简洁,不用费心的去考虑应该用 for 还是while 或者 do while。

+
+

由于 if 和 switch 可接受初始化语句, 因此用它们来设置局部变量十分常见。

+
1
2
3
4
if err := file.Chmod(0664); err != nil {
log.Print(err)
return err
}
+

switch 并不会自动下溯,但 case 可通过逗号分隔来列举相同的处理条件。

+
1
2
3
4
5
6
7
func shouldEscape(c byte) bool {
switch c {
case ' ', '?', '&', '=', '#', '+', '%':
return true
}
return false
}
+
+

不用担心因为漏写 break 而导致的bug,case 中支持多个判断条件也很实用。

+
+

尽管它们在 Go 中的用法和其它类 C 语言差不多,但 break 语句可以使 switch 提前终止。不仅是 switch, 有时候也必须打破层层的循环。在 Go 中,我们只需将标签放置到循环外,然后 “蹦” 到那里即可

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
func main() {
m, n := 2,2
loop:
for i := 0; i < n; i++ {
for j:=0; j< m; j++ {
if j ==1 {
break loop
}
fmt.Println(i,j)
}
}
fmt.Println("done")
}
// output:
// 0 0
// done
+
+

这种用法很少使用,我之前甚至不知道有这种 label break 的用法,类似于其他语言中的 goto。

+
+

switch 也可用于判断接口变量的动态类型。

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
var t interface{}
t = functionOfSomeType()
switch t := t.(type) {
default:
fmt.Printf("unexpected type %T", t) // %T 输出 t 是什么类型
case bool:
fmt.Printf("boolean %t\n", t) // t 是 bool 类型
case int:
fmt.Printf("integer %d\n", t) // t 是 int 类型
case *bool:
fmt.Printf("pointer to boolean %t\n", *t) // t 是 *bool 类型
case *int:
fmt.Printf("pointer to integer %d\n", *t) // t 是 *int 类型
}
+
+

我们的工具库中也有这样的用法,比如将一个 interface{} 类型转为 int64类型,代码如下:

+
+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
func Int64(num interface{}, defaultValue ...int64) int64 {

var rsp int64
var err error

switch t := num.(type) {
case string:
rsp, err = strconv.ParseInt(t, 10, 64)
case int:
rsp = int64(t)
case int8:
rsp = int64(t)
case int16:
rsp = int64(t)
case int32:
rsp = int64(t)
case int64:
rsp = t
default:
}
if err != nil {
if len(defaultValue) > 0 {
return defaultValue[0]
}
}
return rsp
}
+

函数

Go 与众不同的特性之一就是函数和方法可返回多个值。这种形式可以改善 C 中一些笨拙的习惯: 将错误值返回(例如用 -1 表示 EOF)和修改通过地址传入的实参。

+
+

Java 中由于也不支持多返回值,也经常将引用传入一个方法,方法执行完后根据传入引用中的数据进行后续处理,这种方法通常被称为有副作用的方法。

+
+

Go 函数的返回值或结果 “形参” 可被命名,并作为常规变量使用,就像传入的形参一样。 命名后,一旦该函数开始执行,它们就会被初始化为与其类型相应的零值; 若该函数执行了一条不带实参的 return 语句,则结果形参的当前值将被返回。

+

此名称不是强制性的,但它们能使代码更加简短清晰:它们就是文档。若我们命名了 nextInt 的结果,那么它返回的 int 就值如其意了。

+
+

避免在函数签名上命名返回值变量,除非无法从上下中判断返回值的含义用作文档用途,或者希望在 defer 中改变变量值

+
+

Go 的 defer 语句用于预设一个函数调用(即推迟执行函数),该函数会在执行 defer 的函数返回之前立即执行。它显得非比寻常, 但却是处理一些事情的有效方式,例如无论以何种路径返回,都必须释放资源的函数。 典型的例子就是解锁互斥和关闭文件。

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// Contents returns the file's contents as a string.
func Contents(filename string) (string, error) {
f, err := os.Open(filename)
if err != nil {
return "", err
}
defer f.Close() // f.Close will run when we're finished.

var result []byte
buf := make([]byte, 100)
for {
n, err := f.Read(buf[0:])
result = append(result, buf[0:n]...) // append is discussed later.
if err != nil {
if err == io.EOF {
break
}
return "", err // f will be closed if we return here.
}
}
return string(result), nil // f will be closed if we return here.
}
+
+

类似于 Java 中的 finally。

+
+

推迟诸如 Close 之类的函数调用有两点好处:

+
    +
  • 第一, 它能确保你不会忘记关闭文件。如果你以后又为该函数添加了新的返回路径时, 这种情况往往就会发生。
  • +
  • 第二,它意味着 “关闭” 离 “打开” 很近, 这总比将它放在函数结尾处要清晰明了。
  • +
+

被推迟的函数按照后进先出(LIFO)的顺序执行,我们可以充分利用这个特点,即被推迟函数的实参在 defer 执行时才会被求值。 跟踪例程可针对反跟踪例程设置实参。以下例子:

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
func trace(s string) string {
fmt.Println("entering:", s)
return s
}

func un(s string) {
fmt.Println("leaving:", s)
}

func a() {
defer un(trace("a"))
fmt.Println("in a")
}

func b() {
defer un(trace("b"))
fmt.Println("in b")
a()
}

func main() {
b()
}
+

输出:

+
1
2
3
4
5
6
entering: b
in b
entering: a
in a
leaving: a
leaving: b
+

数据

new 是个用来分配内存的内建函数, 但与其它语言中的同名函数不同,它不会初始化内存,只会将内存置零。 也就是说,new(T) 会为类型为 T 的新项分配已置零的内存空间, 并返回它的地址,也就是一个类型为 *T 的值。用 Go 的术语来说,它返回一个指针, 该指针指向新分配的,类型为 T 的零值。

+

表达式 new(File)&File{} 是等价的。

+
+

开发时更常用到的是 &File{} 这种形式,因为可以同时对成员进行初始化。

+
+

复合字面的字段必须按顺序全部列出。但如果以 字段: 值 对的形式明确地标出元素,初始化字段时就可以按任何顺序出现,未给出的字段值将赋予零值。

+

内建函数 make(T, args) 的目的不同于 new(T)。它只用于创建切片、映射和信道,并返回类型为 T(而非 *T)的一个已初始化 (而非置零)的值。 出现这种用差异的原因在于,这三种类型本质上为引用数据类型,它们在使用前必须初始化。

+

make 只适用于映射、切片和信道且不返回指针。若要获得明确的指针, 请使用 new 分配内存。

+
+

这就是 slice, map, channel 需要使用 make 进行初始化的原因。

+
+

映射可使用一般的复合字面语法进行构建,其键-值对使用冒号分隔,因此可在初始化时很容易地构建它们。

+
1
2
3
4
5
6
7
var timeZone = map[string]int{
"UTC": 0*60*60,
"EST": -5*60*60,
"CST": -6*60*60,
"MST": -7*60*60,
"PST": -8*60*60,
}
+
+

map 可以在初始化时同时赋值,很方便。

+
+

集合可实现成一个值类型为 bool 的映射。将该映射中的项置为 true 可将该值放入集合中,此后通过简单的索引操作即可判断是否存在。

+
1
2
3
4
5
6
7
8
9
attended := map[string]bool{
"Ann": true,
"Joe": true,
...
}

if attended[person] { // will be false if person is not in the map
fmt.Println(person, "was at the meeting")
}
+
+

Go 中没有 Set,可以用这种方法代替,有些人习惯将 map 的 value 值声明为 interface 类型,我个人不是很喜欢,bool 更方便使用一些。

+
+

在使用 map 时,有时你需要区分某项是不存在还是其值为零值。如对于一个值本应为零的 “UTC” 条目,也可能是由于不存在该项而得到零值。你可以使用多重赋值的形式来分辨这种情况。

+
1
2
3
var seconds int
var ok bool
seconds, ok = timeZone[tz]
+

在下面的例子中,若 tz 存在, seconds 就会被赋予适当的值,且 ok 会被置为 true; 若不存在,seconds 则会被置为零,而 ok 会被置为 false。

+
1
2
3
4
5
6
7
func offset(tz string) int {
if seconds, ok := timeZone[tz]; ok {
return seconds
}
log.Println("unknown time zone:", tz)
return 0
}
+

若仅需判断映射中是否存在某项而不关心实际的值,可使用 空白标识符 (_)来代替该值的一般变量。

+
1
_, present := timeZone[tz]
+

要删除映射中的某项,可使用内建函数 delete,它以映射及要被删除的键为实参。 即便对应的键不在该映射中,此操作也是安全的。

+
1
delete(timeZone, "PDT")  // Now on Standard Time
+

当打印结构体时,改进的格式 %+v 会为结构体的每个字段添上字段名,而另一种格式 %#v 将完全按照 Go 的语法打印值。

+

初始化

常量只能是数字、字符(符文)、字符串或布尔值。由于编译时的限制, 定义它们的表达式必须也是可被编译器求值的常量表达式。例如 1<<3 就是一个常量表达式,而 math.Sin(math.Pi/4) 则不是,因为对 math.Sin 的函数调用在运行时才会发生。

+

在 Go 中,枚举常量使用枚举器 iota 创建。由于 iota 可为表达式的一部分,而表达式可以被隐式地重复,这样也就更容易构建复杂的值的集合了。

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
type ByteSize float64

const (
// 通过赋予空白标识符来忽略第一个值
_ = iota // ignore first value by assigning to blank identifier
KB ByteSize = 1 << (10 * iota)
MB
GB
TB
PB
EB
ZB
YB
)
+

方法

以指针或值为接收者的区别在于:值方法可通过指针和值调用, 而指针方法只能通过指针来调用。

+

之所以会有这条规则是因为指针方法可以修改接收者;通过值调用它们会导致方法接收到该值的副本, 因此任何修改都将被丢弃,因此该语言不允许这种错误。不过有个方便的例外:若该值是可寻址的, 那么该语言就会自动插入取址操作符来对付一般的通过值调用的指针方法。在我们的例子中,变量 b 是可寻址的,因此我们只需通过 b.Write 来调用它的 Write 方法,编译器会将它重写为 (&b).Write。

+
+

通常我们会将方法写为指针接收者,这种情况下,即便是用值调用这个方法,编辑器会自动帮我们改为指针调用。

+
+

并发

并发是用可独立执行的组件构造程序的方法,而并行则是为了效率在多 CPU 上平行地进行计算。

+
+

并发是两个队列交替使用一台咖啡机,并行是两个队列同时使用两台咖啡机

+
+

错误

若调用者关心错误的完整细节,可使用类型选择或者类型断言来查看特定错误,并抽取其细节。

+
1
2
3
4
5
6
7
8
9
10
11
for try := 0; try < 2; try++ {
file, err = os.Create(filename)
if err == nil {
return
}
if e, ok := err.(*os.PathError); ok && e.Err == syscall.ENOSPC {
deleteTempFiles() // Recover some space.
continue
}
return
}
+

panic 被调用后(包括不明确的运行时错误,例如切片检索越界或类型断言失败),程序将立刻终止当前函数的执行,并开始回溯 Go 程的栈,运行任何被推迟的函数。 若回溯到达 Go 程栈的顶端,程序就会终止。不过我们可以用内建的 recover 函数来重新或来取回 Go 程的控制权限并使其恢复正常执行。

+

调用 recover 将停止回溯过程,并返回传入 panic 的实参。 由于在回溯时只有被推迟函数中的代码在运行,因此 recover 只能在被推迟的函数中才有效。

+

recover 的一个应用就是在服务器中终止失败的 Go 程而无需杀死其它正在执行的 Go 程。

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
func server(workChan <-chan *Work) {
for work := range workChan {
go safelyDo(work)
}
}

func safelyDo(work *Work) {
defer func() {
if err := recover(); err != nil {
log.Println("work failed:", err)
}
}()
do(work)
}
+ +
+ + + + + + + +
+ + + + + + +
+ +
+ + + +
+ + + + + + + + +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2021/event-loop-questions/index.html b/2021/event-loop-questions/index.html new file mode 100644 index 0000000000..b6cd43d3a6 --- /dev/null +++ b/2021/event-loop-questions/index.html @@ -0,0 +1,523 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 关于事件循环的 15 个问题 | 贾攀的流水账 + + + + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

贾攀的流水账

+ +
+

Panmax's Blog

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+ + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ 关于事件循环的 15 个问题 +

+ + +
+ + + + +
+ + +

如果你的程序中只有一个事件循环,没有其他代码,那么是否有可能在同一时间运行两行代码?

不可能。

+

一个事件循环中的所有代码都在一个操作系统线程中运行,所以任何时刻只能有一段代码在运行。

+

在一个有事件循环的程序中,是否可以有其他线程?

是的。

+

例如,在 node.js 中,所有的 Javascript 代码都在一个线程中运行,但还有其他工作线程来处理网络请求和其他 I/O。

+

在一个事件循环中,是否由操作系统来负责调度函数执行的顺序?

不是。

+

例如,用Python的 asyncio,做调度的代码是一个 Python 程序。

+

事件循环真的是一个循环吗?(就像像 for 循环或 while 循环?)

是的。

+

通常事件循环都是以while循环的形式实现的,它看起来像这样。

+
1
2
3
4
while True:
self._run_once()
if self._stopping:
break
+

(以上是 Python 的 asyncio 事件循环的实际代码)

+

事件循环如何决定下一个函数的运行?

通过队列。

+

当函数准备好运行时,它们会被推到队列中,然后事件循环按顺序执行队列中的函数。

+

如果一个网络请求返回,并且它有一个附加的回调,该回调是否会被推送到事件循环的队列中?

是的。

+

当网络请求或其他 I/O 完成后,或者用户点击了某些东西,或者因为该函数计划在那时运行等,函数可能会被推入事件循环的队列中。

+

常规函数和异步函数一样吗?

不一样。

+

异步函数特殊之处在于,它们可以被“暂停”并在稍后的事件循环中重新启动。

+

例如,在下边这段 Javascript 代码中

+
1
2
3
4
5
async function panda() {
let x = 3;
await elephant();
let y = 4;
}
+

事件循环调度 elephant(),暂停 panda,并在 elephant() 运行完毕后调度 panda() 重新启动。普通的非 async 函数不能像这样暂停和重启。这些可以暂停和重启的异步函数的另一个名字是协程。

+

如果你要求事件循环在某个时间运行一个函数(比如 Javascript 中的setTimeout),它能保证在那个时间运行吗?

不能。

+

事件循环会尽力而为,但有时会延迟。

+

在 Javascript 中,promises、setTimeout、async/await 和回调是否都使用相同的事件循环?

是的。

+

虽然它们的语法不同,但它们是安排代码稍后运行的不同方式。

+

在下边这段代码中,是否可以让事件循环在x=3后中断,然后运行别的东西?

1
2
x = 3;
y = 4;
+

不能。

+

你需要显式让步给事件循环,让它运行一个不同的函数,例如使用 await

+

如果你运行一些CPU密集型的代码,如下,事件循环最终会中断代码吗?

1
2
3
while(true) { 
i = i * 3
}
+

不会。

+

通常,你可以通过运行一些 CPU 密集型操作来长时间阻塞事件循环。

+

如果你的Web 服务器事件循环中 CPU 的使用率到达100%,当新的 HTTP 请求进来时,是否能够立即响应?

不能。

+

如果你的事件循环的 CPU 总是很忙,那么新进来的事件就不会得到及时处理。

+

Javascript代码总是有一个事件循环吗?

是的。

+

至少在 node.js 和浏览器中是这样的,Javascript 代码总是有一个事件循环运行。

+

是否存在一个所有的事件循环都在使用标准的事件循环库?

没有。

+

在不同的编程语言中,有很多不同的事件循环实现。

+

你可以在任何编程语言中使用事件循环吗?

可以。

+

大多数编程语言并没有像 Javascript 那样的“一切都在事件循环中运行”的模式,但许多语言都有事件循环库。而且从理论上讲,即使还没有自己语言的事件循环库,你也可以编写一个。

+ +
+ + + + + + + +
+ + + + + + +
+ +
+ + + +
+ + + + + + + + +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2021/git-submodule/index.html b/2021/git-submodule/index.html new file mode 100644 index 0000000000..14dea602f9 --- /dev/null +++ b/2021/git-submodule/index.html @@ -0,0 +1,498 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + git 子模块使用 | 贾攀的流水账 + + + + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

贾攀的流水账

+ +
+

Panmax's Blog

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+ + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ git 子模块使用 +

+ + +
+ + + + +
+ + +

前几天调研了一下 hugo,准备后边把自己的博客迁移过去。hugo 教程中新增主题推荐通过 git 子模块的方式(前提是原始文件就已经在一个 git 项目下),比如我要新增一个名字叫 zen 的主题,可以通过以下命令进行安装:

+
1
git submodule add https://github.com/frjo/hugo-theme-zen.git themes/zen
+

这个命令会将主题仓库中的文件 clone 到 themes/zen 路径下,同时会在我的的仓库根路径下新建一个 .gitmodules 文件,之后 add 其他子模块时,会往这个文件中追加数据,格式如下:

+
1
2
3
4
5
6
7
8
9
10
11
12
[submodule "themes/zen"]
path = themes/zen
url = https://github.com/frjo/hugo-theme-zen.git themes/zen
[submodule "themes/cleanwhite"]
path = themes/cleanwhite
url = https://github.com/zhaohuabing/hugo-theme-cleanwhite.git
[submodule "themes/anatole"]
path = themes/anatole
url = https://github.com/lxndrblz/anatole
[submodule "themes/jane"]
path = themes/jane
url = https://github.com/xianmin/hugo-theme-jane.git
+

我们需要把 .gitmodules 文件加入到 git 的版本控制中。

+

子模块常用管理命令

更新子模块:

1
git submodule update --recursive --remote
+

在新环境中拉取所有子模块代码

首次在一个新的环境中 clone 我们的仓库后是不带子模块代码的,可以通过下边这个命令来把所有子模块代码拉下来:

+
1
git submodule update --init --recursive
+ +
+ + + + + + + +
+ + + + + + +
+ +
+ + + +
+ + + + + + + + +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2021/go-5-normally-mistakes/index.html b/2021/go-5-normally-mistakes/index.html new file mode 100644 index 0000000000..1a7da4b797 --- /dev/null +++ b/2021/go-5-normally-mistakes/index.html @@ -0,0 +1,546 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Go 中 5 个常见错误 | 贾攀的流水账 + + + + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

贾攀的流水账

+ +
+

Panmax's Blog

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+ + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ Go 中 5 个常见错误 +

+ + +
+ + + + +
+ + +

1. 循环中

使用循环时下边几个容易尝试混乱的编码方式我们要尽量避免。

+

1.1 对循环的变量进行引用

考虑到效率,在进行循环遍历过程中,迭代出的变量会赋值到同一个地址。这可能会导致无意识的错误。

+
1
2
3
4
5
6
7
8
9
in := []int{1, 2, 3}

var out []*int
for _, v := range in {
out = append(out, &v)
}

fmt.Println("Values:", *out[0], *out[1], *out[2])
fmt.Println("Addresses:", out[0], out[1], out[2])
+

以上代码得到的结果是:

+
1
2
Values: 3 3 3
Addresses: 0xc000014188 0xc000014188 0xc000014188
+

原因很容易解释:每次迭代时我们将 v 的地址追加到 out 切片中,前边提到,v 在每次遍历时为同一个变量,在输出的第二行可以看到打印出了相同的地址。

+

简单的修复方法是,将每一次的迭代出的变量复制给一个新的变量:

+
1
2
3
4
5
6
7
8
9
10
in := []int{1, 2, 3}

var out []*int
for _, v := range in {
v := v
out = append(out, &v)
}

fmt.Println("Values:", *out[0], *out[1], *out[2])
fmt.Println("Addresses:", out[0], out[1], out[2])
+

输出:

+
1
2
Values: 1 2 3
Addresses: 0xc0000b6010 0xc0000b6018 0xc0000b6020
+

同样的问题会出现在将迭代出的变量用在 Goroutine 中:

+
1
2
3
4
5
6
7
list := []int{1, 2, 3}

for _, v := range list {
go func() {
fmt.Printf("%d ", v)
}()
}
+

输出:

+
1
3 3 3
+

这个 bug 也可以使用上边提到的方法解决。(注:如果不在 Goroutine 中执行,上边的代码是没有问题的)

+

1.2 在循环中调用 WaitGroup.Wait

下边代码循环中的 group.Wait() 会被阻塞,导致无法执行后边的循环。

+
1
2
3
4
5
6
7
8
var wg sync.WaitGroup
wg.Add(len(tasks))
for _, t := range tasks {
go func(t *task) {
defer group.Done()
}(t)
group.Wait()
}
+

正确的写法是把 Wait() 放在循环外:

+
1
2
3
4
5
6
7
8
9
var wg sync.WaitGroup
wg.Add(len(tasks))
for _, t := range tasks {
go func(t *task) {
defer group.Done()
}(t)
}

group.Wait()
+

1.3 在循环中使用 defer

只有当函数返回时,defer 才会被执行。除非你知道你在做什么,否则不应该将 defer 用在循环中。

+
1
2
3
4
5
6
7
8
9
10
var mutex sync.Mutex
type Person struct {
Age int
}
persons := make([]Person, 10)
for _, p := range persons {
mutex.Lock()
defer mutex.Unlock()
p.Age = 13
}
+

在上边的例子中,在完成第一次循环后,之后的循环无法获得互斥锁从而被阻塞。应该改成下边的显性释放锁的方式:

+
1
2
3
4
5
6
7
8
9
10
11
var mutex sync.Mutex
type Person struct {
Age int
}
persons := make([]Person, 10)
for _, p := range persons {
mutex.Lock()

p.Age = 13
mutex.Unlock()
}
+

如果你确实需要在循环中使用 defer,可以考虑将工作委托给另一个函数:

+
1
2
3
4
5
6
7
8
9
10
11
12
var mutex sync.Mutex
type Person struct {
Age int
}
persons := make([]Person, 10)
for _, p := range persons {
func() {
mutex.Lock()
defer mutex.Unlock()
p.Age = 13
}()
}
+

2. 往 unbuffered channel 中发送数据

1
2
3
4
5
6
7
8
9
10
11
12
13
func doReq(timeout time.Duration) obj {
ch :=make(chan obj)
go func() {
obj := do()
ch <- obj
} ()
select {
case result = <- ch :
return result
case<- time.After(timeout):
return nil
}
}
+

上边的代码模拟这样一个行为:超时前获得到结果将结果返回,若超时则返回 nil。

+

我们通过一个 Goroutine 异步获取结果,并通过一个 channel 配合 select 来阻塞代码往后执行。

+

上边代码使用了 unbuffered channel,这会导致的问题是,如果代码因超时提前返回了,Goroutine 在获取到结果后,会阻塞在 ch <- obj 这一行(因为没有其他的 Goroutine 来读取这个 channle),从而这个 Goroutine 无法退出,进而会发生 Goroutine 泄露。

+

解决方法是使用一个长度为 1 的 buffered channel

+
1
2
3
4
5
6
7
8
9
10
11
12
13
func doReq(timeout time.Duration) obj {
ch := make(chan obj, 1)
go func() {
obj := do()
ch <- result
} ()
select {
case result = <- ch :
return result
case<- time.After(timeout):
return nil
}
}
+

还有一种修复方式是在 Goroutine 中使用一个 select 配合一个空的 default

+
1
2
3
4
5
6
...
select {
case ch <- result:
default:
}
...
+

当没有其他 Goroutine 来读取这个 channel 时,会走到 default 行为,这个 Goroutine 也就可以正常退出了。

+

3. 不使用接口

接口可以使代码更具灵活性,是在代码中引入多态的一种方法。接口允许我们关注一组行为而非特定类型。不使用接口不会有错误产生,但会让我们的代码看起来不那么优雅、不具有可扩展性。

+

在众多接口中,io.Readerio.Writer 可能是最受欢迎的一对。

+
1
2
3
4
5
6
7
type Reader interface {
Read(p []byte) (n int, err error)
}

type Writer interface {
Write(p []byte) (n int, err error)
}
+

这些接口非常强大, 假设我们需要将一个对象写入一个文件,可以这样定义一个 Save 方法:

+
1
func (o *obj) Save(file os.File) error
+

如果明天我们有需要将这个文件写入 http.ResponseWriter 呢?我们可不想重新定义一个新的方法,这时 io.Writer 就派上用场了:

+
1
func (o *obj) Save(w io.Writer) error
+

还需明白的一点是:我们应该只关心我们要使用的行为。在上边的例子中,使用 io.ReadWriteCloser 虽然也行得通,但如果我们只用到了 Write 方法,就不是特别好的实践了。接口面积越大,抽象能力越弱。

+

因此,在大部分情况下,我们应关注行为而不是具体类型。

+

4. struct 中未考虑字段声明顺序

下边的代码不会出现错误,但会有使用更多的内存:

+
1
2
3
4
5
type BadOrderedPerson struct {
Veteran bool // 1 byte
Name string // 16 byte
Age int32 // 4 byte
}
+

上边的 struct 看起来会分配 21 bytes 的内存,但实际上分配的是 32 bytes。出现这个情况原因是数据结构对齐。在 64 位架构中,内存以 8 bytes 为一个连续单元,改成下边的声明顺序可以优化到分配 24 bytes:

+
1
2
3
4
5
type OrderedPerson struct {
Name string
Age int32
Veteran bool
}
+

在频繁使用不合理字段顺序的类型时,会导致额外的内存开销。

+

不过,我们也不必手动计算和优化结构体内存,可以使用 go tool 提供的 fieldalignment 工具来检测并修复不合理的声明顺序。

+

fieldalignment 安装:

1
2
3
4
cd $GOPATH
git clone git@github.com:golang/tools.git src/golang.org/x/tools
src/golang.org/x/tools/go/analysis/passes/fieldalignment/cmd/fieldalignment
go install
+

fieldalignment 使用

1
2
3
4
5
6
➜ fieldalignment .
/Users/jiapan/Projects/tantan-live-distribution/app/domain/recommend_service.go:179:30: struct of size 88 could be 80
/Users/jiapan/Projects/tantan-live-distribution/app/domain/voice_recommend_service.go:63:35: struct with 40 pointer bytes could be 24

// 修复字段顺序
➜ fieldalignment -fix .
+

5. test 时未使用 race 检测器

数据竞争会导致一些很迷的问题,而且通常是在部署一段时间后才会发生。所以此类问题在并发系统中是最常见而且最难排查的 bug。为了更方便找出此类 bug,Go 1.1 中引入了一个内置的数据竞争检测器,只需加上 -race 标识就可以了。

+
1
2
3
4
$ go test -race pkg    // to test the package
$ go run -race pkg.go // to run the source file
$ go build -race // to build the package
$ go install -race pkg // to install the package
+

当开启竞争检测器时,编译器会记录代码对内存进行了何时、何种方式的访问,同时 runtime 监控共享变量的非同步访问。

+

发现数据竞争时,竞争检测器会打印包含访问冲突的调用栈记录,如下所示:

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
WARNING: DATA RACE
Read by goroutine 185:
net.(*pollServer).AddFD()
src/net/fd_unix.go:89 +0x398
net.(*pollServer).WaitWrite()
src/net/fd_unix.go:247 +0x45
net.(*netFD).Write()
src/net/fd_unix.go:540 +0x4d4
net.(*conn).Write()
src/net/net.go:129 +0x101
net.func·060()
src/net/timeout_test.go:603 +0xaf
Previous write by goroutine 184:
net.setWriteDeadline()
src/net/sockopt_posix.go:135 +0xdf
net.setDeadline()
src/net/sockopt_posix.go:144 +0x9c
net.(*conn).SetDeadline()
src/net/net.go:161 +0xe3
net.func·061()
src/net/timeout_test.go:616 +0x3ed
Goroutine 185 (running) created at:
net.func·061()
src/net/timeout_test.go:609 +0x288
Goroutine 184 (running) created at:
net.TestProlongTimeout()
src/net/timeout_test.go:618 +0x298
testing.tRunner()
src/testing/testing.go:301 +0xe8
+
+
+

写在最后:人类从历史中学到的唯一教训,就是人类无法从历史中学到任何教训。

+
+ +
+ + + + + + + +
+ + + + + + +
+ +
+ + + +
+ + + + + + + + +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2021/go-memory-leak-by-slice/index.html b/2021/go-memory-leak-by-slice/index.html new file mode 100644 index 0000000000..dd58f0873b --- /dev/null +++ b/2021/go-memory-leak-by-slice/index.html @@ -0,0 +1,498 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Go 中由切片引起的内存泄露 | 贾攀的流水账 + + + + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

贾攀的流水账

+ +
+

Panmax's Blog

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+ + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ Go 中由切片引起的内存泄露 +

+ + +
+ + + + +
+ + +

与 C/C++ 不同,Go 有 GC,所以我们不需要手动处理内存的分配和释放。不过,我们仍然应该谨慎对待内存泄漏问题。

+

来看一个由 slice 引起的内存泄漏案例。

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package main
import (
"fmt"
)
type Object struct {}
func main() {
var a []*Object
for i := 0; i < 8; i++ {
a = append(a, new(Object))
}
fmt.Println(cap(a), len(a)) // 输出: 8, 8
a = remove(a, 5)
fmt.Println(cap(a), len(a)) // 输出: 8, 7
}
func remove(s []*Object, i int) []*Object {
return append(s[:i], s[i+1:]...)
}
+

我们可以看到,即使有一个对象被删除,a 的容量仍然是8,这意味着remove 函数可能导致潜在的内存泄漏。

+

为什么会发生这种情况?

来看一个例子

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
package main
import (
"fmt"
)
func main() {
// a 和 b 代表同一个数组 [1,2] 的两个部分
a := []int{1,2}
b := a[0:1]
fmt.Println(a, b) // 输出: [1 2] [1]

// 底层数组的容量是2,b在 append 后的长度将是2,只需将b的范围增加到数组[0:1]。
// array[1]将被改为3,因为 a 和 b 是在同一个数组上,所以a[1]也是3。
b = append(b, 3)
fmt.Println(a, b) // 输出: [1 3] [1 3]

// 因为 b 的长度将比数组的容量大3,所以将创建一个新的数组
// 新数组的容量将是 2*cap(old) = 4
b = append(b, 4)
b[0] = 0
// 现在 a 和 b 在不同的数组上
fmt.Println(a, b) // 输出: [1 3] [0 3 4]
fmt.Println(cap(a), cap(b)) // 输出: 2 4
}
+

如何避免内存泄漏?

在这种情况下,有两种内存泄漏。

+

1. 底层数组

底层数组的容量只会增加,但不会减少,第一个例子已经证明了这一点。

+

如果我们认为容量太大,我们可以创建一个新的 slice,并将原 slice 中的所有元素复制到新 slice 中。这是一个复制操作(时间)和内存使用(空间)之间的权衡。

+
1
2
3
4
5
6
func remove(s []*Object, i int) []*Object {
s = append(s[:i], s[i+1:]...)
a := make([]*Object, len(s))
copy(a, s) // 时间换空间
return a
}
+

2. 指向数组元素的内存,其类型为指针

解决方法:将未使用的元素设置为nil,它将会被 GC 释放。

+
1
2
3
4
5
6
func remove(s []*Object, i int) []*Object {
old := s
s = append(s[:i], s[i+1:]...)
old[len(old)-1] = nil
return s
}
+ +
+ + + + + + + +
+ + + + + + +
+ +
+ + + +
+ + + + + + + + +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2021/guidetodatamining-points/16180502800725.jpg b/2021/guidetodatamining-points/16180502800725.jpg new file mode 100644 index 0000000000..0c371bd11f Binary files /dev/null and b/2021/guidetodatamining-points/16180502800725.jpg differ diff --git a/2021/guidetodatamining-points/16180503062224.jpg b/2021/guidetodatamining-points/16180503062224.jpg new file mode 100644 index 0000000000..bdd7e39aa6 Binary files /dev/null and b/2021/guidetodatamining-points/16180503062224.jpg differ diff --git a/2021/guidetodatamining-points/16180503540478.jpg b/2021/guidetodatamining-points/16180503540478.jpg new file mode 100644 index 0000000000..e8032a42ef Binary files /dev/null and b/2021/guidetodatamining-points/16180503540478.jpg differ diff --git a/2021/guidetodatamining-points/16180503743318.jpg b/2021/guidetodatamining-points/16180503743318.jpg new file mode 100644 index 0000000000..1710d6ae06 Binary files /dev/null and b/2021/guidetodatamining-points/16180503743318.jpg differ diff --git a/2021/guidetodatamining-points/16180504120770.jpg b/2021/guidetodatamining-points/16180504120770.jpg new file mode 100644 index 0000000000..3294fa95a9 Binary files /dev/null and b/2021/guidetodatamining-points/16180504120770.jpg differ diff --git a/2021/guidetodatamining-points/16180504256020.jpg b/2021/guidetodatamining-points/16180504256020.jpg new file mode 100644 index 0000000000..23c110816f Binary files /dev/null and b/2021/guidetodatamining-points/16180504256020.jpg differ diff --git a/2021/guidetodatamining-points/16180504363132.jpg b/2021/guidetodatamining-points/16180504363132.jpg new file mode 100644 index 0000000000..792e28dd75 Binary files /dev/null and b/2021/guidetodatamining-points/16180504363132.jpg differ diff --git a/2021/guidetodatamining-points/16180504450729.jpg b/2021/guidetodatamining-points/16180504450729.jpg new file mode 100644 index 0000000000..918813da68 Binary files /dev/null and b/2021/guidetodatamining-points/16180504450729.jpg differ diff --git a/2021/guidetodatamining-points/16180504509371.jpg b/2021/guidetodatamining-points/16180504509371.jpg new file mode 100644 index 0000000000..be51cc61f5 Binary files /dev/null and b/2021/guidetodatamining-points/16180504509371.jpg differ diff --git a/2021/guidetodatamining-points/16180504562002.jpg b/2021/guidetodatamining-points/16180504562002.jpg new file mode 100644 index 0000000000..5bd6c787d4 Binary files /dev/null and b/2021/guidetodatamining-points/16180504562002.jpg differ diff --git a/2021/guidetodatamining-points/16180504675659.jpg b/2021/guidetodatamining-points/16180504675659.jpg new file mode 100644 index 0000000000..42743c43dd Binary files /dev/null and b/2021/guidetodatamining-points/16180504675659.jpg differ diff --git a/2021/guidetodatamining-points/16180504735687.jpg b/2021/guidetodatamining-points/16180504735687.jpg new file mode 100644 index 0000000000..8a14375d56 Binary files /dev/null and b/2021/guidetodatamining-points/16180504735687.jpg differ diff --git a/2021/guidetodatamining-points/16180505034865.jpg b/2021/guidetodatamining-points/16180505034865.jpg new file mode 100644 index 0000000000..5863a56995 Binary files /dev/null and b/2021/guidetodatamining-points/16180505034865.jpg differ diff --git a/2021/guidetodatamining-points/16180506173984.jpg b/2021/guidetodatamining-points/16180506173984.jpg new file mode 100644 index 0000000000..d0b2894a48 Binary files /dev/null and b/2021/guidetodatamining-points/16180506173984.jpg differ diff --git a/2021/guidetodatamining-points/16180506253823.jpg b/2021/guidetodatamining-points/16180506253823.jpg new file mode 100644 index 0000000000..d0409614fb Binary files /dev/null and b/2021/guidetodatamining-points/16180506253823.jpg differ diff --git a/2021/guidetodatamining-points/16180506513378.jpg b/2021/guidetodatamining-points/16180506513378.jpg new file mode 100644 index 0000000000..df2325cef1 Binary files /dev/null and b/2021/guidetodatamining-points/16180506513378.jpg differ diff --git a/2021/guidetodatamining-points/16180506567485.jpg b/2021/guidetodatamining-points/16180506567485.jpg new file mode 100644 index 0000000000..406f6ff42d Binary files /dev/null and b/2021/guidetodatamining-points/16180506567485.jpg differ diff --git a/2021/guidetodatamining-points/16180506750399.jpg b/2021/guidetodatamining-points/16180506750399.jpg new file mode 100644 index 0000000000..7a05cddc49 Binary files /dev/null and b/2021/guidetodatamining-points/16180506750399.jpg differ diff --git a/2021/guidetodatamining-points/16180506961757.jpg b/2021/guidetodatamining-points/16180506961757.jpg new file mode 100644 index 0000000000..6a2b832c75 Binary files /dev/null and b/2021/guidetodatamining-points/16180506961757.jpg differ diff --git a/2021/guidetodatamining-points/16180507023401.jpg b/2021/guidetodatamining-points/16180507023401.jpg new file mode 100644 index 0000000000..c6bed96e8f Binary files /dev/null and b/2021/guidetodatamining-points/16180507023401.jpg differ diff --git a/2021/guidetodatamining-points/16180507112641.jpg b/2021/guidetodatamining-points/16180507112641.jpg new file mode 100644 index 0000000000..ded61af750 Binary files /dev/null and b/2021/guidetodatamining-points/16180507112641.jpg differ diff --git a/2021/guidetodatamining-points/16180507150889.jpg b/2021/guidetodatamining-points/16180507150889.jpg new file mode 100644 index 0000000000..c60ff03424 Binary files /dev/null and b/2021/guidetodatamining-points/16180507150889.jpg differ diff --git a/2021/guidetodatamining-points/16180507222041.jpg b/2021/guidetodatamining-points/16180507222041.jpg new file mode 100644 index 0000000000..318af4de02 Binary files /dev/null and b/2021/guidetodatamining-points/16180507222041.jpg differ diff --git a/2021/guidetodatamining-points/16180507271022.jpg b/2021/guidetodatamining-points/16180507271022.jpg new file mode 100644 index 0000000000..17a26cde19 Binary files /dev/null and b/2021/guidetodatamining-points/16180507271022.jpg differ diff --git a/2021/guidetodatamining-points/16180507321601.jpg b/2021/guidetodatamining-points/16180507321601.jpg new file mode 100644 index 0000000000..359ee3f2e2 Binary files /dev/null and b/2021/guidetodatamining-points/16180507321601.jpg differ diff --git a/2021/guidetodatamining-points/16180507539281.jpg b/2021/guidetodatamining-points/16180507539281.jpg new file mode 100644 index 0000000000..8164c26e1d Binary files /dev/null and b/2021/guidetodatamining-points/16180507539281.jpg differ diff --git a/2021/guidetodatamining-points/16180507585991.jpg b/2021/guidetodatamining-points/16180507585991.jpg new file mode 100644 index 0000000000..ab9c34b93f Binary files /dev/null and b/2021/guidetodatamining-points/16180507585991.jpg differ diff --git a/2021/guidetodatamining-points/16180507641583.jpg b/2021/guidetodatamining-points/16180507641583.jpg new file mode 100644 index 0000000000..5e60fc81b6 Binary files /dev/null and b/2021/guidetodatamining-points/16180507641583.jpg differ diff --git a/2021/guidetodatamining-points/16180507753556.jpg b/2021/guidetodatamining-points/16180507753556.jpg new file mode 100644 index 0000000000..048409821e Binary files /dev/null and b/2021/guidetodatamining-points/16180507753556.jpg differ diff --git a/2021/guidetodatamining-points/16180507784873.jpg b/2021/guidetodatamining-points/16180507784873.jpg new file mode 100644 index 0000000000..048409821e Binary files /dev/null and b/2021/guidetodatamining-points/16180507784873.jpg differ diff --git a/2021/guidetodatamining-points/16180507824668.jpg b/2021/guidetodatamining-points/16180507824668.jpg new file mode 100644 index 0000000000..004ab58e50 Binary files /dev/null and b/2021/guidetodatamining-points/16180507824668.jpg differ diff --git a/2021/guidetodatamining-points/16180507871219.jpg b/2021/guidetodatamining-points/16180507871219.jpg new file mode 100644 index 0000000000..2f882b2d42 Binary files /dev/null and b/2021/guidetodatamining-points/16180507871219.jpg differ diff --git a/2021/guidetodatamining-points/16180507950656.jpg b/2021/guidetodatamining-points/16180507950656.jpg new file mode 100644 index 0000000000..5189c2b452 Binary files /dev/null and b/2021/guidetodatamining-points/16180507950656.jpg differ diff --git a/2021/guidetodatamining-points/16180508068666.jpg b/2021/guidetodatamining-points/16180508068666.jpg new file mode 100644 index 0000000000..aadd5f32de Binary files /dev/null and b/2021/guidetodatamining-points/16180508068666.jpg differ diff --git a/2021/guidetodatamining-points/16180508898266.jpg b/2021/guidetodatamining-points/16180508898266.jpg new file mode 100644 index 0000000000..f4f2324604 Binary files /dev/null and b/2021/guidetodatamining-points/16180508898266.jpg differ diff --git a/2021/guidetodatamining-points/16180508947636.jpg b/2021/guidetodatamining-points/16180508947636.jpg new file mode 100644 index 0000000000..8c4db747ac Binary files /dev/null and b/2021/guidetodatamining-points/16180508947636.jpg differ diff --git a/2021/guidetodatamining-points/16180509129002.jpg b/2021/guidetodatamining-points/16180509129002.jpg new file mode 100644 index 0000000000..5ba423b5c8 Binary files /dev/null and b/2021/guidetodatamining-points/16180509129002.jpg differ diff --git a/2021/guidetodatamining-points/16180509679074.jpg b/2021/guidetodatamining-points/16180509679074.jpg new file mode 100644 index 0000000000..4670292883 Binary files /dev/null and b/2021/guidetodatamining-points/16180509679074.jpg differ diff --git a/2021/guidetodatamining-points/16180509735438.jpg b/2021/guidetodatamining-points/16180509735438.jpg new file mode 100644 index 0000000000..6419d1b046 Binary files /dev/null and b/2021/guidetodatamining-points/16180509735438.jpg differ diff --git a/2021/guidetodatamining-points/16180510428253.jpg b/2021/guidetodatamining-points/16180510428253.jpg new file mode 100644 index 0000000000..e54543cc54 Binary files /dev/null and b/2021/guidetodatamining-points/16180510428253.jpg differ diff --git a/2021/guidetodatamining-points/16180510502089.jpg b/2021/guidetodatamining-points/16180510502089.jpg new file mode 100644 index 0000000000..c2b391240f Binary files /dev/null and b/2021/guidetodatamining-points/16180510502089.jpg differ diff --git a/2021/guidetodatamining-points/16180510688805.jpg b/2021/guidetodatamining-points/16180510688805.jpg new file mode 100644 index 0000000000..5a2f5f1fa9 Binary files /dev/null and b/2021/guidetodatamining-points/16180510688805.jpg differ diff --git a/2021/guidetodatamining-points/16180510730702.jpg b/2021/guidetodatamining-points/16180510730702.jpg new file mode 100644 index 0000000000..f4de585d1d Binary files /dev/null and b/2021/guidetodatamining-points/16180510730702.jpg differ diff --git a/2021/guidetodatamining-points/16180510916565.jpg b/2021/guidetodatamining-points/16180510916565.jpg new file mode 100644 index 0000000000..7bff102d4d Binary files /dev/null and b/2021/guidetodatamining-points/16180510916565.jpg differ diff --git a/2021/guidetodatamining-points/16180510979756.jpg b/2021/guidetodatamining-points/16180510979756.jpg new file mode 100644 index 0000000000..a8dd82bb02 Binary files /dev/null and b/2021/guidetodatamining-points/16180510979756.jpg differ diff --git a/2021/guidetodatamining-points/16180511012204.jpg b/2021/guidetodatamining-points/16180511012204.jpg new file mode 100644 index 0000000000..6dcc9bdf9b Binary files /dev/null and b/2021/guidetodatamining-points/16180511012204.jpg differ diff --git a/2021/guidetodatamining-points/16180511094255.jpg b/2021/guidetodatamining-points/16180511094255.jpg new file mode 100644 index 0000000000..11c248982a Binary files /dev/null and b/2021/guidetodatamining-points/16180511094255.jpg differ diff --git a/2021/guidetodatamining-points/16180511236348.jpg b/2021/guidetodatamining-points/16180511236348.jpg new file mode 100644 index 0000000000..cfede17bde Binary files /dev/null and b/2021/guidetodatamining-points/16180511236348.jpg differ diff --git a/2021/guidetodatamining-points/16180511296710.jpg b/2021/guidetodatamining-points/16180511296710.jpg new file mode 100644 index 0000000000..efaad76a96 Binary files /dev/null and b/2021/guidetodatamining-points/16180511296710.jpg differ diff --git a/2021/guidetodatamining-points/16180511348946.jpg b/2021/guidetodatamining-points/16180511348946.jpg new file mode 100644 index 0000000000..ead5f36f51 Binary files /dev/null and b/2021/guidetodatamining-points/16180511348946.jpg differ diff --git a/2021/guidetodatamining-points/16180511443995.jpg b/2021/guidetodatamining-points/16180511443995.jpg new file mode 100644 index 0000000000..06e041ed95 Binary files /dev/null and b/2021/guidetodatamining-points/16180511443995.jpg differ diff --git a/2021/guidetodatamining-points/16180511856276.jpg b/2021/guidetodatamining-points/16180511856276.jpg new file mode 100644 index 0000000000..eef629ffe9 Binary files /dev/null and b/2021/guidetodatamining-points/16180511856276.jpg differ diff --git a/2021/guidetodatamining-points/16180511997961.jpg b/2021/guidetodatamining-points/16180511997961.jpg new file mode 100644 index 0000000000..b97a8deef5 Binary files /dev/null and b/2021/guidetodatamining-points/16180511997961.jpg differ diff --git a/2021/guidetodatamining-points/16180512064821.jpg b/2021/guidetodatamining-points/16180512064821.jpg new file mode 100644 index 0000000000..0f4a9b9437 Binary files /dev/null and b/2021/guidetodatamining-points/16180512064821.jpg differ diff --git a/2021/guidetodatamining-points/16180512109821.jpg b/2021/guidetodatamining-points/16180512109821.jpg new file mode 100644 index 0000000000..6a5d5cb97f Binary files /dev/null and b/2021/guidetodatamining-points/16180512109821.jpg differ diff --git a/2021/guidetodatamining-points/16180512214247.jpg b/2021/guidetodatamining-points/16180512214247.jpg new file mode 100644 index 0000000000..ce6e2d2f12 Binary files /dev/null and b/2021/guidetodatamining-points/16180512214247.jpg differ diff --git a/2021/guidetodatamining-points/16180512279040.jpg b/2021/guidetodatamining-points/16180512279040.jpg new file mode 100644 index 0000000000..76b1128ff9 Binary files /dev/null and b/2021/guidetodatamining-points/16180512279040.jpg differ diff --git a/2021/guidetodatamining-points/16180512332435.jpg b/2021/guidetodatamining-points/16180512332435.jpg new file mode 100644 index 0000000000..00e41df4cd Binary files /dev/null and b/2021/guidetodatamining-points/16180512332435.jpg differ diff --git a/2021/guidetodatamining-points/16180512368972.jpg b/2021/guidetodatamining-points/16180512368972.jpg new file mode 100644 index 0000000000..61efdcad1f Binary files /dev/null and b/2021/guidetodatamining-points/16180512368972.jpg differ diff --git a/2021/guidetodatamining-points/16180512440972.jpg b/2021/guidetodatamining-points/16180512440972.jpg new file mode 100644 index 0000000000..93b6ab18b9 Binary files /dev/null and b/2021/guidetodatamining-points/16180512440972.jpg differ diff --git a/2021/guidetodatamining-points/16180512581633.jpg b/2021/guidetodatamining-points/16180512581633.jpg new file mode 100644 index 0000000000..673a0cd6a0 Binary files /dev/null and b/2021/guidetodatamining-points/16180512581633.jpg differ diff --git a/2021/guidetodatamining-points/16180512917768.jpg b/2021/guidetodatamining-points/16180512917768.jpg new file mode 100644 index 0000000000..be7f6cde65 Binary files /dev/null and b/2021/guidetodatamining-points/16180512917768.jpg differ diff --git a/2021/guidetodatamining-points/16180512974529.jpg b/2021/guidetodatamining-points/16180512974529.jpg new file mode 100644 index 0000000000..4dd3d43e79 Binary files /dev/null and b/2021/guidetodatamining-points/16180512974529.jpg differ diff --git a/2021/guidetodatamining-points/16180513070896.jpg b/2021/guidetodatamining-points/16180513070896.jpg new file mode 100644 index 0000000000..55187c8e06 Binary files /dev/null and b/2021/guidetodatamining-points/16180513070896.jpg differ diff --git a/2021/guidetodatamining-points/16180513116255.jpg b/2021/guidetodatamining-points/16180513116255.jpg new file mode 100644 index 0000000000..c23f221f11 Binary files /dev/null and b/2021/guidetodatamining-points/16180513116255.jpg differ diff --git a/2021/guidetodatamining-points/16180513192590.jpg b/2021/guidetodatamining-points/16180513192590.jpg new file mode 100644 index 0000000000..92d1ecc25d Binary files /dev/null and b/2021/guidetodatamining-points/16180513192590.jpg differ diff --git a/2021/guidetodatamining-points/16180513291608.jpg b/2021/guidetodatamining-points/16180513291608.jpg new file mode 100644 index 0000000000..60c1395287 Binary files /dev/null and b/2021/guidetodatamining-points/16180513291608.jpg differ diff --git a/2021/guidetodatamining-points/16180513358428.jpg b/2021/guidetodatamining-points/16180513358428.jpg new file mode 100644 index 0000000000..190f31b4c6 Binary files /dev/null and b/2021/guidetodatamining-points/16180513358428.jpg differ diff --git a/2021/guidetodatamining-points/16180513402506.jpg b/2021/guidetodatamining-points/16180513402506.jpg new file mode 100644 index 0000000000..db981aa70e Binary files /dev/null and b/2021/guidetodatamining-points/16180513402506.jpg differ diff --git a/2021/guidetodatamining-points/16180513439380.jpg b/2021/guidetodatamining-points/16180513439380.jpg new file mode 100644 index 0000000000..c2cb5782df Binary files /dev/null and b/2021/guidetodatamining-points/16180513439380.jpg differ diff --git a/2021/guidetodatamining-points/16180513694801.jpg b/2021/guidetodatamining-points/16180513694801.jpg new file mode 100644 index 0000000000..873c6ad22a Binary files /dev/null and b/2021/guidetodatamining-points/16180513694801.jpg differ diff --git a/2021/guidetodatamining-points/index.html b/2021/guidetodatamining-points/index.html new file mode 100644 index 0000000000..aba9159b92 --- /dev/null +++ b/2021/guidetodatamining-points/index.html @@ -0,0 +1,782 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 面向程序员的数据挖掘指南-知识点 | 贾攀的流水账 + + + + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

贾攀的流水账

+ +
+

Panmax's Blog

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+ + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ 面向程序员的数据挖掘指南-知识点 +

+ + +
+ + + + +
+ + +

前段时间读了《面向程序员的数据挖掘指南》,原文链接:https://dataminingguide.books.yourtion.com/,把里边的知识点做下整理。

+

曼哈顿距离

x之差的绝对值加上y之差的绝对值

+

欧几里得距离

+

勾股定理

+

闵可夫斯基距离

+
    +
  • r = 1 该公式即曼哈顿距离
  • +
  • r = 2 该公式即欧几里得距离
  • +
  • r = ∞ 极大距离
  • +
+

r值越大,单个维度的差值大小会对整体距离有更大的影响。

+

协同过滤

利用他人的喜好来进行推荐,也就是说,是大家一起产生的推荐。

+

皮尔逊相关系数

用于衡量两个变量之间的相关性,它的值在-1至1之间,1表示完全吻合,-1表示完全相悖。

+

皮尔逊相关系数的计算公式是:

+

皮尔逊相关系数的近似值:

+

余弦相似度

+

“·”号表示数量积。

+

“||x||”表示向量x的模,计算公式是:

+

如:

+

它们的模是:

+

数量积的计算:

+

因此余弦相似度是:

+

余弦相似度的范围从1到-1,1表示完全匹配,-1表示完全相悖。

+

应该使用哪种相似度?

    +
  • 如果数据存在“分数膨胀”问题,就使用皮尔逊相关系数。
  • +
  • 如果数据比较“密集”,变量之间基本都存在公有值,且这些距离数据是非常重要的,那就使用欧几里得或曼哈顿距离。
  • +
  • 如果数据是稀疏的,则使用余弦相似度。
  • +
+

K最邻近算法

+

用户的评价类型可以分为显式评价和隐式评价

    +
  • 显式评价指的是用户明确地给出对物品的评价
  • +
  • 所谓隐式评价,就是我们不让用户明确给出对物品的评价,而是通过观察他们的行为来获得偏好信息。
      +
    • 另一种隐式评价是用户的实际购买记录
    • +
    +
  • +
+

显式评价的问题

    +
  • 问题1:人们很懒,不愿评价物品
  • +
  • 问题2:人们会撒谎,或存有偏见
  • +
  • 问题3:人们不会更新他们的评论
  • +
+

基于用户的协同过滤弊端

    +
  1. 扩展性 上文已经提到,随着用户数量的增加,其计算量也会增加。这种算法在只有几千个用户的情况下能够工作得很好,但达到一百万个用户时就会出现瓶颈。
  2. +
  3. 稀疏性 大多数推荐系统中,物品的数量要远大于用户的数量,因此用户仅仅对一小部分物品进行了评价,这就造成了数据的稀疏性。
  4. +
+

基于用户的协同过滤和基于物品的协同过滤区别

    +
  • 基于用户的协同过滤是通过计算用户之间的距离找出最相似的用户,并将他评价过的物品推荐给目标用户;
  • +
  • 而基于物品的协同过滤则是找出最相似的物品,再结合用户的评价来给出推荐结果。

    +
  • +
  • 基于用户的协同过滤又称为内存型协同过滤,因为我们需要将所有的评价数据都保存在内存中来进行推荐。

    +
  • +
  • 基于物品的协同过滤也称为基于模型的协同过滤,因为我们不需要保存所有的评价数据,而是通过构建一个物品相似度模型来做推荐。
  • +
+

修正的余弦相似度

修正的余弦相似度是一种基于模型的协同过滤算法。这种算法的优势之一是扩展性好,对于大数据量而言,运算速度快、占用内存少。

+

用户的评价标准是不同的,比如喜欢一个歌手时有些人会打4分,有些打5分;不喜欢时有人会打3分,有些则会只给1分。修正的余弦相似度计算时会将用户对物品的评分减去用户所有评分的均值,从而解决这个问题。

+

+

U表示同时评价过物品i和j的用户集合

+

+

表示将用户u对物品i的评价值减去用户u对所有物品的评价均值,从而得到修正后的评分。

+

s(i,j)表示物品i和j的相似度,分子表示将同时评价过物品i和j的用户的修正评分相乘并求和,分母则是对所有的物品的修正评分做一些汇总处理。

+

修正的余弦相似度示例

计算Kacey Musgraves和Imagine Dragons的相似度

+

我已经标出了同时评价过这两个歌手的用户,代入到公式中:

+

所以这两个歌手之间的修正余弦相似度为0.5260

+

使用修正余弦相似度进行预测

比如我想知道David有多喜欢Kacey Musgraves?

+

p(u,i)表示我们会来预测用户u对物品i的评分,所以p(David, Kacey Musgraves)就表示我们将预测David会给Kacey打多少分。
N是一个物品的集合,有如下特性:

+
    +
  • 用户u对集合中的物品打过分
  • +
  • 物品i和集合中的物品有相似度数据(即上文中的矩阵)
  • +
+

Si,N表示物品i和N的相似度,Ru,N表示用户u对物品N的评分。

+

为了让公式的计算效果更佳,对物品的评价分值最好介于-1和1之间。

+

MaxR表示评分系统中的最高分(这里是5),MinR为最低分(这里是1),Ru,N是用户u对物品N的评分,NRu,N则表示修正后的评分(即范围在-1和1之间)。

+

若已知NRu,N,求解Ru,N的公式为:

+

比如一位用户打了2分,那修正后的评分为:

+

反过来则是:

+

修正David对各个物品的评分:

+

结合物品相似度矩阵,代入公式:

+

将其转换到5星评价体系中:

+

Slope One算法

一种比较流行的基于物品的协同过滤算法

+

分为两个步骤:

+
    +
  • 首先需要计算出两两物品之间的差值(可以在夜间批量计算)。
  • +
  • 第二步则是进行预测
  • +
+

Slope One算法计算差值

计算物品之间差异的公式是:

+

card(S)表示S中有多少个元素;X表示所有评分值的集合;card(Sj,i(X))则表示同时评价过物品j和i的用户数。

+

计算Taylor Swift 和 PSY之间的差值

+

card(Sj,i(X))的值是2——因为有两个用户(Amy和Ben)同时对PSY和Taylor Swift打过分。

+

分子uj-ui表示用户对j的评分减去对i的评分,代入公式得:

+

即用户们给Taylor Swift的评分比PSY要平均高出两分。

+

Slope One算法更新

比如说Taylor Swift和PSY的差值是2,是根据9位用户的评价计算的。当有一个新用户对Taylor Swift打了5分,PSY打了1分时,更新后的差值为:

+

使用加权的Slope One算法进行预测
公式为:

+

PWS1(u)j表示我们将预测用户u对物品i的评分。

+

表示遍历Ben评价过的所有歌手,除了Whitney Houston以外(也就是-{j}的意思)。

+

整个分子的意思是:对于Ben评价过的所有歌手(Whitney Houston除外),找出Whitney Houston和这些歌手之间的差值,并将差值加上Ben对这个歌手的评分。同时,我们要将这个结果乘以同时评价过两位歌手的用户数。

+

Ben的评分情况和两两歌手之间的差异值展示如下:

+
    +
  1. Ben对Taylor Swift打了5分,也就是ui
  2. +
  3. Whitney Houston和Taylor Swift的差异是-1,即devj,i
  4. +
  5. devj,i + ui = 4
  6. +
  7. 共有两个用户(Amy和Daisy)同时对Taylor Swift和Whitney Houston做了评价,即cj,i = 2
  8. +
  9. 那么(devj,i + ui) cj,i = 4 × 2 = 8
  10. +
  11. Ben对PSY打了2分
  12. +
  13. Whitney Houston和PSY的差异是0.75
  14. +
  15. devj,i + ui = 2.75
  16. +
  17. 有两个用户同时评价了这两位歌手,因此(devj,i + ui) cj,i = 2.75 × 2 = 5.5
  18. +
  19. 分子:8 + 5.5 = 13.5
  20. +
  21. 分母:2 + 2 = 4
  22. +
  23. 预测评分:13.5 ÷ 4 = 3.375
  24. +
+

向量

在线性代数中,向量(vector)指的是具有大小和方向的几何对象。向量支持多重运算,包括相加、相减及数乘等。

+
    +
  • 当我们用这种方式定义特征后,就可以运用线性代数中的向量运算法则了。
  • +
+

在数据挖掘中,向量则可简单认为是物品的一组特征,比如音乐乐曲的特征。做文本挖掘时,会将一篇文章也用向量来表示——每个元素的位置表示一个特定的单词,这个位置上的值表示单词出现的次数。

+
    +
  • 用「向量」一词比用「物品的一组特征」要来的专业
  • +
+

分类器

分类器是指通过物品特征来判断它应该属于哪个组或类别的程序。

+

分类器程序会基于一组已经做过分类的物品进行学习,从而判断新物品的所属类别。

+

标准化

要让数据变得可用我们可以对其进行标准化,最常用的方法是将所有数据都转化为0到1之间的值。

+

标准分计算公式:

+

mean:平均值
standard deviation:标准差
标准差的计算公式是:

+

card(x)表示集合x中的元素个数。

+

修正的标准分

计算方法:将标准分公式中的均值改为中位数,将标准差改为绝对偏差。

+

中位数指的是将所有数据进行排序,取中间的那个值。如果数据量是偶数,则取中间两个数值的均值。

+

计算工资的对偏差:
首先将所有人按薪水排序,找到中位数,然后计算绝对偏差:

+

可以计算得出Yun的修正标准分:

+

是否需要标准化?

当物品的特征数值尺度不一时,就有必要进行标准化。

+

需要进行标准化的情形:

+
    +
  1. 我们需要通过物品特性来计算距离;
  2. +
  3. 不同特性之间的尺度相差很大。
  4. +
+

十折交叉验证

将数据集随机分割成十个等份,每次用9份数据做训练集,1份数据做测试集,如此迭代10次。

+

留一法

在数据挖掘领域,N折交叉验证又称为留一法。

+

上面已经提到了留一法的优点之一:我们用几乎所有的数据进行训练,然后用一个数据进行测试。

+

留一法的另一个优点是:确定性。

十折交叉验证是一种不确定的验证。相反,留一法得到的结果总是相同的,这是它的一个优点。

+

缺点

最大的缺点是计算时间很长。

+

留一法的另一个缺点是分层问题。

+

在留一法中,所有的测试集都只包含一个数据。所以说,留一法对小数据集是合适的,但大多数情况下我们会选择十折交叉验证。

+

混淆矩阵

表格的行表示测试用例实际所属的类别,列则表示分类器的判断结果。

+

混淆矩阵可以帮助我们快速识别出分类器到底在哪些类别上发生了混淆,因此得名。

+

这个数据集中有300人,使用十折交叉验证,其混淆矩阵如下:

+

可以看到,100个体操运动员中有83人分类正确,17人被错误地分到了马拉松一列;92个篮球运动员分类正确,8人被分到了马拉松;85个马拉松运动员分类正确,9人被分到了体操,16人被分到了篮球。

+

混淆矩阵的对角线(绿色字体)表示分类正确的人数,因此求得的准确率是:

+

从混淆矩阵中可以看出分类器的主要问题。

+

在这个示例中,我们的分类器可以很好地区分体操运动员和篮球运动员,而马拉松运动员则比较容易和其他两个类别发生混淆。

+

Kappa指标

Kappa指标可以用来评价分类器的效果比随机分类要好多少。

+

Kappa指标可以用来衡量我们之前构造的分类器和随机分类器的差异,公式为:

+

P(c)表示分类器的准确率,P(r)表示随机分类器的准确率。

+

动手实践

以下是该分类器的混淆矩阵,尝试计算出它的Kappa指标并予以解释。

+

准确率 = (50+75+123+170)/600= 0.697

+

计算列合计和百分比:

+

然后根据百分比来填充随机分类器的混淆矩阵:

+

随机分类器准确率 = (8 + 24 + 51 + 92) / 600 = (175 / 600) = 0.292

+

最后,计算Kappa指标:

+

这说明分类器的效果还是要好过预期的。

+

kNN算法

考察这条新记录周围距离最近的k条记录,而不是只看一条,因此这种方法称为k近邻算法(kNN)。

+

每个近邻都有投票权,程序会将新记录判定为得票数最多的分类。比如说,我们使用三个近邻(k = 3),其中两条记录属于体操,一条记录属于马拉松,那我们会判定x为体操。

+

KNN 算法预测举例

我们需要预测Ben对Funky Meters的喜好程度,他的三个近邻分别是Sally、Tara、和Jade。

+

下表是这三个人离Ben的距离,以及他们对Funky Meters的评分:

+

在计算平均值的时候,我希望距离越近的用户影响越大,因此可以对距离取倒数,从而得到下表:

+

下面,我们把所有的距离倒数除以距离倒数的和(0.2 + 0.1 + 0.067 = 0.367),从而得到评分的权重:

+

我们可以注意到两件事情:权重之和是1;原始数据中,Sally的距离是Tara的二分之一,这点在权重中体现出来了。

+

最后,我们求得平均值,也即预测Ben对Funky Meters的评分:

+

近邻算法 vs 贝叶斯算法

近邻算法又称为被动学习算法。这种算法只是将训练集的数据保存起来,在收到测试数据时才会进行计算。

+

贝叶斯算法则是一种主动学习算法。它会根据训练集构建起一个模型,并用这个模型来对新的记录进行分类,因此速度会快很多。

+

贝叶斯算法的两个优点

能够给出分类结果的置信度

+

它是一种主动学习算法

+

概率

我们用符号P(h)来表示,即事件h发生的概率:

+
    +
  • 投掷硬币:P(正面) = 0.5
  • +
  • 掷骰子:P(1) = 1/6
  • +
  • 青少年:P(女生) = 0.5
  • +
+

P(h|D)来表示D条件下事件h发生的概率。比如:P(女生|弗兰克学院的学生) = 0.86

+

计算的公式是:

+

概率计算

下表是一些人使用笔记本电脑和手机的品牌:

+

使用iPhone的概率是多少?

+

如果已知这个人使用的是Mac笔记本,那他使用iPhone的概率是?

+

首先计算出同时使用Mac和iPhone的概率:

+

使用Mac的概率则是:

+

从而计算得到Mac用户中使用iPhone的概率:

+

为了简单起见,我们可以直接通过计数得到:

+

贝叶斯法则

贝叶斯法则描述了P(h)、P(h|D)、P(D)、以及P(D|h)这四个概率之间的关系:

+

现实问题中要计算P(h|D)往往是很困难的

+

朴素贝叶斯

朴素贝叶斯计算得到的概率其实是真实概率的一种估计,而真实概率是对全量数据做统计得到的。

+

在朴素贝叶斯中,概率为0的影响是很大的,甚至会不顾其他概率的大小。此外,抽样统计的另一个问题是会低估真实概率。

+

如何解决概率为0的影响?

解决方法是将公式变为以下形式:

+

n表示训练集中y类别的记录数;nc表示y类别中值为x的记录数。

+

m是一个常数,表示等效样本大小。

+

决定常数m的方法有很多,我们这里使用值的类别来作为m,比如投票有赞成和否决两种类别,所以m就为2。

+

p则是相应的先验概率,比如说赞成和否决的概率分别是0.5,那p就是0.5。

+

标准差

+

标准差是用来衡量数据的离散程度的,如果所有数据都接近于平均值,那标准差也会比较小。

+

样本标准差的公式是:

+

我们把有限集合A的元素个数记为card(A)。例如A={a,b,c},则card(A)=3

+

高斯分布

正态分布、钟型曲线、高斯分布等术语,他们指的是同一件事:68%的数据会落在标准差为1的范围内,95%的数据会落在标准差为2的范围内:

+

概率计算公式:

+

假设我们要计算P(100k|i500)的概率,即购买i500的用户中收入是100,000美元的概率。之前我们计算过购买i500的用户平均收入(106.111)以及样本标准差(21.327),我们用希腊字母μ(读“谬”)来表示平均值,σ(读“西格玛”)来表示标准差。

+

xi = 100 指的是收入100k


+

e是自然常数,约等于2.718。

+

监督式和非监督式学习

当我们使用已经标记好分类的数据集进行训练时,这种类型的机器学习称为“监督式学习”。文本分类就是监督式学习的一种。

+

如果训练集没有标好分类,那就称为“非监督式学习”,聚类就是一种非监督式学习

+

聚类

通过物品特征来计算距离,并自动分类到不同的群集或组中。

+

k-means算法可概括为

    +
  1. 随机选取k个元素作为中心点;
  2. +
  3. 根据距离将各个点分配给中心点;
  4. +
  5. 计算新的中心点;
  6. +
  7. 重复2、3,直至满足条件。
  8. +
+

评判聚类结果的好坏

我们可以使用误差平方和(或称离散程度)来评判聚类结果的好坏,它的计算方法是:计算每个点到中心点的距离平方和。

+

上面的公式中,第一个求和符号是遍历所有的分类,比如i=1时计算第一个分类,i=2时计算第二个分类,直到计算第k个分类;第二个求和符号是遍历分类中所有的点;Dist指代距离计算公式(如曼哈顿距离、欧几里得距离);计算数据点x和中心点ci之间的距离,平方后相加。

+

k-means++

前面我们提到k-means是50年代发明的算法,它的实现并不复杂,但仍是现今最流行的聚类算法。不过它也有一个明显的缺点。在算法一开始需要随机选取k个起始点,正是这个随机会有问题。
有时选取的点能产生最佳结果,而有时会让结果变得很差。k-means++则改进了起始点的选取过程,其余的和k-means一致。

+

以下是k-means++选取起始点的过程:

+
    +
  1. 随机选取一个点;
  2. +
  3. 重复以下步骤,直到选完k个点:
      +
    1. 计算每个数据点(dp)到各个中心点的距离(D),选取最小的值,记为D(dp);
    2. +
    3. 根据D(dp)的概率来随机选取一个点作为中心点。
    4. +
    +
  4. +
+

k-means++选取起始点的方法总结下来就是:第一个点还是随机的,但后续的点就会尽量选择离现有中心点更远的点。

+ +
+ + + + + + + +
+ + + + + + +
+ +
+ + + +
+ + + + + + + + +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2021/hongloumeng-homophonic-first-appear-sentence/index.html b/2021/hongloumeng-homophonic-first-appear-sentence/index.html new file mode 100644 index 0000000000..dd14a940a0 --- /dev/null +++ b/2021/hongloumeng-homophonic-first-appear-sentence/index.html @@ -0,0 +1,636 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 红楼梦谐音梗第一次出现的句子 | 贾攀的流水账 + + + + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

贾攀的流水账

+ +
+

Panmax's Blog

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+ + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ 红楼梦谐音梗第一次出现的句子 +

+ + +
+ + + + +
+ + +
+

以下内容以时间轴顺序进行罗列,不作人名、地名的分类。

+
+

虽我未学,下笔无文,又何妨用假语村言,敷演出一段故事来,亦可使闺阁昭传,复可悦世之目,破人愁闷,不亦宜乎?”故曰“贾雨村云云。

+
    +
  • 贾雨村——假语存
  • +
+

原来女娲氏炼石补天之时,于大荒山**无稽崖炼成高经十二丈,方经二十四丈顽石三万六千五百零一块。娲皇氏只用了三万六千五百块,只单单剩了一块未用,便弃在此山青埂峰**下。谁知此石自经煅炼之后,灵性已通,因见众石俱得补天,独自己无材不堪入选,遂自怨自叹,日夜悲号惭愧。

+
    +
  • 大荒山——荒唐
  • +
  • 无稽崖——无稽
  • +
  • 青梗峰——情恨峰
  • +
+

这阊门外有个十里街,街内有个仁清巷,巷内有个古庙,因地方窄狭,人皆呼作葫芦庙。庙旁住着一家乡宦,姓甄,名费,字士隐。

+
    +
  • 仁清巷——人情巷
  • +
  • 十里街——势利街
  • +
  • 甄士隐——真事隐(去)
  • +
+

因这甄士隐禀性恬淡,不以功名为念,每日只以观花修竹、酌酒吟诗为乐,倒是神仙一流人品。只是一件不足:如今年已半百,膝下无儿,只有一女,乳名唤作英莲,年方三岁。

+
    +
  • 甄英莲——真应怜
  • +
+

后来既受天地精华,复得雨露滋养,遂得脱却草胎木质,得换人形,仅修成个女体,终日游于离恨天外,饥则食蜜青果为膳,渴则饮灌愁海水为汤。

+
    +
  • 蜜青果——觅情果(秘情果)
  • +
+

这士隐正痴想,忽见隔壁葫芦庙内寄居的一个穷儒──姓贾名化、表字时飞、别号雨村者走了出来。这贾雨村原系胡州人氏,也是诗书仕宦之族,因他生于末世,父母祖宗根基已尽,人口衰丧,只剩得他一身一口,在家乡无益,因进京求取功名,再整基业。自前岁来此,又淹蹇住了,暂寄庙中安身,每日卖字作文为生,故士隐常与他交接。

+
    +
  • 贾化——假话
  • +
  • 胡州——胡诌
  • +
+

真是闲处光阴易过,倏忽又是元宵佳节矣。士隐命家人霍启抱了英莲去看社火花灯,半夜中,霍启因要小解,便将英莲放在一家门槛上坐着。待他小解完了来抱时,那有英莲的踪影?急得霍启直寻了半夜,至天明不见,那霍启也就不敢回来见主人,便逃往他乡去了。

+
    +
  • 霍启——祸起
  • +
+

方才在咱门前过去,因见娇杏那丫头买线,所以他只当女婿移住于此。

+
    +
  • 娇杏——侥幸
  • +
+

子兴道:“便是贾府中,现有的三个也不错。政老爹的长女,名元春,现因贤孝才德,选入宫作女史去了。二小姐乃赦老爹之妾所出,名迎春;三小姐乃政老爹之庶出,名探春;四小姐乃宁府珍爷之胞妹,名唤惜春

+
    +
  • 元春、迎春、探春、惜春——原应叹息
  • +
+

只眼前现有二子一孙,却不知将来如何。若问那赦公,也有二子,长名贾琏,今已二十来往了,亲上作亲,娶的就是政老爹夫人王氏之内侄女,今已娶了二年。

+
    +
  • 贾琏——假廉
  • +
+

不假,白玉为堂金作马。
阿房宫,三百里,住不下金陵一个
东海缺少白玉床,龙王来请金陵
丰年好大,珍珠如土金如铁。

+
    +
  • 贾史王薛——假史枉学
  • +
+

门子笑道:“不瞒老爷说,不但这凶犯的方向我知道,一并这拐卖之人我也知道,死鬼买主也深知道。待我细说与老爷听:这个被打之死鬼,乃是本地一个小乡绅之子,名唤冯渊,自幼父母早亡,又无兄弟,只他一个人守着些薄产过日子。

+
    +
  • 冯渊——逢冤
  • +
+

警幻冷笑道:“此香尘世中既无,尔何能知!此香乃系诸名山胜境内初生异卉之精,合各种宝林珠树之油所制,名‘群芳髓’。”宝玉听了,自是羡慕而已。

+
    +
  • 群芳髓——群芳碎
  • +
+

警幻道:“此茶出在放春山遣香洞,又以仙花灵叶上所带之宿露而烹,此茶名曰‘千红一窟’。”宝玉听了,点头称赏。因看房内,瑶琴、宝鼎、古画、新诗,无所不有,更喜窗下亦有唾绒,奁间时渍粉污。

+
    +
  • 千红一窟——千红一哭
  • +
+

更不用再说那肴馔之盛。宝玉因闻得此酒清香甘冽,异乎寻常,又不禁相问。警幻道:“此酒乃以百花之蕊,万木之汁,加以麟髓之醅,凤乳之曲酿成,因名为‘万艳同杯’。”宝玉称赏不迭。

+
    +
  • 万艳同杯——万艳同悲
  • +
+

如尔则天分中生成一段痴情,吾辈推之为‘意淫’。‘意淫’二字,惟心会而不可口传,可神通而不可语达。汝今独得此二字,在闺阁中,固可为良友;然于世道中未免迂阔怪诡,百口嘲谤,万目睚眦。今既遇令祖宁荣二公剖腹深嘱,吾不忍君独为我闺阁增光,见弃于世道,是以特引前来,醉以灵酒,沁以仙茗,警以妙曲,再将吾妹一人,乳名兼美字可卿者,许配于汝。

+
    +
  • 秦可卿——情可轻
  • +
+
+

意淫二字最早也是处于此处

+
+

说着,果然出去带进一个小后生来,较宝玉略瘦些,眉清目秀,粉面朱唇,身材俊俏,举止风流,似在宝玉之上,只是怯怯羞羞,有女儿之态,腼腆含糊,慢向凤姐作揖问好。凤姐喜的先推宝玉,笑道:“比下去了!”便探身一把携了这孩子的手,就命他身傍坐了,慢慢的问他:几岁了,读什么书,弟兄几个,学名唤什么。秦钟一一答应了。

+
    +
  • 秦钟——情种(秦可卿弟弟)
  • +
+

谁知到穿堂,便向东向北绕厅后而去。偏顶头遇见了门下清客相公詹光**单聘仁**二人走来,一见了宝玉,便都笑着赶上来,一个抱住腰,一个携着手,都道:“我的菩萨哥儿,我说作了好梦呢,好容易得遇见了你。”说着,请了安,又问好,劳叨半日,方才走开。

+
    +
  • 詹光——沾光
  • +
  • 单聘仁——擅骗人
  • +
+

可巧银库房的总领名唤吴新登与仓上的头目名戴良,还有几个管事的头目,共有七个人,从帐房里出来,一见了宝玉,赶来都一齐垂手站住。独有一个买办名唤钱华,因他多日未见宝玉,忙上来打千儿请安,宝玉忙含笑携他起来。

+
    +
  • 戴良——大量(戴良是荣府管库头目,暗示贾府生活之靡费)
  • +
  • 钱华——钱花
  • +
+

如今何不用计制伏,又止息口声,又伤不了脸面。”想毕,也装作出小恭,走至外面,悄悄的把跟宝玉的书童名唤茗烟者唤到身边,如此这般,调拨他几句。

+
    +
  • 烟者——明言
  • +
+

衣裳任凭是什么好的,可又值什么,孩子的身子要紧,就是一天穿一套新的,也不值什么。我正进来要告诉你:方才冯紫英来看我,他见我有些抑郁之色,问我是怎么了。我才告诉他说,媳妇忽然身子有好大的不爽快,因为不得个好太医,断不透是喜是病,又不知有妨碍无妨碍,所以我这两日心里着实着急。

+
    +
  • 冯紫英——逢梓音(另一解释:逢知音)
  • +
+

里面凤姐见日期有限,也预先逐细分派料理,一面又派荣府中车轿人从跟王夫人送殡,又顾自己送殡去占下处。目今正值缮国公诰命亡故,王邢二夫人又去打祭送殡,西安郡王妃华诞,送寿礼,镇国公诰命生了长男,预备贺礼,又有胞兄王仁连家眷回南,一面写家信禀叩父母并带往之物,又有迎春染病,每日请医服药,看医生启帖,症源,药案等事,亦难尽述。

+
    +
  • 王仁——忘仁(巧姐的舅舅,凤姐死后想把巧姐卖与技院)
  • +
+

贾蔷又近前回说:“下姑苏聘请教习,采买女孩子,置办乐器行头等事,大爷派了侄儿,带领着来管家两个儿子,还有单聘仁,卜固修两个清客相公,一同前往,所以命我来见叔叔。”贾琏听了,将贾蔷打谅了打谅,笑道:“你能在这一行么?这个事虽不算甚大,里头大有藏掖的。”贾蔷笑道:“只好学习着办罢了。”

+
    +
  • 不固修——不顾羞
  • +
+

贾珍因想着贾蓉不过是个黉门监,灵幡经榜上写时不好看,便是执事也不多,因此心下甚不自在。可巧这日正是首七第四日,早有大明宫掌宫内相戴权,先备了祭礼遣人来,次后坐了大轿,打伞鸣锣,亲来上祭。贾珍忙接着,让至逗蜂轩献茶。贾珍心中打算定了主意,因而趁便就说要与贾蓉捐个前程的话。

+
    +
  • 戴权——大权
  • +
+

此一匾一联书于正殿“大观园”园之名。“有凤来仪”赐名曰“潇湘馆”。“红香绿玉”改作“怡红快绿”即名曰“怡红院”。“蘅芷清芬”赐名曰“蘅芜苑”。

+
    +
  • 潇湘馆——消香馆
  • +
  • 怡红院——遗红怨
  • +
  • 蘅芜苑——恨无缘
  • +
+

这里林黛玉见宝玉去了,又听见众姊妹也不在房,自己闷闷的。正欲回房,刚走到梨香院墙角上,只听墙内笛韵悠扬,歌声婉转。

+
    +
  • 梨香院——离乡怨
  • +
+

贾芸出了荣国府回家,一路思量,想出一个主意来,便一径往他母舅卜世仁家来。原来卜世仁现开香料铺,方才从铺子里来,忽见贾芸进来,彼此见过了,因问他这早晚什么事跑了来。

+
    +
  • 卜世仁——不是人
  • +
+

一时,只见一个小丫头子跑来,见红玉站在那里,便问道:“林姐姐,你在这里作什么呢?”红玉抬头见是小丫头子坠儿。红玉道:“那去?”坠儿道:“叫我带进芸二爷来。”说着一径跑了。这里红玉刚走至蜂腰桥门前,只见那边坠儿引着贾芸来了。

+
    +
  • 坠儿——赘儿、罪儿
  • +
+

丫头方进来时,忽有人来回话:“傅二爷家的两个嬷嬷来请安,来见二爷。”宝玉听说,便知是通判傅试家的嬷嬷来了。那傅试原是贾政的门生,历年来都赖贾家的名势得意,贾政也着实看待,故与别个门生不同,他那里常遣人来走动。

+
    +
  • 傅试——附势
  • +
+

谁知就有一个不知死的冤家,混号儿世人叫他作石呆子,穷的连饭也没的吃,偏他家就有二十把旧扇子,死也不肯拿出大门来。

+
    +
  • 石呆子——实呆子
  • +
+

门下庄头乌进孝叩请爷、奶奶万福金安,并公子小姐金安。新春大喜大福,荣贵平安,加官进禄,万事如意。

+
    +
  • 乌进孝——无进孝
  • +
+

原来贾赦已将迎春许与孙家了。这孙家乃是大同府人氏,祖上系军官出身,乃当日宁荣府中之门生,算来亦系世交。如今孙家只有一人在京,现袭指挥之职,此人名唤孙绍祖,生得相貌魁梧,体格健壮,弓马娴熟,应酬权变,年纪未满三十,且又家资饶富,现在兵部候缺题升。

+
    +
  • 孙绍祖——孙臊祖
  • +
+

因他家多桂花,他小名就唤做金桂。他在家时不许人口中带出金桂二字来,凡有不留心误道一字者,他便定要苦打重罚才罢。

+
    +
  • 夏金桂——下金龟
  • +
+

两人正说着,门上的进来回道:“江南甄老爷到来了。”贾政便问道:“甄老爷进京为什么?”那人道:“奴才也打听了,说是蒙圣恩起复了。”贾政道:“不用说了,快请罢。”那人出去请了进来。那甄老爷即是甄宝玉之父,名叫甄应嘉,表字友忠,也是金陵人氏,功勋之后。原与贾府有亲,素来走动的。

+
    +
  • 甄应嘉——真应假
  • +
+ +
+ + + + + + + +
+ + + + + + +
+ +
+ + + +
+ + + + + + + + +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2021/ip-questions/index.html b/2021/ip-questions/index.html new file mode 100644 index 0000000000..dddb49d3bf --- /dev/null +++ b/2021/ip-questions/index.html @@ -0,0 +1,525 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 关于 IP 的 11 个问题 | 贾攀的流水账 + + + + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

贾攀的流水账

+ +
+

Panmax's Blog

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+ + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ 关于 IP 的 11 个问题 +

+ + +
+ + + + +
+ + +

一个 IPv4 地址有多少位?

32 位

+

比如, 127.0.0.1:

+
1
2
3
4
  127         0    
01111111 00000000
0 1
00000000 00000001
+

192.168.1.0/24 代表哪些 IP 地址?

192.168.1.0 到 192.168.1.255

+

192.168.1.0/24 表示所有与 192.168.1.0 前 24 位相同的地址

+

有 2^(32-24)=256 个这样的 IP 地址

+

这被称为「CIDR 表示法」

+

每个 TCP 数据包都有 IP 地址吗?

是的,有两个

+

下面是 TCP 包的结构:

+
1
2
3
4
5
6
7
8
9
+-+-+-+-+-+-+-+-+--+-
| Ethernet header |
+-+-+-+-+-+-+-+-+--+-
| IP header |
+-+-+-+-+-+-+-+-+--+-
| TCP header |
+-+-+-+-+-+-+-+-+-+-+
| packet contents |
+-+-+-+-+-+-+-+-+-+-+
+

IP 头包含一个源和目的 IP 地址,一个 TTL,一个长度字段,以及一些其他东西。

+

每个 UDP 数据包都有 IP 地址吗?

是的

+

它的结构与 TCP 数据包相同,只是有一个 UDP 头而非 TCP 头。

+

每个 IP 数据包都有端口吗?

不是

+

IP 报头含一个 IP 地址,但没有端口

+

例如,ICMP 数据包(ping 使用的数据包)有一个IP地址,但没有端口

+

TCP 和 UDP 数据包有端口

+

计算机的公网 IP 地址是否允许为10.11.12.13?

不允许

+

以下 3 个 IP 范围是为私有网络保留的:

+
    +
  • 10.0.0.0 – 10.255.255.255
  • +
  • 172.16.0.0 – 172.31.255.255
  • +
  • 192.168.0.0 – 192.168.255.255
  • +
+

127.0.0.0/8 也被保留用于同一台计算机内部的连接。

+

如果你的计算机在本地网络上的 IP 地址是 192.168.1.123,它向 google.com 发送了一个数据包,当数据包到达 google.com 时,数据包上的源 IP 地址是 192.168.1.123 吗?

不是

+

192.168.1.123是本地 IP 地址,所以谷歌将无法联系到你的电脑

+

路由器会改写数据包,将数据包的源 IP 改写为计算机的公网 IP

+

这被称为「网络地址转换」 或 NAT(network address translation)

+

是否可以发送一个 2M 的 IPv4 数据包?

不行

+

IPv4 数据包的长度字段为 16 位,所以最大长度是 65535。

+

在实践中,数据包往往必须比这个数字更小(常见的限制是1500字节)。

+

你的 ISP 的路由器使用数据包的哪一部分将数据包发送到正确的服务器?

目的IP地址

+

它们一般不使用数据包中的任何其他信息。

+

网站如何知道你所在的国家?

根据你的源 IP 地址

+

很多服务使用你的源 IP 地址来确定你在哪个国家。这就是为什么很多人使用 VPN:他们通过 VPN 进行代理,从而使数据包的源 IP 地址在一个不同的国家。

+

将 IP 头中的 TTL 字段被设置为 64 有什么用?

它被允许进行 63 次跳跃

+

这意味着在它完成了大约 63 跳(到 63 个服务器或路由器)之后,就不会被进一步发送。

+

TTL(time to live)字段的存在是为了避免数据包在互联网上陷入循环。

+ +
+ + + + + + + +
+ + + + + + +
+ +
+ + + +
+ + + + + + + + +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2021/logic-loopholes/index.html b/2021/logic-loopholes/index.html new file mode 100644 index 0000000000..764f58905d --- /dev/null +++ b/2021/logic-loopholes/index.html @@ -0,0 +1,607 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 常见逻辑漏洞 | 贾攀的流水账 + + + + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

贾攀的流水账

+ +
+

Panmax's Blog

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+ + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ 常见逻辑漏洞 +

+ + +
+ + + + +
+ + +

五一期间读完了《一本小小的蓝色逻辑书》,这本书不厚,内容很干,总的来说还是比较推荐的。

+

里边的附录部分列出了一些逻辑上的漏洞,我将其汇总如下:

+

+

常见逻辑漏洞

因为语言为题导致的逻辑漏洞

模棱两可

    +
  • 说明:指一个字或词在不同上下文中可以有不同的含义。
  • +
  • 举例:所有地方的赌博活动都应该被合法化,因为我们根本无法避免这件事。赌博就是人生不可分割的一部分。只要一坐到方向盘后面或者说出自己的结婚誓言,我们就是在赌博。
  • +
+

用不同的字眼表达同一个意思

    +
  • 说明:讲话者通过静心选择用词,来为自己的行为辩解,虽然他用语不同,表达的却是同一个意思。
  • +
  • 举例:我没说瞎话,我只是稍微夸大了一下事实罢了。
  • +
+

因为「糟糕」的论据而导致的逻辑漏洞

以偏概全

    +
  • 说明:当我们急于做出总结,只根据一小撮样本就得出某个结论时,就容易犯以偏概全的错误。
  • +
  • 举例:我去过凤凰城三次,每次那里都在下雨所以凤凰城肯定是一座多雨的城市。
  • +
+

循环推理

    +
  • 说明:当我们根据一个前提得出结论,而该结论本身又是前提的前提时,我们就是在犯循环推理错误。
  • +
  • 举例:威利先生的办公桌总是乱七八糟的,因为他这人能力不行。桌面乱说明他思维混乱,这只能说明一件事:他不胜任自己的工作。
  • +
+

反面论据

    +
  • 说明:我们仅仅因为一件事『没有被证明是错的』就断定他是对的,或者『没有被证明是对的』就断定它是错的,我们的判断就出现了逻辑漏洞。
  • +
  • 举例:因为公司里的实习生都没有抱怨过薪水太低,所以我们可以自信地说,公司里的实习生对自己的薪水都很满意。
  • +
+

个人偏好

    +
  • 说明:当我们判断一件事情时,只从个人情绪出发,而不考虑实际情况,我们就是在犯这个偏好错误。
  • +
  • 举例:你怎么能向茜拉咨询婚姻问题呢?难道你不知道她曾因为邮件诈骗而坐过牢吗?
  • +
+

毒水井

    +
  • 说明:当我们过于从一个人的背景,尤其是国籍、种族、性别等来判断其言论时,我们的逻辑就会出问题。
  • +
  • 举例:你说的话怎么能算数呢?你是从悉尼来的,当然会说悉尼比墨尔本好。
  • +
+

你也好不到哪去

    +
  • 说明:如果我们因为一个人也犯了跟我们一样的错误,就拒绝接受对方的观点,那我们的逻辑也会出问题。
  • +
  • 举例:父亲:“孩子,你不该喝酒。喝酒伤肝,整天醉醺醺的怎么行?”
  • +
+

儿子:“老爸,你现在手里不就拿着酒杯吗?”

+

红鲱鱼

    +
  • 说明:当我们在谈话中视图通过转移话题来回避自己的弱点时,我们就是在犯「红鲱鱼」错误。
  • +
  • 举例:(上司对下属):“别跟我说工资太低。我像你这么大的时候,一个星期才赚100美元。”
  • +
+

强求不相关的目标或功能

    +
  • 说明:当我们因为某个规定或计划不能满足某个不相关的目标而拒绝接受它时,我们就在犯这种逻辑错误。
  • +
  • 举例:皮特:“你真的以为学习逻辑就能解决这个世界的问题吗?”
  • +
+

蒂凡尼:“应该不能。”

+

皮特:“那我们干吗浪费时间学它呢?”

+

随心所欲

    +
  • 说明:当我们因为自己极其希望某件事是真的(或假的)就去这么假定时,我们就时在犯「随心所欲」的逻辑错误。
  • +
  • 举例:不管我们的球队之前表现怎么样,这次我们都会在第一轮比赛中打败卫冕冠军。我们的队员都很有信心,铆足了劲儿要大获全胜。
  • +
+

倚老卖老

    +
  • 说明:蒂姆,去安纳波利斯这件事你别太当真了!你们家祖祖辈辈——包括你的父亲、兄弟、祖父、叔父等等——一直都是当兵的,而且以后也会一直留在部队。所以小伙子,你的未来在西点军校!
  • +
+

诉诸公众观点

    +
  • 说明:当我们因为大家都认同某个观点而去接受或支持它们时,就是在犯这个逻辑错误。
  • +
  • 举例:我要给税法修正案投赞成票。根据最近一项民意测验的结果,在25岁以下的登记选民中,超过三分之二的人都支持修正案。
  • +
+

装可怜

    +
  • 说明:利用对方的同情心,而非事实证据。
  • +
  • 举例:你一定要给孤儿院捐款。这些孩子生下来就不知道自己的生身父母是谁,更不要说衣食无忧了。
  • +
+

因为错误的假设而导致的逻辑漏洞

非此即彼

    +
  • 说明:不能因为摆在面前的只有两个选择,就认为其中一个必定是对的。很多事情不是「非此即彼」或「非黑即白」的。
  • +
  • 举例:既然不支持自由贸易,那你一定是支持保护主义了!
  • +
+

中间值

    +
  • 说明:很多人喜欢在两个选择之间取中间值,因为它远离任何一个极端,所以有时这种做法也被称为中庸主义。
  • +
  • 举例:初中老师相信,学校应该安排固定的课程表。而家长们则认为,学生应该可以自由选课。所以最好的办法就是把二者结合起来。
  • +
+

大杂烩

    +
  • 说明:把局部正确的东西拼在一起,未必就能得到一个正确的结果。
  • +
  • 举例:布拉德是个不错的年轻男士,珍妮特是个优秀的女士,他们二人一定是完美的一对儿。
  • +
+

局部错误

    +
  • 说明:不能因为整体是对的,就断定其中的每个部分都是正确的。
  • +
  • 举例:因为车子很重,所以车子上的所有配件都很重。
  • +
+

连续性漏洞

    +
  • 说明:有时人们会觉得一些差异太过渺小,不值得重视,这时就会出现连续性漏洞。
  • +
  • 举例:每天学一个新单词,提升你的词汇量。拿本中型词典,从头开始背。每天背一个新单词,慢慢你就能翻到最后一页。而且更重要的是,你还能学会英语中几乎所有重要单词。但是有几个人能做到这点呢?
  • +
+

以点攻击面

    +
  • 说明:很多人相信,被归纳的东西很容易被驳倒,因为只要找出一个例外就够了。
  • +
  • 举例:学生甲:“众所周知,吸烟会缩短人的寿命。”
  • +
+

学生乙:“是的,可我曾祖父每天一包烟,现在都90多岁了,还是活得好好的,这该怎么解释呢?”

+

扭曲

    +
  • 说明:指故意歪曲对手的观点,从而达到驳倒对手的目的。
  • +
  • 举例:正方:“要想提升发展中国家的教育水平,唯一的办法就是多提供一些物质支持,比如说教科书。”
  • +
+

反方:“你的意思是,不论砍掉多少棵树,都要去印更多课本吗?”

+

错误的类比

    +
  • 说明:我们不能因为两件事在某一方面或某几个方面比较相似(或者不相似),就断定他们在其他方面也相似(或者不相似)。
  • +
  • 举例:说到人工鱼饵,我最喜欢的就是拉帕拉鱼饵。今年夏天的时候,我每次用它都能钓到很多小嘴鲈鱼,所以我秋天去钓鳟鱼时一定还会用它。
  • +
+

因果错误

    +
  • 说明:当我们随意把某件事情归因于某个方向时,我们就是在犯因果错误。
  • +
  • 举例:听说有钱人工作都很努力,所以我要努力工作,让自己变成有钱人。
  • +
+

多米诺错误

    +
  • 说明:我们不能因为一件事会引发另一件事,就断定它会引发随后的一系列事件。也称为「链式反应错误」
  • +
  • 举例:我并不反对给无家可归的人提供免费食物,但既然提供免费食物,我们就会需要提供免费衣服,然后是免费住宿。很快,我们就要给他们提供固定的年薪了。
  • +
+

赌徒谬误

    +
  • 说明:当我们不是根据事实,而只是根据之前发生的事情来判断未来某件事情发生的概率时(而且这两件事完全独立,毫不相干),我们就是在犯赌徒谬误
  • +
  • 局里:(父母对医生说)“因为我们已经有三个男孩了,所以我相信,下一个肯定是女孩。”
  • +
+

错误的精确

    +
  • 说明:只随意举出一些看似精确但其实毫无事实根据的数字来证明自己的观点。
  • +
  • 举例:在莎士比亚的时代,每四个人中就有一个人不喜欢莎士比亚的戏剧。
  • +
+

推理过程中的逻辑错误

断定结果的错误

    +
  • 说明:我们不能因为『当 A 成立时 B 就成立』,就断定『当 B 成立时 A 就成立』。也称为「转化错误」
  • +
  • 举例:每次去度假时,我都会感觉很放松。所以当我感觉很放松时,我就一定是在度假。
  • +
+

否定前项

    +
  • 说明:我们不能因为『只要 A 成立,B就成里』,就断定『只要 A 不成立,B 就不成立』
  • +
  • 举例:每次一下雨,地面就变湿。昨夜没下雨,所以地面不可能变湿。
  • +
+ +
+ + + + + + + +
+ + + + + + +
+ +
+ + + +
+ + + + + + + + +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git "a/2021/logic-loopholes/\345\270\270\350\247\201\351\200\273\350\276\221\346\274\217\346\264\236.png" "b/2021/logic-loopholes/\345\270\270\350\247\201\351\200\273\350\276\221\346\274\217\346\264\236.png" new file mode 100644 index 0000000000..a175e001e5 Binary files /dev/null and "b/2021/logic-loopholes/\345\270\270\350\247\201\351\200\273\350\276\221\346\274\217\346\264\236.png" differ diff --git a/2021/my-ideal-work-env/1.png b/2021/my-ideal-work-env/1.png new file mode 100644 index 0000000000..325b835678 Binary files /dev/null and b/2021/my-ideal-work-env/1.png differ diff --git a/2021/my-ideal-work-env/index.html b/2021/my-ideal-work-env/index.html new file mode 100644 index 0000000000..b8bf0a62f3 --- /dev/null +++ b/2021/my-ideal-work-env/index.html @@ -0,0 +1,536 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 我理想的工作环境 | 贾攀的流水账 + + + + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

贾攀的流水账

+ +
+

Panmax's Blog

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+ + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ 我理想的工作环境 +

+ + +
+ + + + +
+ + +
+

写在前边:我写这篇博客的目的不是为了跳槽,而是为了边写边梳理一下我到底想要什么样的工作环境、我当前的工作状态、今后有哪些规划。

+
+

从15年5月开始算的话,到现在我已经工作6年半了,但是从来没有在所谓的大厂工作过。我觉得原因出在我大学度过的一本书上,书名叫《黑客与画家》。

+

书中的其中一个观点是:「一个非常能干的人待在大公司里可能对他本人是一件很糟糕的事情,因为他的表现被其他不能干的人拖累了」,另一个观点是:「编程语言之间是有优劣之分的,黑客欣赏的语言才是好语言,使用更高级语言的黑客可能比别的程序员更聪明」。(这里的黑客不是大多数人理解的骇客,具体含义可以查阅资料或者阅读本书了解,这里不过多解释)。

+

因为我大学用 Java 和 Python 都开发过不算小的项目,那个时候的 Java 还没有现在的 Spring 全家桶光环,写起业务来相当的繁杂,所以我毕业找工作时,既没想着去大厂,又不想做 Java 开发。基于以上这些情况,我的前两份工作都是选择的规模不大的创业公司,使用的 Python 语言。之后又在第二家公司 Leader 的推荐下到一家 toB 的公司做了几年 Java 开发。再之后又来到探探做 Golang 开发。

+

在我看来,我应该还算比较聪明的那类人,有非常好的自驱能力,工程、架构、沟通能力也不错,所以任职过的这几家公司混的都还不错。上家公司巅峰时实线带40+人,后边做了些有业务调整,我离职时实线也有20来人;探探这边目前虚线10来人。

+

我喜欢追求高效、简洁、优雅的代码风格,这里说题外话:一个我观察到但不一定对的现象,那些把 Leetcode、八股文常挂在嘴边的人,实际开发时编码能力通常好不到哪去。

+

我喜欢读书,每天会读5本左右不同类型的书,而且阅读非常广泛,不限于技术书,以当前在读的举个例子:晚上睡觉前我会读《红楼梦》、《伯恩斯焦虑自助疗法》,早晚上下班通勤的地铁上读《人类简史》,早上到公司后读《Google SRE 工作手册》。我到公司比较早,9点到9.30之间就到公司了,因为其他同事大多10点半后陆续才来,所以到公司后我会有一个多小时的阅读时间。我这么早来公司也是处于想多读书的目的。中午午休时间还会再读一本技术相关的书,最近读的是《Go 专家编程》。

+
+

我会享受学习或完成有挑战事情时的成就感,随着年龄的增长,当我没有取得任何成就时,那种焦虑的感觉就演变成了一种厌恶感。

+
+

另外我习惯于早睡早起,晚上睡得再晚早上也会在7点前起床。

+

(作为一个92年的程序员,是不是生活的有点像老年人?)

+

基于以上这些原因,我想我在选择工作时会比较看重下边几点:

+
    +
  1. 不要内卷,有事做事没事早点下班,晚上下班时间不能晚于8点。
  2. +
  3. 下午6点后不要拉会。
  4. +
  5. 没有大小周。
  6. +
  7. 晚上、周末不要经常性搞聚餐团建之类的(包括团队组织的和个人组织的),一个月不要超过1次。
  8. +
  9. 没有酒桌文化,不搞日报、日站会等幺蛾子的事情。
  10. +
+

以上只是我个人站在对生活的态度上列出来的几点,其他更通用一些的考量肯定还包括团队氛围、领导风格、业务方向、公司战略、薪资不能低于业界水平等。

+

目前看来我当前所在的公司以上几条都是满足条件的,据我所知目前市面上绝大多数大厂都无法满足这些点。

+

不知道没进过大厂算不算是一种遗憾,有时候我甚至会因为没有进过大厂而沮丧和焦虑,觉得自己是个 Loser,如果有符合这些条件的大厂能有幸进去体验一下也是可以的。——但是话又说回来,谁规定进过大厂的人生才是完整的?哪里不是围城呢?

+

我也想进一家小而美的公司,最好是 B、C 轮之后,和一群聪明的人在一个细分领域去做一些有挑战的事。

+

大部分人进大厂是因为大厂给的太多了,但我觉得薪资是一个不应该作为决定性决策,我们上班寻求的应该是整个职业生涯利益的最大化,而不仅仅是最近这一份工作利益的最大化,拿上3、5年的高出业界50%的收入也不能解决太多的问题,更何况还搭上了健康和生活乐趣。我说不能解决问题是出于以下考虑:

+
    +
  • 工作的前5-10年,积累技术财富才是更重要的事,而不是为了更多的现金收入。
  • +
  • 普通程序员很难通过跳槽进入一家大厂来实现财富自由了,大厂所派发的期权和股权暂且不谈价值多少,想拿满就要先卷个三五年。
  • +
  • 薪资只是工作的附属,工作的真正报酬是成长
  • +
+

很多人想进大厂的另一个目的是「镀金」,但是所谓的镀金不也是为了未来能进另一个大厂么,但你现在已经到了大厂,为了镀金开始忍耐着在一群人中卷起来,之后跳到另一家继续卷,还是走不出那个圈子。想要走出这个圈子还是要改变自己的想法,用智慧而不是蛮力去解决问题。

+
+

如果你所在的大厂没有内卷,比如 Apple、AWS 这种外企大厂,就另当别论了。

+
+

当然,我的以上观点可能纯属吃不着葡萄说葡萄酸,没有进过大厂也却是有我自己的原因,因为我从小就讨厌应试教育,在我看来现在的面试也接近应试教育:「面试官清楚自己在问八股文,你也清楚自己在背八股文,你们心照不宣的完成了面试。」我从最开始工作到现在,在面试前没有刷过题(我看面试题的唯一的目的就是用在面试别人上),没有做过刻意的准备,都是看缘分,人家看得上我我就去,看不上就是缘分没到,所以可能也是因为这个原因,通常我在入职后的表现都会超出一些预期,具体表现有这几个:

+
    +
  • 我毕业时去的第一家公司,第二个月就给我加了薪;
  • +
  • 在上家公司每年的绩效都是 A 或 S;
  • +
  • 在探探今年上半年也拿到了 S 绩效。
  • +
+
+

最后,理想和追求的多样化,才是避免内卷的终极方法。

+
+
+

P.S.

+

《黑客与画家》这本的作者叫保罗·格雷厄姆(Paul Graham),是著名创业投资公司 Y Combinator 的创始人,之前开发的 Viaweb 卖给了雅虎,Lisp 传道士。最近我看到了他的博客:http://paulgraham.com/articles.html,里边文章内容质量很高、见解独特,所以我建了一个中文站:https://paulgraham-cn.com/ 来做搬运,作者也在 FAQ 中提到允许翻译成其他语言,只要带上他的原文链接就可以了(见下图)。我会不定期的翻译一篇作者的文章到这个中文站中。

+

+ +
+ + + + + + + +
+ + + + + + +
+ +
+ + + +
+ + + + + + + + +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2021/one-line-python/index.html b/2021/one-line-python/index.html new file mode 100644 index 0000000000..fbe5ca00fb --- /dev/null +++ b/2021/one-line-python/index.html @@ -0,0 +1,551 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 一行 Python 代码能做什么 | 贾攀的流水账 + + + + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

贾攀的流水账

+ +
+

Panmax's Blog

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+ + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ 一行 Python 代码能做什么 +

+ + +
+ + + + +
+ + +

大学中虽然教授过 C、C++、Java,但我当时选择自学了 Python,并且在工作的前几年也是用的 Python,我非常喜欢这门语言。

+

虽然我在平时开发时,不喜欢为了让代码精简些、酷炫些而写出匪夷所思的代码,那些代码大部分杂乱无章、可读性差,第二次再读自己的代码就不一定能读懂。但 Python在这方面做得非常好,这也是为什么它经常成为编码挑战、面试手写代码的首选。

+

下面我对 Python 中用一行代码就能解决的问题做了下整理,通过这些代码片段和技巧也能看出这门语言设计的精妙和优雅:

+

一个数字的位数之和

这个单行代码对于计算一个数字的位数之和非常有用:

+
1
2
3
4
5
6
sum_of_digit = lambda x: sum(map(int, str(x)))
output = sum_of_digit(123)
print("Sum of the digits is: ", output)

Output:
Sum of the digits is: 6
+

单一的if-else条件

在其他语言中,条件式有时看起来有点笨重,如:

+
1
2
3
4
5
6
x = 10
y = 5
if x > y:
print("x is greater than y")
else:
print("y is greater than x")
+

用Python简化它:

+
1
2
3
x = 10
y = 5
print("x is greater than y" if x > y else "y is greater than x")
+

读起来和正常的英语有些像。

+

你可以用以下结构在一行代码内形成一个if语句:

+

<条件-真> if 条件 else <条件-假>

+

多个if-else条件

我们有时会使用大量的 if-else 语句,我们使用 elif 关键字,它是其他语言 else if 关键字组合的缩写,这对于转换为单行的python代码来说比较困难,看一个在代码里面使用 elif 的例子:

+
1
2
3
4
5
6
7
x = 200
if x < 20:
print("x is less than 20")
elif x == 200:
print("x is equal to 200")
else:
print("x is above 20")
+

这段代码将打印第二条语句,即 x is equal to 200

+

现在我们把这段代码转换成一行代码:

+
1
2
x = 200
print("x is less than 20") if x < 20 else print("x is equal to 200") if x == 200 else print("x is above 20")
+

这里使用的依然是上一个技巧,只是将其扩展为了多个条件,不过我不建议这样写,它也许很快就会变得难以阅读和维护。你需要权衡好什么时候使用这个技巧。

+

字符串反转

使用字符串切片操作,在一行代码中反转字符串:

+
1
2
3
4
5
6
input_string = "Namaste World!"
reversed_string = input_string[::-1]
print("Reversed string is: ", reversed_string)

Output:
Reversed string is: !dlroW etsamaN
+

分配多个变量

在一行中为每个变量分配不同的值,甚至不同的数据类型:

+
1
name, age, single = ‘jc’, 35, False
+

列表推导

列表推导是一种简单而优雅的方法,它可以从现有的列表中定义和生成新的列表:

+

举个例子,生成填充了数字 0 到 4 的列表:

+
1
2
3
4
scores = []
for x in range (5):
scores.append(x)
print(scores)
+

同样的结果我们可以用列表推导法来实现:

+
1
2
scores = [x for x in range(5)]
print(scores)
+

这是 Python 最伟大的功能之一。

+

列表推导中的条件式

继续扩展上一个技巧,如果我们想根据一个条件来跳过一些项呢?

+

例如,如果我们只想要奇数:

+
1
2
3
4
5
scores = []
for x in range (20):
if x % 2 == 1:
scores.append(x)
print(scores)
+

在列表推导中使用条件语句同样可以实现:

+
1
2
scores = [x for x in range(20) if x % 2 == 1]
print(scores)
+

优点:列表推理不仅更清晰,而且在大部分情况下其性能也比单次循环好得多。

+

斐波那契数列

斐波那契数列是一组数字的集合,其中每个数字都是它前面两个数字之和。

+

在一行代码中,我们使用列表推理和 for 循环生成一个斐波那契数列:

+
1
2
3
4
5
6
7
n=10
fib = [0,1]
[fib.append(fib[-2]+fib[-1]) for _ in range(n)]
print(fib)

Output:
[0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89]
+

合并两个字典

我们可以使用**操作符在一行代码中合并多个字典。

+

我们只需要将字典和**操作符一起传给{},它就会为我们合并字典:

+
1
2
3
4
5
6
7
dictionary1 = {"name": "Joy", "age": 25}
dictionary2 = {"name": "Joy", "city": "New York"}
merged_dict = {**dictionary1, **dictionary2}
print("Merged dictionary:", merged_dict)

Output:
Merged dictionary: {'name': 'Joy', 'age': 25, 'city': 'New York'}
+

交换一个字典中的键和值

1
2
3
4
5
dict = {'Name': 'Joy', 'Age': 25, 'Language':'Python'}
result = {v:k for k, v in dict.items()}
print(result)
Output:
{'Joy': 'Name', 25: 'Age', 'Python': 'Language'}
+

这个交换键值对的代码非常实用。

+

交换变量

在其他语言中,交换两个变量需要借助第三个变量(一个临时变量)来实现:

+
1
2
3
tmp = var1
var1 = var2
var2 = tmp
+

在Python中,可以直接在一条语句中完成:

+
1
var1, var2 = var2, var1
+

甚至更进一步,可以使用相同的技巧来交换数组中的元素

+
1
2
3
4
5
6
colors = ['red', 'green', 'blue']
colors[0], colors[1] = colors[1], colors[0]
print(colors)

Output:
['green', 'red', 'blue']
+

列表推理中的嵌套循环

列表推理还可以用在矩阵(多维数组)上:

+
1
2
3
4
5
my_list = [(x, y) for x in [3, 4, 6] for y in [3, 4, 7] if x != y]
print(my_list)

Output:
[(3, 4), (3, 7), (4, 3), (4, 7), (6, 3), (6, 4), (6, 7)]
+

字典推理

与列表推理的概念相同,举个例子,我们需要一个键/值对,其中值是键的平方:

+
1
2
3
4
square_dict = dict()
for num in range(1, 11):
square_dict[num] = num * num
print(square_dict)
+

下面使用字典推导:

+
1
2
square_dict = { num: num * num for num in range(1, 11) }
print(square_dict)
+

拍平一个列表

数据工程师经常与列表和多维数据打交道,有时他们需要将多维列表转换成一维的。他们经常使用 numpy 之类的包来做这件事。

+

下面的例子展示了如何使用纯 Python 的单行代码来完成同样的工作:

+
1
2
3
4
5
6
my_list = [[1,2], [4, 6], [8, 10]]
flattened = [i for j in my_list for i in j]
print(flattened)

Output:
[1, 2, 4, 6, 8, 10]
+

是的,这依然时列表推导的一种应用。

+

从列表中解构变量

假设你有一个列表,你想把它前边的几个值捕捉到变量中,其余值都放进另一个列表。这在处理参数的时候会很有用。

+

让我们看一个例子:

+
1
2
3
4
5
x, y, *z = [1, 2, 3, 4, 5]
print(x, y, z)

Output:
1 2 [3, 4, 5]
+

将文件加载到一个列表中

脚本最常用的一个场景是处理文本文件,特别是将文件的每一行读入到列表中,这样我们就可以对数据进行我们需要的操作了。

+

在 Python 中,我们可以用强大的列表推导法将文件所有行读入一个列表:

+
1
2
my_list = [line.strip() for line in open('countries.txt', 'r')]
print(my_list)
+

总结

Python 是一门神奇的语言。今天我展示了几个强大的 Python 技巧,它们将帮助我们开发更优雅、更简单、更高效的代码。

+ +
+ + + + + + + +
+ + + + + + +
+ +
+ + + +
+ + + + + + + + +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2021/quick-start-static-file-service/1.png b/2021/quick-start-static-file-service/1.png new file mode 100644 index 0000000000..5b6bbd3d63 Binary files /dev/null and b/2021/quick-start-static-file-service/1.png differ diff --git a/2021/quick-start-static-file-service/2.png b/2021/quick-start-static-file-service/2.png new file mode 100644 index 0000000000..47fc70f4f8 Binary files /dev/null and b/2021/quick-start-static-file-service/2.png differ diff --git a/2021/quick-start-static-file-service/index.html b/2021/quick-start-static-file-service/index.html new file mode 100644 index 0000000000..5b102139af --- /dev/null +++ b/2021/quick-start-static-file-service/index.html @@ -0,0 +1,507 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 快速搭建一个静态文件服务 | 贾攀的流水账 + + + + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

贾攀的流水账

+ +
+

Panmax's Blog

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+ + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ 快速搭建一个静态文件服务 +

+ + +
+ + + + +
+ + +
+ +

前几天朋友问我如何在没有 root 权限且无法编辑 Nginx 配置的条件下,搭建一个静态文件服务。

+

我最快想到的是用 Go 写一个程序,直接在目标机器上执行 Go 编译好的二进制文件,应该不会超过 10 行代码。

+

本着不重复造轮子的想法(另一个主要原因是朋友当时非常着急),尝试找了找前人造好的轮子,于是找到了这个项目:https://github.com/philippgille/serve

+

看了一下介绍,和我的想法相同,同时提供了便于扩展的参数,而且提供了各个平台编译好的可执行文件。

+

安装

Windowns 安装

1
scoop install serve
+

Mac 安装

1
brew install philippgille/tap/serve
+

Linux 安装

1
2
wget https://download.jpanj.com/serve_v0.3.2_Linux_x64.zip
unzip serve_v0.3.2_Linux_x64.zip .
+

运行

检查 serve 版本

1
2
$ serve -v
serve version: v0.3.2
+

服务监听 8100 端口并将当前目录作为静态文件根目录

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
$ serve -p 8100

Serving "." on all network interfaces (0.0.0.0) on HTTP port: 8100

Local network interfaces and their IP addresses so you can pass one to your colleagues:
Interface | IPv4 Address | IPv6 Address
---------------------|-----------------|----------------------------------------
lo | 127.0.0.1 | ::1
eth0 | xxx.xxx.xx.xxx | fe80::a8aa:ff:fe11:fb4b
docker0 | 172.17.0.1 | fe80::42:1dff:fe8b:c5fe
br-556c0122836a | 172.24.0.1 | fe80::42:92ff:fe47:2965
vethf6d94b9 | | fe80::dcdf:59ff:fe90:c6e3
vethf64d185 | | fe80::286e:3eff:fe12:eaf9
veth7259f52 | | fe80::34d3:83ff:fe97:612c
veth0e57d69 | | fe80::c007:bff:fe1b:2b7b

You probably want to share:
http://xxx.xxx.xx.xxx:8100
+

-d 指定文件根目录

1
$ serve -p 8100 -d "/opt"
+

-a 启用 Basic 认证

1
$ serve -p 8100 -d "/opt" -a "test:test"
+
+ +

-s 生成一个自签名证书(7天有效期),开启 https 协议

1
$ serve -p 8100 -d "/opt" -a "test:test" -s
+

-b 指定监听的网络接口,默认为 0.0.0.0

1
$ serve -p 8100 -d "/opt" -a "test:test" -b "0.0.0.0"
+

-h 查看帮助文档

1
2
3
4
5
6
7
8
9
10
11
12
13
14
$ serve -h
Usage of serve:
-a string
Require basic authentication with the given credentials (e.g. -a "alice:secret")
-b string
Bind to (listen on) a specific interface. "0.0.0.0" is for ALL interfaces. "localhost" disables access from other devices. (default "0.0.0.0")
-d string
The directory of static files to host (default ".")
-h Print the usage
-p string
Port to serve on. 8080 by default for HTTP, 8443 for HTTPS (when using the -s flag) (default "8080")
-s Serve via HTTPS instead of HTTP. Creates a temporary self-signed certificate for localhost, 127.0.0.1, <hostname>.local, <hostname>.lan, <hostname>.home and the determined LAN IP address
-t Test / dry run (just prints the interface table)
-v Print the version
+ +
+ + + + + + + +
+ + + + + + +
+ +
+ + + +
+ + + + + + + + +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2021/recovery-form-painc-in-golang/index.html b/2021/recovery-form-painc-in-golang/index.html new file mode 100644 index 0000000000..2c76ed7301 --- /dev/null +++ b/2021/recovery-form-painc-in-golang/index.html @@ -0,0 +1,507 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 从 Go 语言的 panic 中恢复 | 贾攀的流水账 + + + + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

贾攀的流水账

+ +
+

Panmax's Blog

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+ + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ 从 Go 语言的 panic 中恢复 +

+ + +
+ + + + +
+ + +

什么是 painc?

+

Panic 是 Go 语言中一个内置函数,它会中断正常的控制流并开始 panic 流程。当函数 F 调用 panic 时,F 的执行停止,F 中的任何延迟函数(deferred function)都被正常执行,然后 F 返回给它的调用者。对于调用者来说,F 的行为就像对 panic 的调用。这个过程继续在堆栈中进行,直到当前 goroutine 中的所有函数都返回,这时程序就会崩溃。painc 可以通过直接调用 panic 函数来启动,也可以由运行时错误引起,如数组越界。

+
+

简单地说,painc 使一个函数不执行其预期的流程,并可能导致整个程序退出。

+

解决方案

Go 原生提供了一些功能,可以帮助我们从这种情况下恢复。

+

Defer

Go 的 defer 语句安排了一个函数:这个函数在执行 defer 的函数返回之前立即运行。

+

我们称 defer 调用的函数为:延迟函数

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// Contents 将文件内容以字符串形式返回。
func Contents(filename string) (string, error) {
f, err := os.Open(filename)
if err != nil {
return "", err
}
defer f.Close() // f.Close 将在函数完成后执行

var result []byte
buf := make([]byte, 100)
for {
n, err := f.Read(buf[0:])
result = append(result, buf[0:n]...)
if err != nil {
if err == io.EOF {
break
}
return "", err // 如果我们从这里返回,f 会被安全关闭
}
}
return string(result), nil // 如果我们从这里返回,f 会也会被安全关闭
}
+

Recover

panic 被调用时,它立即停止执行当前函数,并沿 goroutine 的堆栈运行所有延迟函数。

+

recover 的调用会终止 panic,并返回传递给 panic 的参数。recover 只在延迟函数中有效,因为 panic 后唯一能够运行的代码在延迟函数中。

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
func server(workChan <-chan *Work) {
for work := range workChan {
go safelyDo(work)
}
}

func safelyDo(work *Work) {
defer func() {
if err := recover(); err != nil {
log.Println("work failed:", err)
}
}()
do(work)
}
+

实现

让我们来实现一个简单的数学函数,它可以将两个数字相除,如果分母是 0,就会 panic Divide by zero error!

+

下边的函数检查分母的值,如果它是 0,就会 panic。

+
1
2
3
4
5
func checkForError(y float64) { 
if y == 0 {
panic("Divident cannot be 0! Divide by 0 error.")
}
}
+

下边这个函数负责对提供的数字进行除法操作并返回,同时它使用上面定义的函数来检查分母是否为 0。

+

由于 checkForError 会破坏流程,因此这个函数实现了recover()defer,以便在发生 panic 时返回 0。

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
func safeDivision(x, y float64) float64 { 
var returnValue float64
defer func() {
if err := recover(); err != nil {
fmt.Println("Panic occured:", err)
fmt.Println("Returning safe values")
returnValue = 0 }
}()
checkForError(y)

returnValue = x / y

return returnValue
}
+

将上边的代码组合起来:

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
package main

import (
"fmt"
)

func main() {
fmt.Println("Pre panic execution")
value1 := safeDivision(2, 0)
fmt.Println("Post panic execution, -> ", value1)
fmt.Println("Pre valid execution")
value2 := safeDivision(2, 1)
fmt.Println("Post valid execution, value -> ", value2)
}

func safeDivision(x, y float64) float64 {
var returnValue float64
defer func() {
if err := recover(); err != nil {
fmt.Println("Panic occured:", err)
fmt.Println("Returning safe values")
returnValue = 0
}
}()
checkForError(y)

returnValue = x / y

return returnValue
}

func checkForError(y float64) {
if y == 0 {
panic("Divident cannot be 0! Divide by 0 error.")
}
}
+

输出为:

+
1
2
3
4
5
6
Pre panic execution
Panic occured: Divident cannot be 0! Divide by 0 error.
Returning safe values
Post panic execution, -> 0
Pre valid execution
Post valid execution, value -> 2
+ +
+ + + + + + + +
+ + + + + + +
+ +
+ + + +
+ + + + + + + + +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2021/redis-slow-checklist/1.png b/2021/redis-slow-checklist/1.png new file mode 100644 index 0000000000..93d7818397 Binary files /dev/null and b/2021/redis-slow-checklist/1.png differ diff --git a/2021/redis-slow-checklist/index.html b/2021/redis-slow-checklist/index.html new file mode 100644 index 0000000000..649027805d --- /dev/null +++ b/2021/redis-slow-checklist/index.html @@ -0,0 +1,531 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Redis 性能变慢时的 Checklist | 贾攀的流水账 + + + + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

贾攀的流水账

+ +
+

Panmax's Blog

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+ + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ Redis 性能变慢时的 Checklist +

+ + +
+ + + + +
+ + +
    +
  1. 获取 Redis 实例在当前环境下的基线性能。
  2. +
  3. 是否用了慢查询命令?
      +
    • 如果是的话,就使用其他命令替代慢查询命令,或者把聚合计算命令放在客户端做。
    • +
    +
  4. +
  5. 是否对过期 key 设置了相同的过期时间?
      +
    • 对于批量删除的 key,可以在每个 key 的过期时间上加一个随机数,避免同时删除。
    • +
    +
  6. +
  7. 是否存在 bigkey?
      +
    • 对于 bigkey 的删除操作,如果你的 Redis 是 4.0 及以上的版本,可以直接利用异步线程机制减少主线程阻塞;如果是 Redis 4.0 以前的版本,可以使用 SCAN 命令迭代删除;
    • +
    • 对于 bigkey 的集合查询和聚合操作,可以使用 SCAN 命令在客户端完成。
    • +
    +
  8. +
  9. Redis AOF 配置级别是什么?业务层面是否的确需要这一可靠性级别?
      +
    • 如果我们需要高性能,同时也允许数据丢失,可以将配置项 no-appendfsync-on-rewrite 设置为 yes,避免 AOF 重写和 fsync 竞争磁盘 IO 资源,导致 Redis 延迟增加。
    • +
    • 如果既需要高性能又需要高可靠性,最好使用高速固态盘作为 AOF 日志的写入盘。
    • +
    +
  10. +
  11. Redis 实例的内存使用是否过大?发生 swap 了吗?
      +
    • 如果是的话,就增加机器内存,或者是使用 Redis 集群,分摊单机 Redis 的键值对数量和内存压力。
    • +
    • 同时,要避免出现 Redis 和其他内存需求大的应用共享机器的情况。
    • +
    +
  12. +
  13. 在 Redis 实例的运行环境中,是否启用了透明大页机制?
      +
    • 如果是的话,直接关闭内存大页机制就行了。
    • +
    +
  14. +
  15. 是否运行了 Redis 主从集群?
      +
    • 如果是的话,把主库实例的数据量大小控制在 2~4GB,以免主从复制时,从库因加载大的 RDB 文件而阻塞。
    • +
    +
  16. +
  17. 是否使用了多核 CPU 或 NUMA 架构的机器运行 Redis 实例?
      +
    • 使用多核 CPU 时,可以给 Redis 实例绑定物理核
    • +
    • 使用 NUMA 架构时,注意把 Redis 实例和网络中断处理程序运行在同一个 CPU Socket 上
    • +
    +
  18. +
+
+

备注:

+

内存大页机制:Linux 内核从 2.6.38 开始支持内存大页机制,该机制支持 2MB 大小的内存页分配,而常规的内存页分配是按 4KB 的粒度来执行的。

+

NUMA 架构:在主流的服务器上,一个 CPU 处理器会有 10 到 20 多个物理核。同时,为了提升服务器的处理能力,服务器上通常还会有多个 CPU 处理器(也称为多 CPU Socket),每个处理器有自己的物理核(包括 L1、L2 缓存),L3 缓存,以及连接的内存,同时,不同处理器间通过总线连接。

+

+ +
+ + + + + + + +
+ + + + + + +
+ +
+ + + +
+ + + + + + + + +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2021/struct-method-value-vs-pointer/index.html b/2021/struct-method-value-vs-pointer/index.html new file mode 100644 index 0000000000..cf4005b16a --- /dev/null +++ b/2021/struct-method-value-vs-pointer/index.html @@ -0,0 +1,525 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Go 结构体方法值传递与引用传递区别 | 贾攀的流水账 + + + + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

贾攀的流水账

+ +
+

Panmax's Blog

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+ + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ Go 结构体方法值传递与引用传递区别 +

+ + +
+ + + + +
+ + +

Go 结构体方法中,有一个很重要的点就是值传递和引用传递,我们通过一个例子来看下什么是值传递、什么是引用传递,二者有什么区别。

+

我们声明 person 结构体,里边有一个 name 字段,用两种方式实现 SetName 方法,分别是值传递和引用传递。

+
1
2
3
4
5
6
7
8
9
10
11
type person struct {
name string
}

func (p person) SetName1(name string) {
p.name = name
}

func (p *person) SetName2(name string) {
p.name = name
}
+

如上,SetName1 就是值传递,SetName2 为引用传递。

+

main 方法中,我们分别调用两个 SetName 方法对 name 进行赋值,并打印每次赋值后的 name 值。

+
1
2
3
4
5
6
7
8
9
func main() {
p := &person{}

p.SetName1("张三")
fmt.Println(p.name)

p.SetName2("李四")
fmt.Println(p.name)
}
+

执行这个程序得到如下结果:

+
1
2

李四
+

可以看到 张三 并没有打印出来,而 李四 打印了出来。

+

在调用 SetName1 时,实际上是复制了一个新的 person,方法内操作的也是那个新 person 把复制 personname 改为了张三,而我们在 main 方法中打印的确是原始 personname 字段。因为我们在初始化 person 时并没有指定 name 的值,所以第一次打印出来的是个空串。

+

对程序稍作调整,先调用 SetName2 再调用 SetName1

+
1
2
3
4
5
6
7
8
9
func main() {
p := &person{}

p.SetName2("李四")
fmt.Println(p.name)

p.SetName1("张三")
fmt.Println(p.name)
}
+

这时输出的结果为:

+
1
2
李四
李四
+

第一次调用后,p 结构体指针中 name 的值已经被改为了 李四,接下来我们调用 SetName1 时,因为是复制了一个新的 person,并没有影响之前的 person,所以打印结果还是 李四

+

**我们日常开发中,编写结构体方法时大部分情况都是用引用传递。

+

值传递的问题是,如果我们结构体的成员数量非常多时,每次调用方法都会进行一次拷贝,会有额外的内存开销。

+

验证一下值传递有没有分配新的内存:

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
type person struct{
name string
}

func (p person) SetName1(name string) {
fmt.Printf("SetName1: %p\n", &p)
p.name = name
}

func (p *person) SetName2(name string) {
fmt.Printf("SetName2: %p\n", p)
p.name = name
}

func main() {
p := &person{}
fmt.Printf("Origin: %p\n", p)

p.SetName1("张三")
p.SetName2("李四")
}
+

运行结果:

+
1
2
3
Origin: 0xc000010200
SetName1: 0xc000010210
SetName2: 0xc000010200
+

可以看到,原始的 person 地址为 0xc000010200,在通过值传递时位置发生了改变,变为了0xc000010210,这也就意味着系统为这个新的 person 分配了新的内存地址,而用引用传递的方式地址是不会变的。

+

引用传递容易犯的错误

我们假设要实现一个发送邮件的功能,定义一个 email 结构体,里边有两个成员 fromto,实现两个方法用来更新这两个成员变量。

+
1
2
3
4
5
6
7
8
9
10
11
12
type email struct {
from string
to string
}

func (e *email) SetFrom(from string) {
e.from = from
}

func (e *email) SetTo(to string) {
e.to=to
}
+

再实现一个发送邮件的方法,这里简单将 fromto 打印出来即可:

+
1
2
3
func (e *email) Send() {
fmt.Printf("from: %s, to: %s\n", e.from, e.to)
}
+

main 方法中,我们写一个循环,实现发送 10 次邮件,0 发送给 1,1 发送个 2,一次以此类推:

+
1
2
3
4
5
6
7
8
9
func main() {
e: = &email{}

for i:=0; i<10; i++ {
e.SetFrom(fmt.Sprintf("%d", i))
e.SetTo(fmt.Sprintf("%d", i+1))
e.Send()
}
}
+

输出如下:

+
1
2
3
4
5
6
7
8
9
10
from: 0, to: 1
from: 1, to: 2
from: 2, to: 3
from: 3, to: 4
from: 4, to: 5
from: 5, to: 6
from: 6, to: 7
from: 7, to: 8
from: 8, to: 9
from: 9, to: 10
+

这时候,如果我们改成并发发送这些邮件,同时发给10个人,很容易就会把上边的代码改写如下:

+
1
2
3
4
5
6
7
8
9
10
11
12
13
func main() {
e := &email{}

for i:=0; i<10; i++ {
go func(i int) {
e.SetFrom(fmt.Sprintf("%d", i))
e.SetTo(fmt.Sprintf("%d", i+1))
e.Send()
}(i)
}

time.Sleep(1 * time.Second)
}
+

再次运行,结果如下:

+
1
2
3
4
5
6
7
8
9
10
from: 2, to: 3
from: 1, to: 2
from: 3, to: 4
from: 4, to: 5
from: 5, to: 6
from: 6, to: 7
from: 0, to: 1
from: 7, to: 8
from: 9, to: 1
from: 8, to: 9
+

没有按照递增的顺序发送是在我们意料之中的,但是我们可以看到其中有一行输出为:from: 9, to: 1,这个并不是我们想要的结果。

+

出现这个问题的原因是在每个 go routine 中都是对原始 email 进行的修改,再并发操作的过程中,fromto 有可能被其他的 go routine 改掉,这是个非常严重的 bug。

+

两种修改方法

每一次都初始化一个新的 email 结构体:

1
2
3
4
5
6
7
8
9
10
11
12
func main() {
for i:=0; i<10; i++ {
e := &email{}
go func(i int) {
e.SetFrom(fmt.Sprintf("%d", i))
e.SetTo(fmt.Sprintf("%d", i+1))
e.Send()
}(i)
}

time.Sleep(1 * time.Second)
}
+

改用值传递,并返回修改后的结构体

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
package main

import(
"fmt"
"time"
)

type email struct {
from string
to string
}

func (e email) SetFrom(from string) email {
e.from = from
return e
}

func (e email) SetTo(to string) email {
e.to = to
return e
}

func (e email) Send() {
fmt.Printf("from: %s, to: %s\n", e.from, e.to)
}

func main() {
e := &email{}

for i:=0; i<10; i++ {
go func(i int) {
e.SetFrom(fmt.Sprintf("%d", i)).
SetTo(fmt.Sprintf("%d", i+1)).
Send()
}(i)
}

time.Sleep(1 * time.Second)
}
+

读者可以自己想一想,为什么这种写法可以解决并发赋值出现的问题。

+ +
+ + + + + + + +
+ + + + + + +
+ +
+ + + +
+ + + + + + + + +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2021/tcp-questions/index.html b/2021/tcp-questions/index.html new file mode 100644 index 0000000000..9777e7b1ef --- /dev/null +++ b/2021/tcp-questions/index.html @@ -0,0 +1,526 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 关于 TCP 的 14 个问题 | 贾攀的流水账 + + + + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

贾攀的流水账

+ +
+

Panmax's Blog

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+ + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ 关于 TCP 的 14 个问题 +

+ + +
+ + + + +
+ + +

每个TCP数据包都有一个源/目的IP地址吗?

是的

+

下边是一个TCP数据包的结构

+
1
2
3
4
5
6
7
8
9
+-+-+-+-+-+-+-+-+--
| Ethernet header |
+-+-+-+-+-+-+-+-+--
| IP header |
+-+-+-+-+-+-+-+-+--
| TCP header |
+-+-+-+-+-+-+-+-+-+
| packet contents |
+-+-+-+-+-+-+-+-+-+
+

这就是为什么我们称它为 「TCP/IP」– TCP数据包总是有一个 IP 头。

+

每个TCP数据包都有一个端口号吗?

是的

+

TCP 头中的端口字段为16位。

+

TCP连接中发送的第一个数据包是什么?

SYN

+

每个TCP连接都是以三次向握手开始的:

+
    +
  • client: SYN
  • +
  • server: SYN + ACK
  • +
  • client: ACK
  • +
+

如果防火墙拦截了连接,TCP握手能否完成?

不能

+

被防火墙拦截的常见症状是 SYN 数据包发出后无 ACK 返回。

+

电子邮件使用TCP吗?

是的

+

电子邮件是通过SMTP发送的,它使用了 TCP。

+

FTP、POP3、HTTP/1、HTTP/2、websockets 等很逗互联网协议也都使用了 TCP。

+

TCP连接是双向的吗?(客户端和服务器都能发送消息吗?)

是的

+

一个HTTP 请求/响应 是个 TCP 连接 —— 客户端发送 HTTP 请求,然后服务器发送响应。

+

如果服务器发送响应给客户端,客户端能否发送更多数据?

是的

+

例如,websockets 使用 TCP 使客户端和服务器根据需要来回发送数据。

+

如果你通过TCP发送一个长消息,是否会被分成多个数据包?

是的

+

数据包有最大限制(通常是1500字节),所以需要将一个 TCP 消息分割成许多数据包。

+

当你发送 TCP 数据包时,数据包是否可以按照你发送的顺序到达目的地?

不是

+

无法确保网络数据包会按照你发送的顺序到达。

+

负责理解TCP协议的代码在哪?

通常是在操作系统中

+

Linux、Mac、Windows等都有 TCP 实现。如果你愿意,也可以编写自己的TCP实现。

+

你的操作系统使用 TCP 数据包中的哪个字段将数据包按正确顺序排列?

序列号(sequence number)

+

数据包的序列号告诉你它在整个数据流中的位置。序列号计算的是字节数,而不是包数。

+

下面是一组数据包内容和序列号的例子(每个字符是1个字节):

+
1
2
3
4
5
6
7
8
9
+-+-+-+-+-+-+-+-+-+-+-
| contents | seq # |
+-+-+-+-+-+-+-+-+-+-+-
| hello I | 0 |
+-+-+-+-+-+-+-+-+-+-+-
| x! | 15 |
+-+-+-+-+-+-+-+-+-+-+-
| 'm panma | 7 |
+-+-+-+-+-+-+-+-+-+-+-
+

如果按照正确的顺序排列,这些数据包将重新排列为“hello I’m panmax!”

+

顺序号总是从0开始吗?

不是

+

通常连接中的第一个序列号是一个很大的数字,比如:1737132723,其他序列号都是与这个数字相对的。所以在计算时需要减去第一个序列号。

+

如果你在 Wireshark/tcpdump 中查看TCP数据包,它们会进行减法处理,使其看起来像序列号从0开始,让人类更易阅读。

+

如果你发送一个 TCP 数据包,如何知道它被接收?

你会收到一个ACK

+

例如,服务器收到了一个序列号为1200的数据包,并且也已收到所有在它之前的数据包,服务器会发送一个序列号为1200的ACK数据包。

+

被丢弃的TCP数据包是否会被重试发送?

是的

+

如果客户端(或服务器)在一定时间内没有收到其发送数据包的ACK,它将会重试发送该数据包。

+ +
+ + + + + + + +
+ + + + + + +
+ +
+ + + +
+ + + + + + + + +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2021/thanks-myself-unperfect/index.html b/2021/thanks-myself-unperfect/index.html new file mode 100644 index 0000000000..4e0d3b2ba9 --- /dev/null +++ b/2021/thanks-myself-unperfect/index.html @@ -0,0 +1,579 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 《感谢自己的不完美》摘抄 | 贾攀的流水账 + + + + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

贾攀的流水账

+ +
+

Panmax's Blog

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+ + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ 《感谢自己的不完美》摘抄 +

+ + +
+ + + + +
+ + +

感谢自己的不完美

一个人的心里健康程度与接纳痛苦的程度成正比。

+

改变恶习最关键的一点是:不和恶习较劲,接受恶习。因为,积习就是你的本性,恶习代表着你内心的需要,你只有理解它并接受它,它才能得到最有效的改造。

+

每一种习惯的形成都必然会经历以下这个循环:行为发生—得到奖励—强化

+

成人需要同时进行几件事情,而且必须为自己的行为负责。这个时候,成人就必须有“延迟满足”这个意识。

+

真正能自控的人是内心和谐的人,他们将自己内心的每一部分需求都当作朋友来看待,这样每一部分都不会捣乱。这样的人不是试图控制或压制一些缺点,而总能从它们当中找到正面的信息。

+

当你真正想做一件事情时,动力会从内心自动产生,你自然会自律。不要从外界去寻找迫使你改变习惯的东西,因为它们很容易被你放弃。

+

增强自控力的唯一根本在于要找到你真正爱做的事情是什么,真正想成为怎样的人,也就是要找到你的人生使命。

+

改变恶习仍需要一点:立即去做。因为,每一个旧习惯对应着的神经回路是无法消失的,只能靠新习惯打造更强大的新神经回路,用新的神经回路去战胜旧的神经回路。

+

养成新习惯的策略:

+
    +
  1. 从最容易的事情开始
  2. +
  3. 每天必须做一件事情
  4. +
  5. 每天必须不做一件事情
  6. +
  7. 不要积累太多的未完成的事情
  8. +
  9. 有决定胜过没决定
  10. +
+

人生最大的痛苦莫过于知道该怎么做却没有去做,你会自责,你会对自己不满意,你会觉得自己是渺小的、不讲信誉不可信的。总而言之,就是你开始不信任自己,自信心降低了。

+

痛苦时,不要只沉浸在痛苦中,或者以寻找刺激的方式来降低或麻木自己的痛苦,而要思考一下“我为什么这么痛苦,我重复了童年的什么体验?”

+

自己的惧怕与愤怒是建立在有限的人生体验上的,是不合理的。

+

痛苦背后的问题恰恰是我们的一部分,须臾不可分离,根本逃避不了。所谓的逃避,只不过是运用种种自欺的方式扭曲了我们对问题的认识,从而减少我们的痛苦。我们以为看不到它们了,但其实它们还是我们甩不掉的尾巴。

+

潜意识的特点是,我们越想控制它,就越控制不了,它的活动会越来越频繁。

+

不要总是和潜意识过不去,不必和走神、坏念头等偶尔出现的问题较真。否则,它们就会成为真正的问题。

+

按照存在主义哲学,只要你渴望触及人类、社会乃至世界的真相,那么你会一直焦虑下去。因为,不管成长到哪一层次,你一定会发现新的局限性,这时焦虑就势必会发生。所以,许多哲人越深入这个世界,就越明白自己无知。从这一点而言,焦虑是推动我们认识世界的动力

+

一个人在原生家庭中的关系决定了这个人的心理健康程度。

+

人生的悲剧本身并不一定会导致心理问题,它之所以最后令我们陷入困境,是因为我们想否认自己人生的悲剧性。

+

我们的力量不在于我们看上去有多快乐,而在于我们的心离我们的人生真相有多近。

+

作为一个人,我们必须深入地探讨自己经历过的所有事件以及教训,只有在这个深度上我们才能发现我们自己的真实,找到自己的决策能力。

+

一个总是不断诞生强人的社会,必然是一个失序与窒息不断轮回的社会。

+

对世界而言,控制欲望是万恶之源。对个人而言,控制欲望是万病之源。

+

强人们其实首先想控制自己内心的失序,但他们做不到,于是他们去追求控制别人。他们内心越失序,就越渴望控制更多的人。最终,不管他们意识上的目的是什么,制造的或留下的多是苦难。

+
+

想到了希特勒

+
+

不管一份体验带给我多大的痛苦,只要不作任何抵抗地沉到这份痛苦中,体会它、看着它,那么它最多半个小时后就会融解并转化。

+

看心理医生,随着安全感和信任感的增加,患者一些更深层的痛苦反而会映现出来,于是会体会到平时生活中都体会不到的痛苦。

+

任何一次袭来的痛苦,不管多么难过,只要你沉入其中体会它觉察它,那么最多半个小时就会融解并转化,有时会以喜悦结束,有时会以平静结束。

+

当你非要压制自己的悲伤,并相反表现出极大的快乐时,你最终收获的,会是更大的悲伤。

+

我们都在寻求价值感,如果童年时,某一种方式令我们找到了价值感,此后我们便会执着在这个方式上。并且,这世界上的大多数人一般只找到了一套寻求价值感的方式,越困难的时候,我们会越执着于这一套方式,认为这是唯一的,但其实在最困难的时候,改变或调整这一方式会更好。

+

你想让一个人对你好,就请他帮你一个忙。这个办法之所以更好,是因为我们都很自恋。多数时候,我们看似爱的是别人,其实爱的是自己在这个人身上的付出。

+

每一次挫折事件都是一次机遇,因为它暴露了自己的缺点和弱点。进行自我归因的人会借此完善自己。这样一来,挫折就成了人生的一种财富。

+

好的愤怒,针对的必须是导致你愤怒的那个人。你对这个人愤怒,你才能捍卫自己的空间,并且愤怒的表达才会有效果。如果这个人惹了你,你不敢对他愤怒,你跑去把愤怒发泄到其他人身上。那么,你发泄得再厉害都没用,因为对象选错了,那样愤怒就没有任何意义。

+

治疗痛苦的唯一办法就是直面并接受人生悲剧

+

抑郁症常源自两个原因:一是重大的丧失;二是压抑的愤怒

+

失去发生时的第一时间所产生的悲伤与泪水,是有治疗效果的,只要悲伤能在我们身体上自然流动,这份疗愈就会自然产生。

+

爱的关系中,付出和接受的循环被破坏,很多时候不是因为不愿意给予,而是因为不愿意接受。

+

假如你没有一点儿负罪感,而只有清白感,那其实就是你把负罪感强加给其他人了,而那个被强加者一般都是你最亲密的人。

+

从情感上看,单纯的“付出者”其实并不伟大,他们不计得失的付出,从根本上是一种自恋。

+

“付出者”其实在享受这种逻辑:既然我是付出的一方,那么我们的关系无论出现什么问题都是你的错了。

+

最好的关系是彼此慷慨地付出和坦然地接受,通过这种交换,双方的接受和付出达成了一种平衡,且彼此都感到自己在这个关系中富有价值。

+

好人,势必有一个特点——牺牲自己的需要。坏人,势必有一个特点——纵容自己的需要。

+

关系有两种,一种是我与你,一种是我与它。

+
    +
  • 当我将你视为满足我的需要的工具与对象时,这一关系就是我与它。
  • +
  • 当我没有任何期待与目的,而是带着我的全部存在与你的全部存在相遇时,这一刻的关系就是我与你。
  • +
+

我们在与别人交往时,多数时候不过是在重复小时候我们与父母等亲人打交道的方式而已。

+

童年得到的爱越多,一个人就越是难追。这样的人会相信自己的感觉,凭感觉去找到适合自己的人。如果他觉得你是他想要的,那他可能很快接纳你;如果不是,那么可能无论你怎么努力,都是没有用的。
相对而言,童年得到的爱越少,一个人就越容易追。只要你对他很好,他就很容易感动,而暂时接纳你。但是,他是一开始容易追到,而以后会很难相处,因为他会过于敏感。

+

作为人类一种最基本的情绪,恐惧和其他情绪一样,也有着它的独特价值,而一味地追求战胜恐惧,就忽略了恐惧所传递的重要信息。

+

我们越恐惧一件事情,那件事情背后隐藏着的信息可能就越重要。

+

恐慌的背后,常藏着我们生命中重要的答案;恐慌程度越高,答案就越重要。

+

关系匮乏所带来的恐惧,在相当的程度上可以说是源自对死亡的恐惧

+

我们的人格也源自我们与父母的关系,父母和我们的原生关系,最终被我们内化为“内在的父母”和“内在的小孩”。

+

当我们想与死去的亲人同甘共苦的时候,我们忽视了很重要的一点:死去的亲人不希望我们这样做。

+

我们很容易只沉浸在自己的痛苦自己的幻想中,自以为死去的亲人希望我们怎么样,却忘记了他们对我们真切的叮嘱。如果真是这样,那才是对爱的误解。

+

追求优秀不是克服自卑的良药,特别自控也不是情绪化的答案。

+

替别人承担问题,这会令自己获得一种价值感。但若心理医生在咨询室中追求这种价值感,他便在一定程度上阻碍了病人的自我发展。

+

只要你在乎一个关系,那么你一定会把你的内在的关系投射到这个外部关系上。

+

任何一个你在乎的关系,其实都是一面心灵的镜子,可以照出你内心的秘密来。

+

假若我们渴望变成一个健康、和谐的人,那么,我们就要好好地观察自己在重要关系上的表现

+

重要的亲密关系是我们生命中的拯救者,遇到一个真心爱自己的人,那是生命中最有价值的事情。

+

你越在乎一个关系,你的那个内在的关系模式就越会淋漓尽致地展现在这个关系上。

+

什么是内心的声音?就是你的感觉,你那些说不出来但却又模模糊糊捕捉到的信息。这种声音,要学会聆听它,并尊重它。

+

追求人格的自由,结束已经发生的事实对我们心灵的羁绊只有一条途径:接受已经发生的事实,承认它已不可改变。

+

一个人的人格就是这个人过去所有人生体验的总和。

+

一个人假若常常失去控制,那么一个重要的原因是他把自己太多的事情压抑进了潜意识

+

所谓接受,即直面我们人生中的所有真相,深深地懂得,任何事实一旦发生就无可更改,而且不管多么亲密的人,我们都不能指望他们为自己而改变。

+

多数心理问题,就是因为我们小时候拒绝接受自己的父母,拒绝接受这个生命中最大的命运。相反,我们渴望改变父母。这种渴望注定会失败,于是我们将这个渴望深埋在心底,长大了,再按照这个渴望去选择配偶,并像童年渴望改变父母一样来改造配偶。

+

童年时所受过的苦,长大后我们会再受一次,不过,这次的受苦,目的是纠正童年的错误

+

愤怒其实是在提醒我们,别人对你侵犯得太厉害了,你要告诉对方:停!你不能再侵入我的空间。

+

内疚,本来是一个信号,告诉你,某个关系的付出与接受已经失去了平衡,需要调整了。

+

一个关系,就是在相互的付出和接受的循环中不断发展的。假若一个人只付出不接受,那么他就不可能与人建立很深的亲密关系

+

最不讨人喜欢的恐惧,其实具备着最重要的价值。只有恐惧,才能强有力地打破我们的自恋状态,告诉我们:你,真的很渺小;你,必须放弃一些虚假的自大,而去寻找真正重要的东西。

+

从心理学角度而言,人生宛如一个轮回,我们有一个相对固定的人格结构,也即我常写的“内在关系模式”,这导致我们会不断地在同一个地方摔倒

+

一个对自己太苛刻的人,很难做到宽以待人。相反,对自己苛刻的人,更可能的选择,是挑剔别人。

+

宽容胜于挑剔。所以,一个宽容而温和的朋友,要胜于一个优秀而挑剔的朋友。后者或许会把“严于律己,宽以待人”当作座右铭,但因为不符合最基本的心理学原理,他在过于挑剔自己的同时,也势必会苛责别人。

+

在生活中,我们的人生不断发生变化,每一次转变,我们都需要一些仪式来提醒自己。

+

仪式并不一定是一个刻意的程序,其实,入学、毕业、工作、恋爱、结婚乃至为人父母,都是一个仪式。

+

仪式只是为了告别,而不是为了忘却,因为事实一旦发生,就注定是我们命运中的一部分,我们必须接受这一部分,忘却既不能真正做到,也不利于心灵的康复。

+

仪式只是一道门,这道门,把我们的人生路划成两段,前一段属于过去,后一段属于未来,但门仍是通的,属于门那边的过去并未消失。也就是说,它只是一个象征,在提示我们,转变已发生

+ +
+ + + + + + + +
+ + + + + + +
+ +
+ + + +
+ + + + + + + + +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2021/udp-questions/index.html b/2021/udp-questions/index.html new file mode 100644 index 0000000000..e879aca0d3 --- /dev/null +++ b/2021/udp-questions/index.html @@ -0,0 +1,512 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 关于 UDP 的 10 个问题 | 贾攀的流水账 + + + + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

贾攀的流水账

+ +
+

Panmax's Blog

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+ + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ 关于 UDP 的 10 个问题 +

+ + +
+ + + + +
+ + +

是否可以向 UDP 的 90000 端口发送数据包?

不可以

+

TCP 或 UDP 数据包中的端口字段为 16 位,2^16 是 65536,所以最大的端口号是 65535。

+

每个UDP数据包都有一个目的端口吗?

是的

+

UDP 报头为 8 个字节。

+

根据 RFC,源端口是可选的,但目的端口是必须的。以下是 UDP 的报头结构:

+
1
2
3
4
5
6
 <-   16 bits  ->
+-+-+-+-+-+-+-+-+--+-+-+-+-+-+-+-+-
| source port | dest port |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| length | checksum |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+

可以发送一个长度为 100万字节的 UDP 数据包吗?

不可以

+

UDP 数据包的长度字段同样为 16 位,所以单个包的最大长度是 65535。

+

可以把把 JSON 放在 UDP 数据包里吗?

可以

+

UDP 数据包中可以放入任何字节,甚至可以放一个很短的 MP3 文件。

+

能否保证 UDP 数据包的到达顺序与发送顺序一致?

不能

+

发送一个UDP数据包后,有没有方法办法判断它是否到达?

没有

+

协议没有提供。

+

如果把 UDP 数据包发送到同一数据中心的另一台服务器上,是否能保证到达?

不能

+

即使在同一台计算机内发送,数据包仍然可能被丢弃(例如:缓冲区满了)。

+

当你发送一个 UDP 数据包时,如果发生丢失会怎样?

那就真的就丢了

+

如果想在 UDP 之上实现重试,只能自己去实现。

+

操作系统的 TCP 协议实现了 TCP 包的重试。

+

UDP 的 80 端口与 TCP 的 80 端口一样吗?

不一样

+

UDP 和 TCP 都支持相同的端口号(1-65535),但它们是不同的协议。

+

你可以同时在 UDP 的 80 端口和 TPC 的 80 端口运行 2 个不同的服务。

+

建立在 UDP 之上的协议有哪些?

DNS、DHCP, QUIC, NTP, statsd 和各种视频会议协议。

+ +
+ + + + + + + +
+ + + + + + +
+ +
+ + + +
+ + + + + + + + +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2021/unix-permission-questions/index.html b/2021/unix-permission-questions/index.html new file mode 100644 index 0000000000..a795e67454 --- /dev/null +++ b/2021/unix-permission-questions/index.html @@ -0,0 +1,529 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 关于 Unix 权限的 13 个问题 | 贾攀的流水账 + + + + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

贾攀的流水账

+ +
+

Panmax's Blog

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+ + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ 关于 Unix 权限的 13 个问题 +

+ + +
+ + + + +
+ + +

文件权限是多少位(bits)?

12位

+

分为 4 个组,每组 3 位。

+

例如,4755 对应的是 100 111 101 101

+

下面是各部分对应的内容:

+
1
2
3
4
100: setuid, setgid, sticky bits
111: user r/w/x bits
101: group r/w/x bits
101: other r/w/x bits
+

当我们运行 ls -l 时,显示的权限是 -rwxr-xr-x,这里的 r、w 和 x 是什么意思?

读、写、执行

+

每个文件有 3 套 读/写/执行 权限:

+
    +
  • 拥有该文件的用户
  • +
  • 拥有该文件的组
  • +
  • 其他用户
  • +
+

如果一个文件的权限是 0644,拥有该文件的组是否能写这个文件?

不能

+

0644 在二进制中是 000 110 100 100

+

说明如下:

+
1
2
3
4
000
110 拥有该文件的用户可以读写此文件
100 拥有该文件的组可以读此文件
100 其他用户可以读此文件
+

所以任何人都可以读取该文件,但只有拥有该文件的用户才可以写文件。

+

操作系统内核是否关心你的用户名是什么?

不关心

+

内核基于 用户ID/组ID 进行所有的权限检查 —— 用户名和组名的存在只是为了让人类更容易识别和使用。

+

如果一个目录被设置为可读权限,这意味着什么?

意味着你可以列出该目录中的文件

+

对于目录来说,下面是读/写/执行的含义:

+
    +
  • 读:你可以列出文件
  • +
  • 写:你可以创建文件
  • +
  • 执行:你可以进入该目录并访问其下的文件
  • +
+

如果一个文件的权限被设置为 0666,这是否意味着任何人都可以阅读它?

不一定

+

如果该文件的父目录的执行位被置为 0,这将使你无法读取该目录下的任何文件。

+

如果一个文件的权限被设置为 0000,这是否意味着没有人可以读它?

不是

+

root 可以读写权限为 0000 的文件。

+

每个进程都有一个用户ID(UID)吗?

是的

+

当你以用户身份登录时,几乎你启动的所有进程都会把它们的 UID 设置为你的 UID。

+

一个进程可以有多个组ID(GID)吗?

是的

+

进程有一个主 GID,也有一个补充(supplementary)组ID列表。文件权限检查将检查进程的任何一个组ID是否与文件的所有者匹配。

+

如果你把一个用户添加到一个组,以该用户身份运行的现有进程是否会自动将该 GID 添加到他们的 GID 列表中?

不会

+

退出并重新登陆后才会生效。

+

setuid 位的作用是什么?

在一个可执行文件上,它意味着该进程将以文件所有者的 UID 运行

+

例如,passwd(用来修改密码)通常设置了setuid位,因为它需要以 root 身份运行,以便能够写入修改密码的文件。

+

一个没有特权的进程有可能改变其 UID 吗?

不可能

+

你必须有超级用户的权限来改变你的 UID。

+

为什么 sudo 可以让你以 root 身份运行命令?

它设置了 setuid 位

+

sudo 总是以 root 身份运行。

+

所以如果 /etc/sudoers 允许你以 root 身份启动程序,则它将以 root 身份为你启动程序。

+ +
+ + + + + + + +
+ + + + + + +
+ +
+ + + +
+ + + + + + + + +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2022/2022-first-half-app/18181657610358.jpg b/2022/2022-first-half-app/18181657610358.jpg new file mode 100644 index 0000000000..b3de7b952b Binary files /dev/null and b/2022/2022-first-half-app/18181657610358.jpg differ diff --git a/2022/2022-first-half-app/18201657610386.png b/2022/2022-first-half-app/18201657610386.png new file mode 100644 index 0000000000..4670b71738 Binary files /dev/null and b/2022/2022-first-half-app/18201657610386.png differ diff --git a/2022/2022-first-half-app/20220708220756.png b/2022/2022-first-half-app/20220708220756.png new file mode 100644 index 0000000000..e4af0756d0 Binary files /dev/null and b/2022/2022-first-half-app/20220708220756.png differ diff --git a/2022/2022-first-half-app/20220708220822.png b/2022/2022-first-half-app/20220708220822.png new file mode 100644 index 0000000000..9d7458bdb5 Binary files /dev/null and b/2022/2022-first-half-app/20220708220822.png differ diff --git a/2022/2022-first-half-app/20220708220849.png b/2022/2022-first-half-app/20220708220849.png new file mode 100644 index 0000000000..10ae181673 Binary files /dev/null and b/2022/2022-first-half-app/20220708220849.png differ diff --git a/2022/2022-first-half-app/20220708220915.png b/2022/2022-first-half-app/20220708220915.png new file mode 100644 index 0000000000..be0e1f8017 Binary files /dev/null and b/2022/2022-first-half-app/20220708220915.png differ diff --git a/2022/2022-first-half-app/20220708220940.png b/2022/2022-first-half-app/20220708220940.png new file mode 100644 index 0000000000..e719338444 Binary files /dev/null and b/2022/2022-first-half-app/20220708220940.png differ diff --git a/2022/2022-first-half-app/20220708223141.png b/2022/2022-first-half-app/20220708223141.png new file mode 100644 index 0000000000..6d42435e3b Binary files /dev/null and b/2022/2022-first-half-app/20220708223141.png differ diff --git a/2022/2022-first-half-app/20220708223145.png b/2022/2022-first-half-app/20220708223145.png new file mode 100644 index 0000000000..2b3d52b4c2 Binary files /dev/null and b/2022/2022-first-half-app/20220708223145.png differ diff --git a/2022/2022-first-half-app/20220709063333.png b/2022/2022-first-half-app/20220709063333.png new file mode 100644 index 0000000000..559e7b5e2c Binary files /dev/null and b/2022/2022-first-half-app/20220709063333.png differ diff --git a/2022/2022-first-half-app/20220709083927.png b/2022/2022-first-half-app/20220709083927.png new file mode 100644 index 0000000000..c0d1980524 Binary files /dev/null and b/2022/2022-first-half-app/20220709083927.png differ diff --git a/2022/2022-first-half-app/20220709084623.png b/2022/2022-first-half-app/20220709084623.png new file mode 100644 index 0000000000..306ce57e77 Binary files /dev/null and b/2022/2022-first-half-app/20220709084623.png differ diff --git a/2022/2022-first-half-app/IMG_5348.png b/2022/2022-first-half-app/IMG_5348.png new file mode 100644 index 0000000000..c16a47c7f5 Binary files /dev/null and b/2022/2022-first-half-app/IMG_5348.png differ diff --git a/2022/2022-first-half-app/index.html b/2022/2022-first-half-app/index.html new file mode 100644 index 0000000000..0a4ef01743 --- /dev/null +++ b/2022/2022-first-half-app/index.html @@ -0,0 +1,565 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 2022 年上半年我使用的新工具盘点 | 贾攀的流水账 + + + + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

贾攀的流水账

+ +
+

Panmax's Blog

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+ + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ 2022 年上半年我使用的新工具盘点 +

+ + +
+ + + + +
+ + +

最近刚好听到一句话「学霸两支笔,差生文具多」,我觉得里边的差生形容我再适合不过了。我总是喜欢以「磨刀不误砍柴工」、「工具善其事必先利其器」的“借口”去找寻那些(可能)有用、能提高我效率的五花八门的工具,我日常用到的工具太多了,下边盘点几个我在今年上半年入手并且用认为好用的几个新工具,包括 Native 工具、Web 工具和 Chrome 插件。

+

飞书妙记

我平时喜欢听一些内容,比如蒋勋讲解的红楼梦,有时候听到好的段落想把那些部分记录下来,但又不想一个字一个字地手敲,所以一直在找一款好用的语音转文字的工具,最后被一个叫《 组织进化论 》的播客节目安利了飞书 App 里的「飞书妙记」的功能,这里提一句「组织进化论」也是字节孵化的一档节目。

+

我为了试用这个功能而下载了飞书,给我惊喜的是真的非常好用(包括飞书和飞书妙记),不仅支持导入音频文件转文字还支持实时录制的音频转文字,如果是多个人对话的话,可以自动帮我们把多个说话人区分出来。这就不止可以让我从有声书中提取内容了,它还可以让我更轻松地记录会议内容:好记性不如烂笔头,开会的时候打开录音模式,会后「妙记」就帮我把逐字稿生成了,参考着逐字稿写会议纪要就不会漏掉任何重要内容了。我还尝试使用在面试的场合,比如下图是我近期面的一个候选人,可以给我在后期写面评时提供抓手。

+

+

妙记还可以把对话中的关键词帮我们提取出来:

+

+

最最重要的是,它还是免费的,我之前用过几个收费的,比如科大讯飞,不管是价格还是可操作性方面都被飞书妙记吊打,但是有一说一正确性方面相比科大讯飞,妙记还是有一定提升空间的。

+

MenubarX 是一款非常实用的 macOS 小工具,它可以让你在菜单栏固定任何网页,供随时使用,就像原生 App 一样,我们可以将常用网站放在菜单栏使用。网友们已经把这个工具玩出花了,有在里边刷 Twitter、Ins 的,有用来看行情的甚至还有用来养鱼的,真成了个摸鱼 APP。

+

我自己最常用的就是用来看日历,用了好多日历 APP 都感觉不尽人意,比如有些不支持农历显示、有些不支持节假日显示,有些每周第一天是周日而且无法修改等等。在没有 MenubarX 之前,我每次在项目排期或者其他原因需要看日历时,都是手动在浏览器打开 https://wannianrili.bmcx.com/ ,这是我所找到的最简洁、实用的日历界面,但无奈没有 APP。

+

有了 MenubarX 问题就解决了,我直接将它固定到了我的顶部菜单栏,很方便就能唤出,而且可以设置独立的快捷键。因为 MenubarX 的窗口可以模拟成手机的尺寸和 UserAgent,所以日历页面就更简洁了,PC 版右侧的黄历移到了下边,这样就可以一目了然看到我最关注的日期部分了。

+

+

下载地址:https://apps.apple.com/app/id1575588022

+

我是在作者刚刚上架的时候开始的,那时候解锁 Pro 版限免,现在貌似是 30RMB。

+

Image Smith

Image Smith 这是个图片压缩工具,压缩后的图片体积可以减少非常多,而且压缩后的图片质量我们肉眼看不出差别(反正我是看不出来)。

+

我有时会把手机拍的照片放到博客中,比如这篇:https://jiapan.me/2022/da-guan-yuan/,原图一张十几 MB,压缩后可以到几 MB,这样能节省一半多的空间和带宽,而且效果上没有差别。

+

+

下载地址:https://apps.apple.com/app/id1623828135

+

这个工具也是我在作者刚上架限免时候下载的,现在卖 30RMB。类似的图片压缩工具还有一些在线版本,不想花钱买的可以试试:

+ +

为了用户体验和性能,我在有 Native 版本的情况下就不用 Web 版的,所以上边那几个在线版本我没有深入使用和对比过,都是之前收藏的以备不时之需,我按照网站的颜值排了序,大家可以自己测评一下看看哪个更好用。

+

Input Source Pro

这个工具可以帮我在使用不同的软件时自动切换到不同的输入法,避免我们因切换输入法而打断思路,比如在用开发工具(如 IDEA、GoLand、WebStorm)时切换到英文,在使用钉钉、微信这类聊天工具时切换到百度输入法,甚至支持在浏览不同的网站时使用不同的输入法。还可以做到自动记录上一次在某个软件中使用的输入法,下次再切回这个软件时自动切换为上一次使用的输入法。

+

+

每次从一个软件换到另一个软件时,Input Source Pro 都会贴心的提醒我当前在用(后这切换后)的输入法是哪个,但这个功能有个不好的地方,会在我要截图时给我带来困扰,每次我都要等 3 秒,等那个输入法悬浮提示消失后才能截图。

+

这个工具的同样想法我之前也想到过,而且还和朋友讨论过,无奈吃了不会 macOS 开发的亏。既然有人做了,而且做的还不错,那咱也就没必要再惦记着造轮子了。

+

官方地址:https://inputsource.pro/zh-CN

+

ZenUML(Web 工具)

ZenUML 是一款用伪代码画时序图的在线工具,画出来的时序图简洁漂亮,左边写代码右侧的时序图实时生成,所见即所得,而且可以直接导出 PNG 或者 JPG 格式的图片,登录后还可以将作品进行保存。我最近用它画了好多图,经常在做完方案介绍后被人问到图是用什么软件画的。

+

+

而且这还个良心工具,除非你想为信仰充值升级到 Pro 版,它们的唯一区别是 Pro 版没有代码行数的限制。免费版代码限制多少行我并不是很清楚,我之前画过几个很复杂的流程图都没有触发限制,说明我完全没有升级 Pro 版的必要。

+

+

官网地址:https://app.zenuml.com/

+

类似工具还有:

+ +

ripgrep(命令行工具)

ripgrep 是一个支持递归和正则且性能超强的文本搜索命令行工具,类似于系统自带的 grep,但是甩 grep 几十条街。

+

Mac 安装:

+
1
brew install ripgrep
+

虽然它叫 ribgrep,为了让用户更方便使用,它在命令行中输入 rg 就可以使用了。我会把所有公司的项目放到同一个目录下,有次一个同事问我是否知道某个类型的消息是哪个服务发出的,我通过这个工具快速给了他答案:

+

+

ripgrep 是开源、用 Rust 编写的:https://github.com/BurntSushi/ripgrep

+

tldr(命令行工具)

tldr 是 too long; didn’t read 的缩写,tldr 这个工具也是出于同样的目的,告诉我们某个命令行的最常用、最实用的用法。我们在用某个命令时,如果看它的 help 可能会看到巨多无比的参数,从网上搜又会很费时,这时候 tldr 就派上用场了。

+

比如以上边刚刚介绍的 rg 为例,我想知道它的常用功能都有那些,如果用 rg --help 参数会多到直接让我放弃,看下 tldr 的效果:

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
~ took 5s ➜ tldr rg

rg

Ripgrep is a recursive line-oriented CLI search tool.
Aims to be a faster alternative to `grep`.
More information: <https://github.com/BurntSushi/ripgrep>.

- Recursively search the current directory for a regular expression:
rg regular_expression

- Search for regular expressions recursively in the current directory, including hidden files and files listed in `.gitignore`:
rg --no-ignore --hidden regular_expression

- Search for a regular expression only in a certain filetype (e.g. HTML, CSS, etc.):
rg --type filetype regular_expression

- Search for a regular expression only in a subset of directories:
rg regular_expression set_of_subdirs

- Search for a regular expression in files matching a glob (e.g. `README.*`):
rg regular_expression --glob glob

- Only list matched files (useful when piping to other commands):
rg --files-with-matches regular_expression

- Show lines that do not match the given regular expression:
rg --invert-match regular_expression

- Search a literal string pattern:
rg --fixed-strings -- string
+

tldr 用一句话给我们描述了 rg 的功能,并给出了官方地址。下边还列了一些常见用法,比如不加任何参数可以递归查询当前目录下所有文件。rg --type filetype regular_expression 可以指定要查询的文件类型,等等。是不是比官方手册实用很多而且省去了到网上查一圈的麻烦。

+

还有很多小伙伴经常忘记 tar 的用法,也可以用 tldr 来做个快速回顾:

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
~ ➜ tldr tar

tar

Archiving utility.
Often combined with a compression method, such as gzip or bzip2.
More information: <https://www.gnu.org/software/tar>.

- [c]reate an archive and write it to a [f]ile:
tar cf target.tar file1 file2 file3

- [c]reate a g[z]ipped archive and write it to a [f]ile:
tar czf target.tar.gz file1 file2 file3

- [c]reate a g[z]ipped archive from a directory using relative paths:
tar czf target.tar.gz --directory=path/to/directory .

- E[x]tract a (compressed) archive [f]ile into the current directory [v]erbosely:
tar xvf source.tar[.gz|.bz2|.xz]

- E[x]tract a (compressed) archive [f]ile into the target directory:
tar xf source.tar[.gz|.bz2|.xz] --directory=directory

- [c]reate a compressed archive and write it to a [f]ile, using [a]rchive suffix to determine the compression program:
tar caf target.tar.xz file1 file2 file3

- Lis[t] the contents of a tar [f]ile [v]erbosely:
tar tvf source.tar

- E[x]tract files matching a pattern from an archive [f]ile:
tar xf source.tar --wildcards "*.html"
+

Mac 安装:

+
1
brew info tldr
+

tldr 也是个开源项目,Github 地址:https://github.com/tldr-pages/tldr

+

GitToolBox(JetBrains 插件)

GitToolBox 是个 IDE 插件,这个插件可以直接让我们看到光标所在代码行的提交信息(提交人, 提交时间, CommitMessage),不用再通过侧边栏的 Annotate with Git Blame 来查看了,很方便。

+

+

Relingo(Chrome 插件)

Relingo 是个浏览器插件,可以在阅读英语文章时自动标记出那些我们可能生疏单词的解释,当我们认识某个单词后鼠标悬浮到对应单词然后在弹出框上打个勾,之后就不会再标记这个单词了,还可以对已经掌握的单词进行回顾,是个阅读英语文章和学习英语的不错工具。

+

+

官方地址:https://relingo.net/zh/index

+

Language Reactor(Chrome 插件)

Language Reactor 也是个浏览器插件,可以让我们在 Youtube 看英语视频时通过字幕学习遇到的句子或单词。打开视频后会自动在视频右侧加载出字幕列表,可以直接点击某行字幕将视频进度跳转到我们想看的那句话,在视频中鼠标悬浮在某个单词上后视频会自动暂停播放,然后弹出这个单词的解释,鼠标移开后自动开始播放。

+

+

Relingo 也带类似功能,但是术业有专攻,我觉得 Reactor 做的更好一些,所以如果两个插件都装了的话,需要手动把 Relingo 的字幕功能关闭。

+

+

官方地址: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/

+ +
+ + + + + + + +
+ + + + + + +
+ +
+ + + +
+ + + + + + + + +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2022/Atomic-Habits-action/1.jpg b/2022/Atomic-Habits-action/1.jpg new file mode 100644 index 0000000000..a07d2b47f3 Binary files /dev/null and b/2022/Atomic-Habits-action/1.jpg differ diff --git a/2022/Atomic-Habits-action/index.html b/2022/Atomic-Habits-action/index.html new file mode 100644 index 0000000000..bb4431acf8 --- /dev/null +++ b/2022/Atomic-Habits-action/index.html @@ -0,0 +1,548 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 《掌控习惯》——读后行动 | 贾攀的流水账 + + + + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

贾攀的流水账

+ +
+

Panmax's Blog

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+ + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ 《掌控习惯》——读后行动 +

+ + +
+ + + + +
+ + +

今天是六一儿童节,祝各位大宝宝小宝宝儿童节快乐🌸

+

上周读完了 《掌控习惯》 这本书,里边总结了四个定律来培养好习惯或者戒除不良习惯,我列出这四个定律和每个定律给出的几个主要方案,在每个方案下写上自己可以将此应用在哪些地方,同时会写一些自己的感想。

+

第一定律:让它显而易见

填写习惯「积分卡」;记下你当前的习惯并留意他们

+早上不赖床
=洗脸刷牙上厕所
+洗漱期间听红楼梦、播客
-上厕所时候玩游戏
+上厕所时候学英语
+手冲一杯很淡的美式,可以提神、让自己多喝水
+使用 Things 管理自己的待办事项
-工作或者看书的空档没有思路时会刷会手机
-外边走路时会不自主的拿出手机,虽然也不知道要做什么
=边走路变听播客
+读书
=带娃
+背 Anki
=看邮件
-看 Telegram、Twitter
+每周一次慢跑
+晚上在 10:30 前做好睡眠准备

+

应用执行意图:「我将于【时间】 在【地点】【行为】。」

    +
  • 我将于早晚洗漱期间听播客。
  • +
  • 我将于上下班的地铁上,根据当时的状态(如地铁拥挤情况、自身状况)选择阅读、听课或者听播客。
  • +
  • 我每天 8:30 前出门,下地铁后步行到公司,下班步行到地铁。
  • +
  • 我将于每天中午 12:00-14:00 其他人午休这段安静的时间练习写作
  • +
+

应用习惯叠加:「继【当前习惯】之后,我将会养成【新习惯】。」

这个我没有想出与我自己相匹配的场景,先从书中抄几条吧(同时会应用在自己身上),等自己有了灵感再补充。

+
    +
  • 当我想买超过 200 元的东西时,我会等 24 小时后再买。
  • +
  • 电话铃响时,我会深吸一口气,微笑着接电话。
  • +
  • 每当买一件新物品时,我会将一些旧物品送人会丢弃。
  • +
+

设计你的环境,让好习惯的提示清晰明了。

在地铁上读书时我会戴上 AirPods Pro,播放我长期在听的那些钢琴曲,可能是已经听的太长时间吧(2年?),每当听到这些音乐后我很容易就能进入阅读状态,同时 AirPods Pro 的降噪效果也能给我提供一个不那么嘈杂的环境,更容易让我沉浸在阅读中。

+

这里附上我的歌单

+

第一定律反用:让它脱离视线

降低出现频率。把习惯的提示清除出你所在的环境。

工作时将手机屏幕扣在桌面上,晚上睡觉前将手机放在卧室充电,十点后就不再玩手机和其他点子设备(Kindle除外),这样可以避免睡前刷手机影响睡眠。

+

第二定律:让它有吸引力

利用喜好绑定。用你喜好的行为强化你需要的动作

    +
  • 我要在每天冲完咖啡回工位后冥想一分钟(最近一段时间没有冥想了,要重新培养起来);
  • +
  • 待补充…
  • +
+

加入把你喜好的行为视为正常行为的文化群体

这也是为什么家长喜欢让自己的孩子和那些更优秀的孩子在一块玩的原因,人会相互影响,尤其是那些和我们亲近的人。

+

这也是为什么有时候我们学习一样东西,自学的效率没有报一个班和大家一起学高的原因之一吧(另一个原因是能得到专业的指导),好多人一起学可以创造一种氛围,让你觉得这个事情也并没有那么难。

+

同样和自己志趣相投的一群人一起工作更能保持充足的热情,书中提到「没有什么比群体归属感更能维持一个人做事的动力了」。

+

通过这一节我也知道了我们为什么会在焦虑、无所事事时喜欢刷朋友圈、抖音、淘宝的原因:「当我们不确定改如何做时,我们都会期待得到团体的指导」。我们想看看其他人在做什么,想看看其他人在玩什么,想看看其他人在买什么。

+

我们会从众,希望能被这个社会所接纳,哪怕整个社会都在做的可能是一件不正确的事,我们为了得到认可同样也会选择做这件事。比如最近一段时间的每天一次核酸监测,再过几年再回头来看,我不认为这是正确的。「我们宁愿跟众人一起犯错,也不愿特立独行坚持真理。」

+

创设一种激励仪式。在实施低频行动之前先做一件让你特别喜好的事

第二定律反用:让它缺乏吸引力

重新梳理你的思路。罗列出戒除坏习惯带来的益处

    +
  • 戒除吃饭时喜欢配辣酱的习惯,这可以让我吃的更健康,可以控制食量,也能避免饭后和大量的水,更近一步晚上不至于总上洗手间能有更好的睡眠。恢复正常办公,回到公司后我要把公司冰箱里我的那瓶辣酱扔掉。
  • +
  • 戒除刷手机的习惯后我会有很多时间做其他更有意义的事情
  • +
  • 戒除拖延的时间后也压缩出更多的时间做其他事情
  • +
  • 戒除看到感兴趣的商品脑子一热就下单的习惯后,可以节省一些钱还能让自己生活的空间更简洁。
  • +
  • 戒除暴饮暴食的习惯后,我可以更好的做好体重管理、健康管理,也不至于总因为吃的过撑而懊恼。
  • +
  • 晚上不再喝大量饮料(包括牛奶)或吃西瓜类水份糖分过高的水果,这样对健康有利,同时不会再半夜醒来上厕所,提升睡眠质量。
  • +
+

第三定律:让它简便易行

减小阻力。减少培养好习惯的步骤

我现在只在电脑上安装了 Anki,手机上的 Anki 是付费的,价格还不低,就一直没装,我准备今天就购买并安装上手机版。我现在每天做知识回顾的时候都需要使用电脑,无法随时随地的回顾知识,多少有些阻力。

+

备好环境。创造一种有利与未来行为的环境

我的书包里时刻装着一本书和 Kindle,出门乘坐交通工具或者等人的时候,可以随时拿出来进行阅读,同时书包里还有铅笔、荧光笔,可以随时让我做标记使用。

+

再有,比如我要求自己在周六下午慢跑一次,我在白天或者提前一天就把跑步需要的衣服鞋子准备好,去跑步的概率会更大一些,因为到了那个时间我只需把衣服换好就可以出门跑步,不需要再去想着需要先找衣服、换衣服才能出门。

+

把握好决定性时刻。优化可以产生重大影响的小选择

    +
  • 路过便利店,没有必需品要买就不要想着进去转一圈
  • +
  • 如果外出吃饭想吃健康餐,就选择去专门提供健康餐的店
  • +
  • 待补充…
  • +
+

利用两分钟准则。在能够锁定你未来行为的技术和物品上有所犹如??

我现在刚刚开始尝试练习写作,写作一定是一个对未来非常好的投资,每当我需要写点什么的时候,即使自己不想写我也会要求自己坐在电脑前,打开写作工具只写一段就好。

+

想到另一个用途:当我非常想做点杀时间的事情,比如打局游戏、刷会短视频,我会跟自己说先看会书吧,就看两分钟就行。如果两分钟过去心里没那么浮躁可以读下去了就会继续读,如果还是读不下去就按照之前的计划想做什么做点什么。

+

第三定律反用:让它难以施行

增大阻力。增加实行坏习惯的步骤

这个方法可以利用在避免浪费太多时间在刷手机上,我将自己容易沉迷的软件收在一个目录中,而不是直接平铺在桌面上,这样在每次打开手机时就不会直接看到它们,同时将能关闭的推送关闭,很多不必要的软件如果没有推送我们是不会主动想到去打开它的,更进一步,我们可以尝试卸载 APP,比如我手机中现在就没有抖音、快手这钟既浪费我时间又会降低我心智的软件。

+

如果我们把手机的面部识别功能关闭,是不是能较少我们划手机的次数?有时候我们打开手机是个无意识行为,看一眼手机随手往上一滑就解锁了,然后人们会顺着这个动作不自主的启动后边一系列的动作。前段时间人们在公共场合都需要戴口罩,在使用面部识别时会比较麻烦,这应该多少也会减少玩手机的次数吧,然而后来 Apple 支持了带口罩识别,「破坏」了这个隐性的好处。我因为有 Apple Watch,所以自始至终都可以在佩戴口罩的情况下解锁 😂

+

利用承诺机制。锁定未来会有利于你的选择项

我在两周前发了个朋友圈,表示自己又又又要开始减肥了,算是一种对公共的承诺,虽然没有奖惩措施,但也这会给我提供动力,好让我在约定时间内再次在公共面前交上答卷。这样能带来的好处是我能让自己恢复到正常体重,而且能让其他人看到我是一个言必行、行必果,做事靠谱的人。

+

+

第四定律:让它令人愉悦

利用增强法。完成一套习惯后立即奖励自己

Apple Watch 会在我每次完成计划的运动量后亮出三环的烟花,每月完成运动天数后会奖励一个奖牌。

+

让「无所事事」变得愉快。当避免坏习惯时,设计一种让由此带来的好处显而易见的好处而显而易见的方式

    +
  • 早上步行到公司的路上可以听播客;
  • +
  • 在跑步过程中听点有意思的东西;
  • +
+

利用习惯追踪法。记录习惯倾向,不要中断

这个方法我在保持体重和培养阅读习惯上有所使用。为了不让自己体重超出某个范围,我会每天早上称一次体重,如果体重和前一天有较大的 diff,我就会回顾昨天做了什么吃了什么。在阅读方面,我会将自己计划读、正在读、读完的书单记录下来,对自己能起到一定的激励作用。

+

我阅读时更喜欢读纸质书是因为便于追踪,人是视觉动物,读纸质书可以看到自己已经读过了多少,还剩多少没有读,肉眼可见读过的页数越来越多、剩余的页数越来越少,也能继续激励自己往下读。相比来说,电子书在这上面就没有这个优势了,只有右下角冷冰冰的数字告诉我们当前进度是多少。

+

绝不连续错过两次。如果你忘了做,一定要尽快补救

减肥是长期的事,如果前一天因为聚餐或者嘴馋吃了过量的食物,也不要过于自责,第二天要快速调整状态,可以选择跳过一顿早餐,午餐也减些量作为弥补。

+

学习也是一样,我给自己设置了一些固定每天要学的东西作为日课,如果有一天因为某种原因(如身体不适、公司事项积压)而错过了,我要在第二天或者周末的时候做些追赶,最差的情况哪怕不做追赶,只要第二天不要再错过就好。「成功最大的威胁不是失败,而是倦怠。」

+

第四定律反用:让它令人厌恶

找一个问责伙伴。请人监督你的行为

现在没有这样的伙伴了,自己监督好自己吧。

+

创立习惯契约,让坏习惯的恶果公开化并令人难以忍受

还是上边减肥的那个朋友圈,无论有没有减到自己承诺的体重,我都会在十月一日当天把结果公布出来。

+ +
+ + + + + + + +
+ + + + + + +
+ +
+ + + +
+ + + + + + + + +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2022/awk-statistics-p99/index.html b/2022/awk-statistics-p99/index.html new file mode 100644 index 0000000000..ae4b8ed248 --- /dev/null +++ b/2022/awk-statistics-p99/index.html @@ -0,0 +1,495 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 使用 awk 统计 p99 | 贾攀的流水账 + + + + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

贾攀的流水账

+ +
+

Panmax's Blog

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+ + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ 使用 awk 统计 p99 +

+ + +
+ + + + +
+ + +

最近在重构公司内的一个重要服务,目前已经把主要流程写完了,由于新写的服务对底层的存储组件进行了变更,所以要对性能进行一个对比。

+

老服务的监控不是太完善,接口平均时延、p99 之类的都没有上报到 Prometheus 里,只在日志文件中进行了每次请求响应时间的统计,所以我写了一个 Python 脚本遍历所有日志,从中抽取出我需要的数值,然后将这些时间进行加和再除以数量就可以得到平均时间了。但是 p90、p95、p99 这些还需要我再去编写额外的代码逻辑进行统计。

+

因为太懒了,不想去写那些统计逻辑,于是从网上搜了下有没有现成的脚本,找到了一个使用 awk 统计时延的脚本,如下:

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
#! /usr/bin/awk -f  
{variance=0;sumCount+=$1;sumCost+=($2*$1);count[NR]=$1;cost[NR]=$2}
END {
staticTotal[0]=50;
staticTotal[1]=66;
staticTotal[2]=80;
staticTotal[3]=85;
staticTotal[4]=90;
staticTotal[5]=95;
staticTotal[6]=98;
staticTotal[7]=99;
staticTotal[8]=99.9;
staticFlag[0]=1;
staticFlag[1]=1;
staticFlag[2]=1;
staticFlag[3]=1;
staticFlag[4]=1;
staticFlag[5]=1;
staticFlag[6]=1;
staticFlag[7]=1;
staticFlag[8]=1;
printf "%3s %10s %15s %15s\n", "static", "costt", "count", "diffPre";
averageCost = sumCost/sumCount;
for(i=1; i <=length(count); i++) {
diff = (cost[i] - averageCost);
variance += (diff*diff*count[i]/(sumCount-1));
#printf("diff %s, variance %s, count[%s]: %s, cost[%s]: %s \n", diff, variance, i, count[i], i, cost[i]);
countTotal += count[i];
for (j=0; j <length(staticTotal); j++) {
if (countTotal >= sumCount*staticTotal[j]/100) if (staticFlag[j]==1) {
staticFlag[j]=sprintf("P%-3s %10s %15s %15s", staticTotal[j],cost[i],countTotal, countTotal - countTotalPre); countTotalPre = countTotal;
}
}
};

for( i=0;i<length(staticFlag);i++) print staticFlag[i];
printf "count total: %s\n", sumCount, countTotal;
printf "average cost: %s \n", averageCost;
printf "variance cost: %s \n", variance;
}
+

用法也很简单,准备好我们每次请求响应时间的数据,一行一条,如:

+
1
2
3
4
5
6
7
8
9
10
11
1.803322
12.561867
3.819391
0.468846
23.792512
0.362949
0.347554
2.739202
12.407241
39.385484
...
+

假如我们的数据文件叫做 time.log,将上边的脚本保存为 cal.awk,用一下命令就可以得出时延的统计信息了:

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
$ cat time.log | sort -n | uniq -c | awk -f cal.awk
static costt count diffPre
P50 3.154644 50000 50000
P66 5.481086 66000 16000
P80 9.649493 80000 14000
P85 12.548806 85000 5000
P90 17.208233 90000 5000
P95 26.653718 95000 5000
P98 42.952164 98000 3000
P99 59.790145 99000 1000
P99.9 102.811803 99900 900
count total: 100000
average cost: 7.03982
variance cost: 128.59
+

这条命令组合了 sort 对数据进行排序、uniq 对数据进行去重+次数统计,最后调用我们的 awk 脚本实现统计。

+

可以看到统计的种类很全,p50、p90、p99 都有,还有平均值和方差,非常方便。

+ +
+ + + + + + + +
+ + + + + + +
+ +
+ + + +
+ + + + + + + + +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2022/blog-security-diary/index.html b/2022/blog-security-diary/index.html new file mode 100644 index 0000000000..54dd600cbc --- /dev/null +++ b/2022/blog-security-diary/index.html @@ -0,0 +1,497 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 如何在博客上安全发布私密日记 | 贾攀的流水账 + + + + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

贾攀的流水账

+ +
+

Panmax's Blog

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+ + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ 如何在博客上安全发布私密日记 +

+ + +
+ + + + +
+ + +

我平时除了写一些可以公开的博客外,还会写点自己私密的想法,那些内容七零八落的散布在各个工具里,比如 Notion、Obsidian、Drafts、备忘录。我想把这些内容都发布到博客中,统一管理,但是无奈有些无法公之于众的内容,而且我的博客是纯静态的,无法进行访问控制。其实也可以用 Nginx 来实现一个简单的密码校验,但这么做不是很优雅,更何况我的博客目前完全托管在了 Cloudflare,控制权已经不在我手里了。

+

《黑客与画家》那本书里有这么一句话:

+
+

创造优美事物的方式往往不是从头做起,而是在现有成果的基础上做一些小小的调整,或者将已有的观点用比较新的方式组合起来。

+
+

前段时间看到了这个网站,https://txtmoji.com/,它可以把我们的内容加密成 emoji,只有知道密码的人才可以解密。这给了我一个可以组合的灵感:我将私密日记先转成 emoji 后再然后发布就好了。

+

比如这段内容:

+

😹🙊😹👔👯🙊😰😵😰👐😵😱🙍👰😱👱😯👳😵👚👓🙄😲👦👑🙃👰👢👏👓😵👐😸👑👳👶😯🙃👰👚😹👦😫👖👏👤👯🙄👺👺👨👬👘👺👵👬👡🙇🙆👳😸😹😶👓👗👨👩👤🙅👧🙁👴👶👮👖🙎👐😸👔😲🙊👵👵👑😹🙅👫👓👡🙆👏👗😲👵😰🙆👴👗😸👑👵👏🙁👩👤👩😲👡😰👮👩😴👶👧😳👌👡👤👒🙃👬👏👷😴👹🙄👐👡😲👏👬👔👫👣😷👸👺🙊👲👷😽😽

+

密码是:1234,看看我留下了什么悄悄话。

+

我看了下这个网站完全是通过前端加密,没有将我的内容上传到服务器。有点遗憾的是这个网站目前还没有开源,等以后开源了自己再私有化部署一个。

+

最近准备用这个方式发布几篇文章试试看,文章分两种,一种是标题可以外露的,这种只加密正文部分,另一种是标题和正文都不方便外露的,这样两个部分都会进行加密,加密的密码当然只有我自己知道。

+

后边想想有没有什么方式可以让那些加密的文章在未来指定的某一天改为明文显示,或者将密码展示出来。

+ +
+ + + + + + + +
+ + + + + + +
+ +
+ + + +
+ + + + + + + + +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2022/choice-log-level/1.png b/2022/choice-log-level/1.png new file mode 100644 index 0000000000..48c9c518b3 Binary files /dev/null and b/2022/choice-log-level/1.png differ diff --git a/2022/choice-log-level/index.html b/2022/choice-log-level/index.html new file mode 100644 index 0000000000..eb38c72733 --- /dev/null +++ b/2022/choice-log-level/index.html @@ -0,0 +1,485 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 选择合适的日志级别 | 贾攀的流水账 + + + + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

贾攀的流水账

+ +
+

Panmax's Blog

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+ + +
+
+
+ + +
+ + + + + + + + + + + + +
+ +
+ + + +
+ + + + + + + + +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2022/da-guan-yuan/20220630095131.png b/2022/da-guan-yuan/20220630095131.png new file mode 100644 index 0000000000..8d1704957f Binary files /dev/null and b/2022/da-guan-yuan/20220630095131.png differ diff --git a/2022/da-guan-yuan/20220630095722.png b/2022/da-guan-yuan/20220630095722.png new file mode 100644 index 0000000000..0d4fc7d033 Binary files /dev/null and b/2022/da-guan-yuan/20220630095722.png differ diff --git a/2022/da-guan-yuan/20220630100133.png b/2022/da-guan-yuan/20220630100133.png new file mode 100644 index 0000000000..92008dea7d Binary files /dev/null and b/2022/da-guan-yuan/20220630100133.png differ diff --git a/2022/da-guan-yuan/20220630100238.png b/2022/da-guan-yuan/20220630100238.png new file mode 100644 index 0000000000..ab43ec7b04 Binary files /dev/null and b/2022/da-guan-yuan/20220630100238.png differ diff --git a/2022/da-guan-yuan/20220630100308.png b/2022/da-guan-yuan/20220630100308.png new file mode 100644 index 0000000000..84260cccd4 Binary files /dev/null and b/2022/da-guan-yuan/20220630100308.png differ diff --git a/2022/da-guan-yuan/20220630100545.png b/2022/da-guan-yuan/20220630100545.png new file mode 100644 index 0000000000..41eb9b312e Binary files /dev/null and b/2022/da-guan-yuan/20220630100545.png differ diff --git a/2022/da-guan-yuan/20220630100907.png b/2022/da-guan-yuan/20220630100907.png new file mode 100644 index 0000000000..fbcea711c6 Binary files /dev/null and b/2022/da-guan-yuan/20220630100907.png differ diff --git a/2022/da-guan-yuan/20220630100922.png b/2022/da-guan-yuan/20220630100922.png new file mode 100644 index 0000000000..5a8ed743c1 Binary files /dev/null and b/2022/da-guan-yuan/20220630100922.png differ diff --git a/2022/da-guan-yuan/20220630101118.png b/2022/da-guan-yuan/20220630101118.png new file mode 100644 index 0000000000..a292285151 Binary files /dev/null and b/2022/da-guan-yuan/20220630101118.png differ diff --git a/2022/da-guan-yuan/20220630132938.png b/2022/da-guan-yuan/20220630132938.png new file mode 100644 index 0000000000..24923f7b07 Binary files /dev/null and b/2022/da-guan-yuan/20220630132938.png differ diff --git a/2022/da-guan-yuan/index.html b/2022/da-guan-yuan/index.html new file mode 100644 index 0000000000..0110da13b0 --- /dev/null +++ b/2022/da-guan-yuan/index.html @@ -0,0 +1,521 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 大观园记 | 贾攀的流水账 + + + + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

贾攀的流水账

+ +
+

Panmax's Blog

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+ + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ 大观园记 +

+ + +
+ + + + +
+ + +

20220630100907.png

+
+

这篇小记写于早上上班的地铁上,我之前都是正襟危坐在电脑旁用 Obsidian 写,你这次尝试在手机上用 Drafts 写,然后到公司后再用电脑做些调整、配上图片后发表。

+
+

上上个周末去了一趟大观园,也算圆了我这几年的一个愿望,其实大观园离我住的地方并不远,开车也就 15 分钟,只是由于疫情再加上自己的行动力不足一直拖到现在。

+

行动力不足的一个原因是大观园 40 每人的票价比其他公园高出很多。可是虽然贵,但它又包含在了公园通票中,所以我去年的时候就想今年办个通票,到时候去个痛快,然后一晃半年多就过去了。这次让我行动的一个触发点是前几天高考的作文题目中出现了大观园,又唤起了我去大观园的念头,刚好疫情也没那么紧张,索性就来了。

+

+

下图是我拍摄的作文题目中出现的「沁芳」:

+

+

实话实说,大观园没有我想想的那么大、那么宏伟奢华,可能也是因为当初的成本和地基大小所限,毕竟当时的建园最主要的目的是拍摄红楼梦电视剧,那些宏伟的场景可以通过镜头的运用来突显。气势上虽然没达到我的预期,但里边的景色还是极好的,待到冬天下雪后我会再来二刷。因为姓氏的缘故,我在逛大观园时总会幻想在逛自己家的园子。

+
+

当时大观园建在北京郊区,谁成想当年的郊区现在已经成了北京的核心地段。

+
+

因为先前读过几遍红楼梦,所以看到每一处景观都能回想起书中在这里发生过的故事,比如看到花冢,就会想到黛玉的葬花词:「侬今葬花人笑痴,他年葬侬知是谁?」

+

+

看到省亲别墅,想到元妃说的那句「当日既送我到那不得见人的去处,好容易今日回家娘儿们一会,不说说笑笑,反倒哭起来。」

+

+

看到写着顾恩思义的祠堂,想到中秋节时祠堂内传出的几声叹息,暗示着贾家的败落。

+

+

看到潇湘馆和怡红院想到宝黛两个小冤家在这里或喜或悲或叹或惊的那些场景。

+


+

我小时候并没有看过红楼梦,甚至没看过他的相关影视作品,著名桥段也只听说过刘姥姥进大观园。在我的思维定势中红楼梦是一本讲儿女情长的小说,前几年读它的原因是随着阅读量越来越大,读到的对这本书引用的内容也越来越多,而且红楼梦在豆瓣上稳坐头把交椅,我就越来越对这本书产生好奇。

+

+

书一开始讲大荒山一块石头的故事,我差点弃读,但是往后读了读发现又讲到女娲补天,石头是最后没有使用的那一块,石头有思想后想去人间走一遭,跛足道人和癞头和尚答应带它去看一看这个繁杂的人世间,顺便让它看着了却几段姻缘,于是我遍产生了兴趣。

+

红楼梦中作者要表达的并不是那几对小情侣或者三角恋之间的恩怨情仇,而且讲了一个美好的青春王国的故事,这个王国的结束于在抄检大观园。每每读到大观园中宝玉与姐妹们嬉笑玩乐的情结,我也会回忆我自己的童年时光。作者在书中表达了对所有人和事的怜悯,作者从来不觉得一个人恶,没有批评书中的任何角色,而且书中很多为人处世之道挪到现在的职场和官场也非常适用。

+

《红楼梦》还有一个特点:它是一本关于女孩子的书。在《红楼梦》中,贾宝玉在某种程度上都被女性化了,这在中国的经典著作中很少见。男生若要读懂女生的心思,不妨读读它。

+ +
+ + + + + + + +
+ + + + + + +
+ +
+ + + +
+ + + + + + + + +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2022/database-isolation-level/index.html b/2022/database-isolation-level/index.html new file mode 100644 index 0000000000..c68c1e8c3f --- /dev/null +++ b/2022/database-isolation-level/index.html @@ -0,0 +1,565 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 简述数据库隔离级别 | 贾攀的流水账 + + + + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

贾攀的流水账

+ +
+

Panmax's Blog

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+ + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ 简述数据库隔离级别 +

+ + +
+ + + + +
+ + +

常见的数据库有四种隔离级别,从强到弱分别为:可串行化(Serializable)、可重复读(Repeatable Read)、读已提交(Read Committed)、读未提交(Read Uncommitted)。

+

不同隔离级别在实现上的本质是各种的使用有所不同,包括锁的多样性和锁的粒度

+

现有的文章大多是直接深入到数据库的细节中讨论这几种隔离级别,而且介绍的也很全面了,这里我尝试站在锁的角度来对这几种隔离级别做个讨论。

+

可串行化(Serializable)

可串行化使用了最全的锁:写锁、读锁、范围锁。

+

读写锁平时比较常见,这里简单介绍下范围锁。

+

范围锁的定义为:对于某个范围直接加排他锁,在这个范围内的数据不能被写入。

+

要注意的时这里的访问内的数据不止包括已有的数据,即使不存在的数据也会被加锁,可以理解为不允许在这个范围内新增数据。

+

我举个例子,比如我现在有这样一些数据:

+
1
2
3
4
5
6
id	price
1 10
2 30
3 70
4 90
5 120
+

当我们在一个事务中使用范围查询 price<100 时,在这个事务还未结束的情况下,其他事务无法在新增一个 price 为 20 的数据。

+

可串行化保障了最好的隔离级别,但也是这几种隔离级别中性能最差的。

+

可重复读(Repeatable Read)

可重复读只使用了读锁和写锁,未使用范围锁。

+

还用上边的数据举例,这种情况下会产生的一个问题是:当一个事务在第一次查询 price<100 时返回了 4 条数据,这时候另一个事务新增了一条 price 为 20 的数据,当第一个事务再次查询 price<100 的数据时发现变成了 5 条,也就是说出现了幻读

+

幻读:在事务执行过程中,两个完全相同的范围查询得到了不同的结果集。

+

读已提交(Read Committed)

读已提交表面上看和可重复度使用的锁相同,都使用了读锁和写锁,但在读锁的加锁粒度上和之前有所区别

+

在上边的可重复读中,读锁是一直锁到事务结束,但在读已提交中,读锁在查询完成后会立即释放,下边我写两个 Go 程序来演示下这两种情况的区别。

+

可重复读程序(读锁锁到事务结束)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
package main

import (
"sync"
"time"
)

func main() {
mutex := new(sync.RWMutex)

i := 1

// 事务1
go func() {
// 使用 defer 确保事务结束后再释放锁
defer mutex.RUnlock()
mutex.RLock()
// 先查询1次
println(i)

// 2秒后再查询一次
time.Sleep(2 * time.Second)
println(i)
}()

// 事务2
go func() {
defer mutex.Unlock()

// 在1秒后进行数据更新
time.Sleep(1 * time.Second)
mutex.Lock()
i += 1
}()

time.Sleep(3 * time.Second)
}
+

输出:

+
1
2
1
1
+

读已提交程序(读锁锁到查询完成)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
package main

import (
"sync"
"time"
)

func main() {
mutex := new(sync.RWMutex)

i := 1

// 事务1
go func() {
// 读锁加锁
mutex.RLock()
println(i)
// 读锁释放
mutex.RUnlock()

time.Sleep(2 * time.Second)

// 读锁加锁
mutex.RLock()
println(i)
// 读锁释放
mutex.RUnlock()
}()

// 事务2
go func() {
defer mutex.Unlock()
time.Sleep(1 * time.Second)
mutex.Lock()
i += 1
}()

time.Sleep(3 * time.Second)
}
+

输出:

+
1
2
1
2
+

这个程序和上边的程序区别在于读锁是锁了整个事务还是只锁了查询的瞬间,在读已提交的情况下,第一个事务读取数据并打印出 1 后就释放了读锁,这时候另一个事务可以拿到写锁并将数据修改为 2,之后第一个事务再次读取时就读到了另一个事务修改后的数据。

+

这种情况我们称之为不可重复读问题

+

不可重复读问题:在事务执行过程中,对同一行数据的两次查询得到了不同的结果。

+

读未提交(Read Uncommitted)

读未提交只使用了写锁,同样我们也通过一个 Go 程序观察下这个情况。

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
package main

import (
"sync"
"time"
)

func main() {
mutex := new(sync.Mutex) // 这里将读写锁换成普通的互斥锁

i := 1

go func() {
time.Sleep(1 * time.Second)
println(i) // 可以读到另一个事务第一次修改后的数据
time.Sleep(3 * time.Second)
println(i) // 可以读到另一个事务第二次修改后的数据
}()

go func() {
defer mutex.Unlock()
mutex.Lock()
i += 1 // 在事务中修改了数据
time.Sleep(2 * time.Second)
i += 1 // 在事务中再次修改了数据
}()

time.Sleep(5 * time.Second)
}
+

输出:

+
1
2
2
3
+

这里演示的是,一个事务对数据进行修改,另一个事务只是读取数据,由于在读未提交下不存在读锁,可以直接读数据。

+
    +
  • 数据初始值为 1,写事务将值修改为 2(但并未释放写锁)
  • +
  • 读事务在 1 秒后读到了 2
  • +
  • 写事务在 2 秒后又将数据修改为 3(由于后边的 sleep,也并没有立即释放写锁)
  • +
  • 读事务在 3 秒后又读到了 3
  • +
+

可以看到,我们的只读事务读到了写事务还没有提交的数据,我们称之为脏读

+

脏读:在事务执行过程中,一个事务读取到了另一个事务未提交的数据。

+

总结一下

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
脏读不可重复读幻读隔离级别
写锁、读锁、范围锁可串行化
读锁、写锁可重复读
读锁(读完释放)、写锁读已提交
写锁读未提交
+ +
+ + + + + + + +
+ + + + + + +
+ +
+ + + +
+ + + + + + + + +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2022/ddd-cant/20220624105137.png b/2022/ddd-cant/20220624105137.png new file mode 100644 index 0000000000..c10c44e919 Binary files /dev/null and b/2022/ddd-cant/20220624105137.png differ diff --git a/2022/ddd-cant/20220624142308.png b/2022/ddd-cant/20220624142308.png new file mode 100644 index 0000000000..52caabba21 Binary files /dev/null and b/2022/ddd-cant/20220624142308.png differ diff --git a/2022/ddd-cant/20220624144225.png b/2022/ddd-cant/20220624144225.png new file mode 100644 index 0000000000..ed15150db8 Binary files /dev/null and b/2022/ddd-cant/20220624144225.png differ diff --git a/2022/ddd-cant/20220624145253.png b/2022/ddd-cant/20220624145253.png new file mode 100644 index 0000000000..499614bda2 Binary files /dev/null and b/2022/ddd-cant/20220624145253.png differ diff --git a/2022/ddd-cant/index.html b/2022/ddd-cant/index.html new file mode 100644 index 0000000000..80a1b6f92c --- /dev/null +++ b/2022/ddd-cant/index.html @@ -0,0 +1,616 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + DDD “黑话”指南 | 贾攀的流水账 + + + + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

贾攀的流水账

+ +
+

Panmax's Blog

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+ + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ DDD “黑话”指南 +

+ + +
+ + + + +
+ + +

背景

开始前声明,我不相信技术上存在任何银弹,包括微服务、DDD,不要指望用一套方法论或者架构能解决所有问题,能够根据当时的情况(资源、人才、业务)权衡出最符合当时场景的架构,才是一个合格的架构师的价值所在。

+

在讨论 DDD 时经常会一起讨论的是两种模型,传统贫血模型和 DDD 所推崇的充血模型。对于业务不复杂的系统开发来说,基于贫血模型的传统开发模式简单够用,基于充血模型的 DDD 开发模式有点大材小用,无法发挥作用。比如数据统计和分析,DDD 很多方法可能都用不上,或用得并不顺手,而传统的方法很容易就解决了。

+
+

我在大概 19 年左右看过一些 DDD 相关的内容,也在上家公司团队内推行过 DDD 的调研,但限于上家公司对技术升级、培养没有那么看重,并且推行 DDD 对人员的技术要求也比较高,只做了一次内部分享就没再有下文了。

+

最近来到新的部门后,这边在推行使用 DDD 来梳理我们的业务架构,从更高的视角审视我们目前的技术架构,用于评估架构是否合理、服务是否应当做一些拆分或者将应该归在一起的业务进行合并。我觉得这个做法是合理并且正确的,DDD 更推崇的是设计思想,可以用这个思想来指导我们做业务建模和服务设计。我们没必要为了 DDD 而 DDD,更不能脱离领域模型来空谈微服务设计。

+

由于时间久远,当时看过的内容已经忘的七七八八了,翻开之前看文章时做的一些记录,将 DDD 中常用的 “黑话” 回顾一下,记录在下文,尽量和其他人的认知对齐,在交流时能更通畅一些。

+

八股图镇楼

20220624105137.png

+

DDD 与微服务的关系:

    +
  • DDD 是一种架构设计方法,微服务是一种架构风格,两者从本质上都是为了追求高响应力,而从业务视角去分离应用系统建设复杂度的手段。
  • +
  • 两者都强调从业务出发,其核心要义是强调根据业务发展,合理划分领域边界,持续调整现有架构,优化现有代码,以保持架构和代码的生命力,也就是我们常说的演进式架构
  • +
  • DDD 主要关注:从业务领域视角划分领域边界,构建通用语言进行高效沟通,通过业务抽象,建立领域模型,维持业务和代码的逻辑一致性。
  • +
  • 微服务主要关注:运行时的进程间通信、容错和故障隔离,实现去中心化数据管理和去中心化服务治理,关注微服务的独立开发、测试、构建和部署。
  • +
+

领域

DDD 的领域就是这个边界内要解决的业务问题域

+

20220624144225.png

+

子域

我们把划分出来的多个子领域称为子域,每个子域对应一个更小的问题域或更小的业务范围。

+

子域可以根据自身重要性功能属性划分为三类子域:

+
    +
  • 核心域:决定了产品和公司核心竞争力,它是业务成功的主要因素和公司的核心竞争力。
  • +
  • 通用域:没有太多个性化的诉求,同时被多个子域使用的通用功能子域,如认证、权限。
  • +
  • 支持域:不包含决定产品和公司核心竞争力的功能,也不包含通用功能的子域,如数据代码类的数据字典系统
  • +
+

限界上下文

用来封装通用语言和领域对象,提供上下文环境,保证在领域之内的一些术语、业务相关对象等(通用语言)有一个确切的含义,没有二义性。

+

可以将限界上下文拆解为两个词:限界和上下文

+
    +
  • 限界就是领域的边界
  • +
  • 而上下文则是语义环境
  • +
+

理论上限界上下文就是微服务的边界。

+

聚合

聚合就是由业务和逻辑紧密关联的实体和值对象组合而成的,聚合是数据修改和持久化的基本单元,每一个聚合对应一个仓储,实现数据的持久化。

+

聚合的特点:

高内聚、低耦合,它是领域模型中最底层的边界,可以作为拆分微服务的最小单位。

+

一个微服务可以包含多个聚合,聚合之间的边界是微服务内天然的逻辑边界。

+

聚合的一个设计原则:在边界之外使用最终一致性。一次事务最多只能更改一个聚合的状态。如果一次业务操作涉及多个聚合状态的更改,应采用领域事件的最终一致性。

+

聚合根

    +
  • 根据业务单一职责和高内聚原则,定义了聚合内部应该包含哪些实体和值对象
  • +
  • 主要目的是为了避免由于复杂数据模型缺少统一的业务规则控制,而导致聚合、实体之间数据不一致性的问题。
  • +
  • 如果把聚合比作组织,那聚合根就是这个组织的负责人。聚合根也称为根实体,它不仅是实体,还是聚合的管理者。
  • +
+

聚合根的特点:

聚合根是实体,有实体的特点,具有全局唯一标识,有独立的生命周期。

+
    +
  • 一个聚合只有一个聚合根,聚合根在聚合内对实体和值对象采用直接对象引用的方式进行组织和协调。
  • +
  • 聚合根与聚合根之间通过 ID 关联的方式实现聚合之间的协同。
  • +
+

领域服务

如果一个业务动作或行为跨多个实体,我们就需要设计领域服务。

+
    +
  • 领域服务通过对多个实体和实体方法进行组合,完成核心业务逻辑
  • +
  • 领域服务是位于实体方法之上应用服务之下的一层业务逻辑。
  • +
+

在微服务内部,实体的方法被领域服务组合和封装,领域服务又被应用服务组合和封装。

+

仓储

每一个聚合都有一个仓储,仓储主要用来完成数据查询和持久化操作。

+

领域事件

领域事件是领域模型中非常重要的一部分,用来表示领域中发生的事件。一个领域事件将导致进一步的业务操作,在实现业务解耦的同时,还有助于形成完整的业务闭环。

+

如果发生某种事件后,会触发进一步的操作,那么这个事件很可能就是领域事件

+

领域事件驱动设计可以切断领域模型之间的强依赖关系,事件发布完成后,发布方不必关心后续订阅方事件处理是否成功,这样可以实现领域模型的解耦,维护领域模型的独立性和数据的一致性。

+

在领域模型映射到微服务系统架构时,领域事件可以解耦微服务,微服务之间的数据不必要求强一致性,而是基于事件的最终一致性

+

通过领域事件驱动的异步化机制,可以推动业务流程和数据在各个不同微服务之间的流转,实现微服务的解耦,减轻微服务之间服务调用的压力,提升用户体验。

+

实体

在 DDD 中有这样一类对象,它们拥有唯一标识符,且标识符在历经各种状态变更后仍能保持一致。对这些对象而言,重要的不是其属性,而是其延续性和标识,对象的延续性和标识会跨越甚至超出软件的生命周期。我们把这样的对象称为实体。

+

在代码模型中,实体的表现形式是实体类,这个类包含了实体的属性和方法,通过这些方法实现实体自身的业务逻辑。

+

聚合根是一种特殊的实体,它有自己的属性和方法。聚合根可以实现聚合之间的对象引用,还可以引用聚合内的所有实体。

+

实体的特点:有 ID 标识,通过 ID 判断相等性,ID 在聚合内唯一即可。

    +
  • 状态可变,它依附于聚合根,其生命周期由聚合根管理。
  • +
  • 实体一般会持久化,但与数据库持久化对象不一定是一对一的关系。
  • +
  • 实体可以引用聚合内的聚合根、实体和值对象。
  • +
+

值对象

通过对象属性值来识别的对象,它将多个相关属性组合为一个概念整体。在 DDD 中用来描述领域的特定方面,并且是一个没有标识符的对象,叫作值对象。

+

值对象创建后就不允许修改了,只能用另外一个值对象来整体替换。

+

值对象的特点:无 ID,不可变,无生命周期,用完即扔。

    +
  • 值对象之间通过属性值判断相等性。
  • +
  • 它的核心本质是值,是一组概念完整的属性组成的集合,用于描述实体的状态和特征。
  • +
  • 值对象尽量只引用值对象。
  • +
+

实体 vs 值对象

实体和值对象是组成领域模型的基础单元。

+
    +
  • 实体一般对应业务对象,它具有业务属性和业务行为
  • +
  • 值对象主要是属性集合,对实体的状态和特征进行描述
  • +
+

实体是看得到、摸得着的实实在在的业务对象,实体具有业务属性、业务行为和业务逻辑。而值对象只是若干个属性的集合,只有数据初始化操作和有限的不涉及修改数据的行为,基本不包含业务逻辑。值对象的属性集虽然在物理上独立出来了,但在逻辑上它仍然是实体属性的一部分,用于描述实体的特征。

+

聚合与实体、值对象的关系

领域模型内的实体和值对象就好比个体,而能让实体和值对象协同工作的组织就是聚合,它用来确保这些领域对象在实现共同的业务逻辑时,能保证数据的一致性。

+

聚合就是由业务和逻辑紧密关联的实体和值对象组合而成的,聚合是数据修改和持久化的基本单元,每一个聚合对应一个仓储,实现数据的持久化。

+

聚合之间通过聚合根 ID 关联引用,如果需要访问其它聚合的实体,就要先访问聚合根,再导航到聚合内部实体,外部对象不能直接访问聚合内实体。

+

领域事件基本属性至少包括:

    +
  • 事件唯一标识
  • +
  • 发生时间
  • +
  • 事件类型
  • +
  • 事件
  • +
+

数据一致性

聚合内数据强一致性,而聚合之间数据最终一致性。

+

在一次事务中,最多只能更改一个聚合的状态。如果一次业务操作涉及多个聚合状态的更改,应采用领域事件的方式异步修改相关的聚合,实现聚合之间的解耦。

+

产品愿景

产品愿景是对产品顶层价值设计,对产品目标用户、核心价值、差异化竞争点等信息达成一致,避免产品偏离方向。

+

场景分析

场景分析是从用户视角出发,探索业务领域中的典型场景,产出领域中需要支撑的场景分类、用例操作以及不同子域之间的依赖关系,用以支撑领域建模。

+

领域建模

领域建模是通过对业务和问题域进行分析,建立领域模型。

+
    +
  • 向上通过限界上下文指导微服务边界设计
  • +
  • 向下通过聚合指导实体对象设计
  • +
+

DDD 战略设计和战术设计

战略设计主要从业务视角出发,建立业务领域模型,划分领域边界,建立通用语言的限界上下文,限界上下文可以作为微服务设计的参考边界。

+

战术设计则从技术视角出发,侧重于领域模型的技术实现,完成软件开发和落地,包括:聚合根、实体、值对象、领域服务、应用服务和资源库等代码逻辑的设计和实现。

+

服务的封装和调用方式

1. 应用服务的组合和编排

应用服务会对多个领域服务进行组合和编排,暴露给用户接口层,供前端应用调用。

+

2. 领域服务的组合封装

领域服务会对多个实体和实体方法进行组合和编排,供应用服务调用。

+

3. 实体方法的封装

实体方法是最底层的原子业务逻辑。

+

20220624142308.png

+

DDD 的设计过程:

    +
  1. 在事件风暴中,我们会梳理出业务过程中的用户操作、事件以及外部依赖关系等,根据这些要素梳理出实体等领域对象
  2. +
  3. 根据实体对象之间的业务关联性,将业务紧密相关的多个实体进行组合形成聚合,聚合之间是第一层边界。
  4. +
  5. 根据业务及语义边界等因素将一个或者多个聚合划定在一个限界上下文内,形成领域模型,限界上下文之间的边界是第二层边界。
  6. +
+

领域对象设计过程

    +
  1. 设计实体
  2. +
  3. 找出聚合根
  4. +
  5. 设计值对象
  6. +
  7. 设计领域事件
  8. +
  9. 设计领域服务
  10. +
  11. 设计仓储
  12. +
+

DDD 分层架构从上到下依次是

用户接口层

用户接口层负责向用户显示信息和解释用户指令。这里的用户可能是:用户、程序、自动化测试和批处理脚本等等。

+

应用层

应用层是很薄的一层,理论上不应该有业务规则或逻辑,主要面向用例和流程相关的操作。

+

领域层

领域层的作用是实现企业核心业务逻辑,通过各种校验手段保证业务的正确性。

+

领域层包含聚合根、实体、值对象、领域服务等领域模型中的领域对象。

+

基础层

    +
  • 基础层是贯穿所有层的,它的作用就是为其它各层提供通用的技术和基础服务,包括第三方工具、驱动、消息中间件、网关、文件、缓存以及数据库等。比较常见的功能还是提供数据库持久化。
  • +
+

20220624145253.png

+

参考

    +
  • DDD 实战
  • +
  • 设计模式之美
  • +
+ +
+ + + + + + + +
+ + + + + + +
+ +
+ + + +
+ + + + + + + + +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2022/deadline-is-the-first-power/20220804155946.png b/2022/deadline-is-the-first-power/20220804155946.png new file mode 100644 index 0000000000..81ce293866 Binary files /dev/null and b/2022/deadline-is-the-first-power/20220804155946.png differ diff --git a/2022/deadline-is-the-first-power/20220804160448.png b/2022/deadline-is-the-first-power/20220804160448.png new file mode 100644 index 0000000000..e6d3f88589 Binary files /dev/null and b/2022/deadline-is-the-first-power/20220804160448.png differ diff --git a/2022/deadline-is-the-first-power/20220804161204.png b/2022/deadline-is-the-first-power/20220804161204.png new file mode 100644 index 0000000000..0dfaf9b998 Binary files /dev/null and b/2022/deadline-is-the-first-power/20220804161204.png differ diff --git a/2022/deadline-is-the-first-power/20220804162244.png b/2022/deadline-is-the-first-power/20220804162244.png new file mode 100644 index 0000000000..77540e68ec Binary files /dev/null and b/2022/deadline-is-the-first-power/20220804162244.png differ diff --git a/2022/deadline-is-the-first-power/index.html b/2022/deadline-is-the-first-power/index.html new file mode 100644 index 0000000000..63d5ee9279 --- /dev/null +++ b/2022/deadline-is-the-first-power/index.html @@ -0,0 +1,504 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + deadline 是第一生产力 | 贾攀的流水账 + + + + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

贾攀的流水账

+ +
+

Panmax's Blog

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+ + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ deadline 是第一生产力 +

+ + +
+ + + + +
+ + +

我在今年六月份转岗后被邀请加入了公司的架构组,这个组织的意图是提升公司后端整体技术能力。我们规划了每周一次公司内技术分享,一般在周五进行。

+

本周二下午架构组例会上讨论本周分享内容时,突然被老板点名希望我做一次分享,内容就是上一周他随机挑选 10 个人写了三道算法题,让我围绕这些代码做个代码质量相关的分享。

+

周二晚上告诉我要周五分享,留给我整理素材、做 PPT 的时间非常紧,所以晚上回家路上我就开始收集之前看过的资料,回家后又把《代码整洁之道》这本书翻出来,快速把里边之前标记了重点的地方进行了阅读,大致在脑子里形成了一个提纲。

+

第二天一早也就是周三,在 PPT 还没有开始做之前,老板的助理找我要分享的内容介绍,包括主题、听众收益,我基于昨晚的提纲写了一份介绍交给了她,之后我也就按照这些组织我的 PPT。

+

+

之后助理又找我要个人照片,我把去年公司给我拍的一张照片给了它,没想到过了没多久,公司所有投影仪、电视开屏背景就成了我的宣传页,有点受宠若惊。

+

+

也被公司其他同事看到纷纷发来问候:

+

+

找照片的过程也比较坎坷,因为我也没有艺术照啥的,唯一一张正式点的照片就是公司去年给我拍的形象照,当时因为运气好被评为了公司年度优秀员工。公司当时把这张照片修好后的原图发给了我,我只是打开看了下,并没有额外去保存,所以这次再找的时候就找不到了。我平时有清理 Download 目录的习惯,当时那张照片就是放到了 Download 里了。不过我还有另一个习惯,就是每次在清理 Download 前先把这个目录做个备份,将里边所有内容上传到 OneDrive 中,我会在 OneDrive 中建一个今天日期的目录,然后把此时 Download 中所有文件上传进去,再清理掉本地的文件。当然我还会定期把 OneDrive 日期太久远的目录删除,比如超过 1 年的备份,否则容量不太够用。我抱着试一试的态度在 OneDrive 的备份中找这张照片,竟然被我找到了,果然凡事需要留个后路。

+

+

在 Deadline、公司大力宣传的驱动下,我的效率倍增,在今天也就是周四下午完成了第一版的 PPT,自己都感慨效率如此之高,也多亏了之前阅读过的一些资料和书籍,将那些资料结合实际情况做下整理就成了我的 PPT。计划在发布完这篇 blog 后重头过一遍 PPT 做些微调,然后再找个会议室做做练习。

+

Keynote 可以将文稿转成 PDF、HTML 等格式,我尝试转成 HTML 后发现就是一个标准的前端项目,有一个 index.html 作为入口,这不就可以放到 Cloudflare 上做一个静态站了吗,于是我把这些文件上传到了 Cloudflare,并关联了我的域名:https://codestyle.jiapan.me/,这样就得到了一个可以在线浏览的版本,看了下效果分辨率比原始文档偏低,但又不是不能用🤷🏻‍♂️。

+

最后再感叹一声,Deadline 是第一生产力!

+ +
+ + + + + + + +
+ + + + + + +
+ +
+ + + +
+ + + + + + + + +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2022/design-system-using-event-sourcing/20220722094137.png b/2022/design-system-using-event-sourcing/20220722094137.png new file mode 100644 index 0000000000..1e9135d5f7 Binary files /dev/null and b/2022/design-system-using-event-sourcing/20220722094137.png differ diff --git a/2022/design-system-using-event-sourcing/20220722094154.png b/2022/design-system-using-event-sourcing/20220722094154.png new file mode 100644 index 0000000000..5d1e4d2f32 Binary files /dev/null and b/2022/design-system-using-event-sourcing/20220722094154.png differ diff --git a/2022/design-system-using-event-sourcing/index.html b/2022/design-system-using-event-sourcing/index.html new file mode 100644 index 0000000000..e8670b800c --- /dev/null +++ b/2022/design-system-using-event-sourcing/index.html @@ -0,0 +1,512 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 如何用事件溯源模式设计一个系统 | 贾攀的流水账 + + + + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

贾攀的流水账

+ +
+

Panmax's Blog

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+ + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ 如何用事件溯源模式设计一个系统 +

+ + +
+ + + + +
+ + +

事件溯源模式用于设计一个具有确定性的系统,这改变了普通系统设计的理念。我们通过一个电商系统来演示一下普通的 CRUD 和事件溯源模式的区别。

+

事件溯源模式不是在数据库中记录订单状态,而是将导致状态变化的事件保存在事件存储中,事件存储是一个仅附加的日志,类似于数据库中的 undo log。

+

事件必须有递增的主键 ID,以保证其顺序。订单状态通过回放事件来构建,并在订单视图(OrderView)中维护。如果订单视图发生故障,我们总是可以依赖事件存储进行修正,它是恢复订单状态的真实来源。

+

让我们来看看详细的步骤。

+

非事件溯源模式

20220722094137.png

+
    +
  • 步骤 1 和 2:Bob 想买一个产品,订单被创建并插入到数据库中。
  • +
  • 步骤 3 和 4:Bob 想把数量从 5 改为 6。该订单被修改为新的状态。
  • +
  • 步骤 5 和 6:Bob 为该订单支付了 6 元,订单完成,状态改为已支付(PAID)。
  • +
  • 步骤 7 和 8:Bob 查询最新的订单状态,查询服务从数据库中检索状态。
  • +
+

事件溯源

20220722094154.png

+
    +
  • 步骤 1 和 2:Bob 想买一个产品:一个 NewOrderEvent 被创建,按序存储在事件仓库中,此时 eventID=2001。
  • +
  • 步骤 3 和 4:Bob 想把商品数量从 5 改为 6:一个 ModifyOrderEvent 被创建,并以 eventID=2002 的形式按序保存在事件仓库中。
  • +
  • 步骤 5 和 6:Bob 为这个订单支付了 60 元:一个 OrderPaymentEvent 被创建,并以 eventID=2003 的形式存储在事件仓库中。注意不同的事件类型有不同的事件属性。
  • +
  • 第 7 步:订单视图(OrderView)监听从事件仓库中发布的事件,并建立订单的最新状态。虽然订单视图收到了 3 个事件,但是它一个一个按序应用这些事件,并保持订单最新的状态。
  • +
  • 第 8 步:Bob 从订单服务(OrderService)查询订单状态,订单服务可以通过订单视图(OrderView)来获取订单状态。
      +
    • 订单视图可以在内存或缓存中,不需要被持久化,因为它可以从事件存储中恢复。
    • +
    • 订单视图表也可以持久化到其他数据库引擎中,如 ElasticSearch 来支持订单搜索,这就用到了另一种模式:CQRS。
    • +
    +
  • +
+ +
+ + + + + + + +
+ + + + + + +
+ +
+ + + +
+ + + + + + + + +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2022/do-not-want-to-work/0.jpg b/2022/do-not-want-to-work/0.jpg new file mode 100644 index 0000000000..7b52a2eadc Binary files /dev/null and b/2022/do-not-want-to-work/0.jpg differ diff --git a/2022/do-not-want-to-work/IMG_1517.jpeg b/2022/do-not-want-to-work/IMG_1517.jpeg new file mode 100644 index 0000000000..07425f7461 Binary files /dev/null and b/2022/do-not-want-to-work/IMG_1517.jpeg differ diff --git a/2022/do-not-want-to-work/index.html b/2022/do-not-want-to-work/index.html new file mode 100644 index 0000000000..2c9e876770 --- /dev/null +++ b/2022/do-not-want-to-work/index.html @@ -0,0 +1,502 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 不想上班 | 贾攀的流水账 + + + + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

贾攀的流水账

+ +
+

Panmax's Blog

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+ + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ 不想上班 +

+ + +
+ + + + +
+ + +

+

今天是国庆节最后一天,可是我极度不想上班,所以又继续请了 2 天假。

+

最近这大半年来我特别不愿意面对工作,对工作持续性没有热情,偶尔有热情的时候是在纯粹写代码的那几个小时。那几个小时里不用考虑和人打交道,不用考虑怎么在晨会、周会上汇报工作,不用去迎合他人做让自己违心的事。

+

还有个原因是我不喜欢被当成未成年人那样去管理,我更喜欢靠完全的自驱去工作,和 LD 沟通好「双赢协议」后他的工作就完成了、就可以退场了,他所要做的就是做好后勤工作,而不是每天来问一问进度或者开会让每个人秀一下自己的工作量。对我的管理越紧我会越认为是对我的不信任,我也越会以敷衍作为回报。

+

另一个对工作不再有热情的原因是认清了一些现实,之前会幻想自己可以靠技术改变世界,靠技术发大财,现在不再有这样的想法了,对技术的热情也没有那么高了,反而会考虑如果可以的话应该在业务方面更深入一些,技术不是核心,至少对于大部分互联网公司是这样的。

+

我在刚工作的时候特别喜欢上班,虽然那个时候公司周末不加班,平时 6 点就下班,但我还是会在下班的时间在公司以外的地方写公司的代码。

+

我记得很清楚的是自己刚来北京的时候,那时候连房都没有租到,和一个大学认识的朋友一起住在他老家一个哥哥的工作室里,那里白天需要办公,我俩早上起床后把铺盖收到一个橱柜里,晚上再拿出来铺在地上睡觉。我有过几次整晚不睡觉去写代码,而且是非常心甘情愿非常开心地写代码。

+

找不到当时打地铺的照片了,只找到一张在那个工作室住了半年后租到房子时要搬家前的一张照片。

+

+

现在绝对不会再这样做了,现在我晚上到家后连打开电脑的欲望都没有,甚至周末都不想动一下电脑,也不会再去看技术书籍,周末的时候也不会看书了,就纯粹歇着虚度时光,我躺平了,这种躺平给我带来的坏处是技术方面不带成长,好处是我不用再那么频繁的复用抗焦虑药物了,从之前的一周有 4 天要吃药,降低到了现在的一周只需要吃 1-2 次。

+

虽然不认可现在公司的所作所为,但我也不想去找工作,我不是面试选手,而且现在整体经济也在下行,我在教育背景、工作履历上都没有优势。

+

我发现我现在越来越喜欢读鸡汤书了,因为工作中遇到的都是糟心事,读一读鸡汤多少能给我一些慰藉。

+

我甚至已经把工作当成了对自己的一种折磨,比如我不会在工作日吃美食,因为一点吃饭的心情都没有,而且那也是对美食的不尊重,工作日凑合吃一口让自己不至于饿死就行,工作日的时候朋友找我约饭我也都会推掉(不管是中午还是晚上)。这样导致的另一个问题是:到了周末我会暴饮暴食,每周工作日 5 天掉的称周末两天我可以翻倍补回来。国庆节休息这几天我已经涨了 6 斤了🙁。

+

去他妈的工作、去他们的 OKR、去他妈的 KPI。

+ +
+ + + + + + + +
+ + + + + + +
+ +
+ + + +
+ + + + + + + + +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2022/doctor-Inconsistent-caliber/index.html b/2022/doctor-Inconsistent-caliber/index.html new file mode 100644 index 0000000000..3d9397e123 --- /dev/null +++ b/2022/doctor-Inconsistent-caliber/index.html @@ -0,0 +1,500 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 医生不一致的口径 | 贾攀的流水账 + + + + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

贾攀的流水账

+ +
+

Panmax's Blog

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+ + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ 医生不一致的口径 +

+ + +
+ + + + +
+ + +

今年年初我确诊了桥本甲状腺炎,其实好几年前在体检时就发现了甲状腺有问题,但一直都没有去医院复查过,也错过了最佳干预时间。

+

我是去的家附近的一家三甲医院,前几次都是找的同一个医生,刚开始她一直给我开的甲功七项检查单,大概检查几次抽了几次血后,她确诊我是甲状腺功能减退症,开始让我服用优甲乐,从半片开始吃。一周后复查,还是查的甲功七项,优甲乐计量改为一片,之后又找这个医生查了一次,依旧开甲功七项,药的计量加到了一片半,并且我基本每次找这个医生的时候都会问,有没有什么注意事项,这个医生说没有,每次开的单子上,描述处也都是写的甲状腺功能减退,从来没用过桥本甲状腺炎这个词。中间我有一次好奇,就问医生桥本甲状腺炎和我的病是什么关系,医生说是一个意思。

+

后来有一次我去医院,刚好之前一直看的医生不在,我找了另一个医生,她给我开的化验单是甲功三项,而且跟我说了很多注意事项,比如不能吃海带、饮食清淡。临走时我问她坐诊时间是哪几天,她说不用必须找她,其他医生也可以,但之前那个医生跟我说了她哪天当班,让我固定找她治疗。

+

再往后我就什么时候有时间什么时候挂号复查,每次遇到的医生也不一样,我发现医生的口径各有区别,比如:

+
    +
  • 这个医生说 2 周后来抽血复查、那个医生说 3-4 周来一次就行;
  • +
  • 这个医生开甲功三项的检查单、那个医生开甲功五项、另一个医生开甲功七项;
  • +
  • 这个医生说没有什么要注意的、另一个医生让我注意这个注意那个;
  • +
  • 这个医生让我下次还找她,那个医生让我随便挂号;
  • +
  • 这个医生给我开的病历上写甲状腺功能减退,另一个医生开的单子写桥本甲状腺炎。
  • +
+

我也不知道为什么会有这么大差别,也许在医生看来这些小小的不同无关紧要,但是对病人来说会让他们不知所措。我不知道该听谁的,如果选择的话,我当然想听最权威的那个,但是这几个医生的经验和资历对我来说都是黑盒。

+

也许这些医生所接受的教育、培训不同,才给出了不同的回答,让我想到我之前看的读库上边介绍过的循证医学。当前医生对患者的判断大多是基于主观的,中间掺杂了自己的个人信仰。希望在未来能通过一些技术手段使循证医学得到推广。

+

从确诊桥本到现在,快半年时间了,我的优甲乐计量还在不停的调整,中间 TSH 降低到过 0.4 以下,还涨到过 20 多,药量不变的情况下,TSH 在两个极端徘徊,每过几周抽管血,优甲乐要每日、终身服用,这是我在未来要习以为常的事项。想找一些桥本的患者交流交流经验,但目前在认识的人里还没有找到同样得这个病的,只有一个同事的母亲有这个病,交流起来也不大方便,我查了一下患病率大约是 5%,也就是说 20 个人里会有一个。

+

有一个症状我能明显感觉得到了大大的改善,就是喉咙的压迫感,上周五感受尤其明显,当天下午在新的组内做了一次接近两小时的业务串讲,晚上还去参与了一个饭局,也说了很多话,没有任何不适感。治疗前我只要说话时间久一点,比如主持一场会议、做一次分享,讲一会话后就会有被锁喉的感觉,喉咙中卡住了东西,咽也咽不下去,说话声音明显变得沙哑,必须很用力才能发出声音。

+ +
+ + + + + + + +
+ + + + + + +
+ +
+ + + +
+ + + + + + + + +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2022/event-sourcing-vs-event-driven-architectures/index.html b/2022/event-sourcing-vs-event-driven-architectures/index.html new file mode 100644 index 0000000000..bae7042f4b --- /dev/null +++ b/2022/event-sourcing-vs-event-driven-architectures/index.html @@ -0,0 +1,540 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 事件溯源与事件驱动架构的区别 | 贾攀的流水账 + + + + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

贾攀的流水账

+ +
+

Panmax's Blog

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+ + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ 事件溯源与事件驱动架构的区别 +

+ + +
+ + + + +
+ + +

名词定义

事件驱动架构:Event Driven Architecture,简称 EDA
事件溯源:Event Sourcing,简称 ES

+

写在前边

我之前对「事件驱动架构」和「事件溯源」这两个概念的理解是比较模糊的,所以查了下资料,结论是「事件驱动架构」和「事件溯源」没有太多的可对比性。为了说明这两个概念的区别,后文我会从目的、范围、数据存储、可测试性这几方面分别对「事件驱动架构」和「事件溯源」做下介绍。

+

事件溯源

事件溯源指的是将应用状态的所有变化存储为一连串事件的系统。一个常见的例子是支持事务的数据库系统,它将所有状态变化存储在事务日志中。

+

在事件溯源中,术语「事件」指的不仅仅是「通知」,更多指的是「状态变化」。事件溯源使用只追加存储来记录对数据采取的完整系列操作,而不是仅存储域中数据的当前状态。因此,所有的历史操作都会被保留。

+

事件驱动架构

事件驱动架构这一术语可用于任何类型的软件系统,它基于仅通过事件进行通信的组件。事件驱动架构是一种松耦合、分布式的驱动架构,收集到某应用产生的事件后实时对事件采取必要的处理后路由至下游系统,无需等待系统响应。

+

在事件驱动架构中,一个事件可以被定义为「状态的重大变化」。在事件驱动架构的背景下,术语「事件」通常意味着「通知」。

+

事件溯源 vs 事件驱动架构

目的

    +
  • 事件溯源是一种持久化策略的代替方案,目的是保留历史。
  • +
  • 事件驱动架构是一种分布式异步架构模式,用于提升应用程序的扩展性。
  • +
+

范围

    +
  • 事件溯源通常应用于单一的系统或应用
  • +
  • 事件驱动架构在多个系统或应用中使用。作为一种可靠的集成模式,具有很高的灵活性,可迅速响应不断变化的环境。
  • +
+

数据存储

    +
  • 事件溯源有一个中央事件仓库,通常有副本、分片等。它依赖于一个中央数据库。
  • +
  • 事件驱动架构是分布式的,每个组件或处理器都是解耦的,可能各自有独立的仓库。
  • +
+

可测实性

    +
  • 事件溯源更容易测试,因为它可以从头开始重放整个事件序列,直到达到某个状态或情况。
  • +
  • 事件驱动架构很容易单独测试每个组件,但由于这种模式的异步性,对整体的测试就比较复杂了。
  • +
+

综合

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
事件溯源模式事件驱动架构模式
目的保留历史提高适应性和扩展性
范围单一系统或应用多个系统或应用
存储中央事件仓库分布式存储
测试更简单更困难
+

两者都以事件为基础,但其目的、范围和属性却截然不同。

+ +
+ + + + + + + +
+ + + + + + +
+ +
+ + + +
+ + + + + + + + +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2022/hongloumeng-short-video/20220809120852.png b/2022/hongloumeng-short-video/20220809120852.png new file mode 100644 index 0000000000..573f1781ba Binary files /dev/null and b/2022/hongloumeng-short-video/20220809120852.png differ diff --git a/2022/hongloumeng-short-video/index.html b/2022/hongloumeng-short-video/index.html new file mode 100644 index 0000000000..1928c2ca04 --- /dev/null +++ b/2022/hongloumeng-short-video/index.html @@ -0,0 +1,493 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 红楼梦给我的启发-短视频 | 贾攀的流水账 + + + + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

贾攀的流水账

+ +
+

Panmax's Blog

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+ + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ 红楼梦给我的启发-短视频 +

+ + +
+ + + + +
+ + +

+

这个启发来自「谐星聊天会」的第三季第 6 期,大概 46 分钟左右家宇提出的。

+

在红楼梦中有这么一回,贾瑞勾引王熙凤后被王熙凤下了「相思局」,致使贾瑞卧床不起。破足道人为了救他给了他一把风月宝鉴,也就是一面镜子,并告诉贾瑞只能看镜子背面,不要看正面,但是贾瑞不听劝告,非要看正面,每一次看正面就看到凤姐在里边搔首弄姿勾引他,笑盈盈的招手让他进去和她交欢,这么几十次后,贾瑞就因下溺连精,精尽人亡而死。

+

贾瑞明知道把镜子翻过来能治自己的病,可是翻过来看到的是个骷髅,而另一面有个王熙凤在里边扭动,他在欲望面前无法控制自己,最后使自己命丧黄泉。

+

这个就很像我们现在刷短视频,比如快手、抖音这些。我们一刷就几个小时过去了,刷完之后很有负罪感。短视频是这个时代的精神鸦片,过一段时间不刷心里就会发痒,刷之前认为自己有足够的控制力,只刷几分钟就停下来。刷的过程中也明知道自己只要把手机扣过来就可以解决问题,可自己就是控制不住不停的往下刷,像贾瑞一样不停的看镜子中虚幻的王熙凤。贾瑞丢掉的是性命,我们何尝不是在消耗自己的生命呢。

+

跛足道人作为红楼梦中菩萨的象征,是无法直接救贾瑞和我们的,菩萨只能点化我们,能救赎我们的只有自己。

+ +
+ + + + + + + +
+ + + + + + +
+ +
+ + + +
+ + + + + + + + +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2022/how-cdn-work/20220626201736.jpg b/2022/how-cdn-work/20220626201736.jpg new file mode 100644 index 0000000000..4057c42dba Binary files /dev/null and b/2022/how-cdn-work/20220626201736.jpg differ diff --git a/2022/how-cdn-work/index.html b/2022/how-cdn-work/index.html new file mode 100644 index 0000000000..3ef4636d58 --- /dev/null +++ b/2022/how-cdn-work/index.html @@ -0,0 +1,512 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + CDN 是如何工作的? | 贾攀的流水账 + + + + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

贾攀的流水账

+ +
+

Panmax's Blog

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+ + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ CDN 是如何工作的? +

+ + +
+ + + + +
+ + +

维基百科给 CDN 的定义如下:

+
+

内容分发网络Content Delivery Network 或Content Distribution Network,缩写:CDN)是指一种透过互联网互相连接的电脑网络系统,利用最靠近每位用户的服务器,更快、更可靠地将音乐、图片、视频、应用程序及其他文件发送给用户,来提供高性能、可扩展性及低成本的网络内容传递给用户。

+
+

我们用更精简的语句概括一下:CDN 是一种利用分布在各个地理位置的服务器来提供快速内容交付的技术。

+
    +
  • 这里的服务器我们也称为边缘(edge)服务器
  • +
  • 交付的内容包括静态内容和动态内容
  • +
+

假如住在北京的小贾想要访问一个部署在杭州的电商网站,如果这个请求历经大半个中国进入位于杭州的服务器再返回,响应会非常慢。因此,那个电商网站可以在小贾居住地附近部署 CDN 服务器,网站的内容将从附近的 CDN 服务器加载。

+

下图说明了这个过程:

+

20220626201736.jpg

+
    +
  1. 小贾在浏览器中输入 www.taobao.com ,浏览器在本地 DNS 缓存中查找该域名对应的 IP 地址。
  2. +
  3. 如果没有在本地 DNS 缓存中找到该域名,浏览器就会去找 DNS 解析器进行域名解析。DNS 解析器通常位于互联网服务供应商(ISP,如中国联通、中国电信)。
  4. +
  5. DNS 解析器通过递归的方式解析域名,最终它会要求权威名称服务器(Authoritative Name Server)查找该域名。
  6. +
  7. 如果我们不使用 CDN,权威名称服务器会返回 www.taobao.com 位于杭州的 IP 地址。使用 CDN 后,权威名称服务器会返回一个别名指向 www.taobao.cdn.com (CDN 服务器的域名,这里只是举例,taobao 的 CDN 域名以实际为准)。
  8. +
  9. DNS 解析器找到 CDN 权威名称服务器解析 www.taobao.cdn.com
  10. +
  11. CDN 权威名称服务器再次返回一个别名:CDN 负载均衡器的域名 www.taobao.lb.com
  12. +
  13. DNS 解析器继续要求 CDN 负载均衡器解析 www.taobao.lb.com ,负载均衡器根据用户的 IP 地址、ISP、请求的内容和服务器负载状况等条件选择一个最佳的 CDN 边缘服务器。
  14. +
  15. CDN 负载均衡器返回 CDN 边缘服务器的 IP 地址。
  16. +
  17. DNS 解析器将得到的 CDN 边缘服务器 IP 地址返回给浏览器。
  18. +
  19. 浏览器访问 CDN 边缘服务器加载网站内容。CDN 服务器上缓存了静态和动态两种类型的内容,前者包含静态页面、图片、视频,后者包含边缘计算的结果。
  20. +
  21. 如果 CDN 边缘服务器的缓存中没有找到用户需要的内容,它就将请求发给该地区(如华北大区)的 CDN 服务器。如果仍然没有找到,会将继续请求更上一级的中央 CDN 服务器,以此类推最终有可能会请求到源站,也就是位于杭州的服务器。这就是所谓的 CDN 分布式网络,其中服务器被部署在不同的地理位置。
  22. +
+ +
+ + + + + + + +
+ + + + + + +
+ +
+ + + +
+ + + + + + + + +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2022/how-to-write/1.jpg b/2022/how-to-write/1.jpg new file mode 100644 index 0000000000..cd42c6aef2 Binary files /dev/null and b/2022/how-to-write/1.jpg differ diff --git a/2022/how-to-write/index.html b/2022/how-to-write/index.html new file mode 100644 index 0000000000..684b44bd46 --- /dev/null +++ b/2022/how-to-write/index.html @@ -0,0 +1,509 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 如何写作 | 贾攀的流水账 + + + + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

贾攀的流水账

+ +
+

Panmax's Blog

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+ + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ 如何写作 +

+ + +
+ + + + +
+ + +

+

翻译自:https://www.swiss-miss.com/2019/09/how-to-write-by-elizabeth-gilbert.html

+

1) 向某人讲述你的故事。挑选一个你爱的或钦佩的或想与之联系的人,把整个故事直接写给他们——就像你在写一封信。这将带来你的自然声音。无论你做什么,都不要写信给人群。

+

(想象我们所写的内容是在给一个喜欢的姑娘或者我钦佩的人写信,而不是写给一群人的说教。)

+

2) 从故事的开头开始,写出发生的事情,一直写到最后。

+

(有头有尾,循序渐进)

+

3) 使用极其简单的句子。

+

(能说清楚事情就行,不追求华丽的辞藻)

+

4) 不要担心它是否好;只要完成它。无论你的项目是否好,结束后你会成为一个不同的人,这总是值得做的。

+

(完成比完美更重要,只要完成就会有成长。写作不是一蹴而就的,而来你有的是机会来修改、打磨它。)

+

5) 不要以改变任何人的生活为目的而写作。这将导致沉重的、令人恼火的散文。只需分享令你高兴、愤怒或着迷的东西。如果有人的生活因此而改变,那是一种奖励。

+

(写作的目的不是要改变其他人,而是记录自己的所思所想。曹雪芹的《红楼梦》中从来没有批判过一个人的好坏,只是在做客观的描写。)

+

6) 只要你可以,就讲故事而不是解释东西。人类喜欢故事,而我们讨厌别人向我们解释东西。以耶稣为例。他几乎只用比喻说话,并允许每个人从他伟大的讲故事中吸取自己的教训。而且他做得非常好。

+

(多讲故事,人们更容易记住有画面感的东西)

+

7) 你的作品不必是任何特定的长度,或为任何特定的市场而写。它甚至不一定要被另一个人看到。如何以及是否出版你的作品是另一个问题。今天,就写吧。

+

(想写什么写什么,不追求写多长。)

+

8) 记住,您一直在研究自己的一生,只是因为存在。你是你自己经验中的唯一专家。拥抱这一点是你的最高资格。

+

(这句话没明白什么意思,是只有自己才了解自己的意思吗?)

+

9) 每位作家在第一天都是从同一个地方开始的:超级兴奋,并准备好做大事。第二天,每个作家都看着她在第一天写的东西,恨死自己了。专业作家和非专业作家区别在于,专业的作家在第三天回到他们的任务中。让你达到目的的不是骄傲而是怜悯。向自己表示宽恕,因为你不够好。然后继续前进。

+

(成功最大的威胁不是失败,而是倦怠。当你感到心烦意乱,苦不堪言或筋疲力竭时,是鼓足干劲还是萌生退意,这是专业人士和业余人士的分水岭。)

+

10) 愿意让它变得简单。你可能会感到惊讶。

+

(这句话也没有太理解,是想说我们应该把写作当成一件简单的事情去做的意思吗?)

+ +
+ + + + + + + +
+ + + + + + +
+ +
+ + + +
+ + + + + + + + +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2022/i-am-positive-i-am-ok/IMG_7238.jpg b/2022/i-am-positive-i-am-ok/IMG_7238.jpg new file mode 100644 index 0000000000..f679c802fc Binary files /dev/null and b/2022/i-am-positive-i-am-ok/IMG_7238.jpg differ diff --git a/2022/i-am-positive-i-am-ok/IMG_7240.jpg b/2022/i-am-positive-i-am-ok/IMG_7240.jpg new file mode 100644 index 0000000000..aa9147076a Binary files /dev/null and b/2022/i-am-positive-i-am-ok/IMG_7240.jpg differ diff --git a/2022/i-am-positive-i-am-ok/IMG_7246.jpg b/2022/i-am-positive-i-am-ok/IMG_7246.jpg new file mode 100644 index 0000000000..a0dcc55605 Binary files /dev/null and b/2022/i-am-positive-i-am-ok/IMG_7246.jpg differ diff --git a/2022/i-am-positive-i-am-ok/IMG_7247.jpg b/2022/i-am-positive-i-am-ok/IMG_7247.jpg new file mode 100644 index 0000000000..fb7dd14d37 Binary files /dev/null and b/2022/i-am-positive-i-am-ok/IMG_7247.jpg differ diff --git a/2022/i-am-positive-i-am-ok/index.html b/2022/i-am-positive-i-am-ok/index.html new file mode 100644 index 0000000000..2a31c5bb7b --- /dev/null +++ b/2022/i-am-positive-i-am-ok/index.html @@ -0,0 +1,523 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 我阳了,我好了 | 贾攀的流水账 + + + + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

贾攀的流水账

+ +
+

Panmax's Blog

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+ + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ 我阳了,我好了 +

+ + +
+ + + + +
+ + +

结论先行,先说说我是吃了什么药康复的:

+
    +
  • 第一晚和第二晚每次一粒对乙酰氨基酚
  • +
  • 第二天白天吃了一小瓶桃罐头
  • +
+

是的,就这么些东西。

+

下边记录一些中间的过程。

+

第一天

12月13日,星期二

+

中午的时候嗓子开始不舒服,有点沙沙的感觉,而且腰有些酸,以为是坐姿有问题,总想躺着,多亏是在家办公,工作一会躺一会。

+

5:30左右测了个体温,37.3℃,没太当回事,但是明显感觉体力开始急剧下降。开完公司晚会后,7:30再次测了个体温 38.5℃,这时候已经完全不想动弹了,浑身发冷一直打寒颤,冷到想盖上10层棉被。

+

晚上挣扎着洗了澡,睡前吃了粒对乙酰氨基酚,钻被窝盖了两层被子,半昏半睡、睡一会醒一会,中间还出现过幻觉,虽然一晚上无法动弹没有测过体温,但我自己估摸着肯定到了40℃,晚上摸身上火烧火燎的,浑身疼,甚至蛋蛋也疼。。。

+

第二天

2022年12月14日,星期三

+

艰难的爬起床,浑身疼,那种疼像是跑了10公里步或者被揍了一顿似的,于是和老板请假,之后将手机通知关闭、静音。这一天除了吃饭喝水上厕所其余时间就是躺着,中间还吃了个黄桃罐头,冰冰凉凉的吃下去的时候很舒服。

+

+

就这么难受我还加持把今天的多邻国学习了,为了不破坏200多天的连胜记录😂

+

我把两层窗帘拉紧,灯关掉,屋里完全黑的,就这样睡一会、醒一会、刷一会小红书,这天还有些拉肚子,但不是很稀。晚上睡觉前吃了一粒对乙酰氨基酚。

+

晚上八点多睡的,到第二天早上5点,加上白天的时间,这应该是我近些年卧床时间最长的一次。

+

第三天

2022年12月16日,星期四

+

一觉醒来感觉舒服多了,测了下体温也基本退烧了,身上也没那么疼了,就是嗓子巨疼无比,开始咳嗽,咳嗽时喉咙和肺疼,能咳出浓痰。

+

考虑的目前是居家办公,也不用通勤,实在累了也可以躺会,于是就没有再请假,强行开机开始上班搬砖了。

+

下午的时候测了个抗原,阳气十足。

+

+

第四天(今天)

2022年12月16日,星期五

+

今天算起来是得病的第4天,嗓子有些疼、咳嗽,说话非常非常吃力且沙哑,除此之外就没有其他症状了。

+

嗅觉味觉还在,但是貌似不那么灵敏了,预计还需要3、5天才能转阴。

+

昨晚睡的有些晚,而且睡前玩了会手机,之前从来不玩,但是根据前两天生病的经验发现玩手机也能睡着。将近12点放下手机的时候感觉还是没有困意就开始看书,看到胳膊举不动书了放下书开始尝试入睡,两点多还是没睡着我意识到失眠了,于是起来吃了安眠药。

+

只有生病最严重的那两天我真正让自己放轻松了,不管再晚再难受也不会感觉有什么焦虑,不再考虑工作或者其他烦心的事情,可能是身体的本能告诉我狗命要紧,别考虑乱七八糟的了。

+

我得新冠后只耽误了一天工作,真是个合格的打工人。🙂

+

P.S. 我发现这几天都没有晨勃过了,可能是不行了吧。

+
+

我前几天续订了独库的2023全年阅读计划,今天刚好收到了一份读库提前送来的小礼物,如果今年让我推荐一本书的话,毫无疑问我会推荐读库,他不是一本书,而是一个每两个月发行一期的综合性人文社科读物,以中篇非虚构文章为主,内容包括传记、书评、影评、历史事件等。

+


+ +
+ + + + + + + +
+ + + + + + +
+ +
+ + + +
+ + + + + + + + +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2022/land-surfboard-study-week2/20220725105210.png b/2022/land-surfboard-study-week2/20220725105210.png new file mode 100644 index 0000000000..b1c061b6d3 Binary files /dev/null and b/2022/land-surfboard-study-week2/20220725105210.png differ diff --git a/2022/land-surfboard-study-week2/IMG_5536.mp4 b/2022/land-surfboard-study-week2/IMG_5536.mp4 new file mode 100644 index 0000000000..78b1e6c529 Binary files /dev/null and b/2022/land-surfboard-study-week2/IMG_5536.mp4 differ diff --git a/2022/land-surfboard-study-week2/index.html b/2022/land-surfboard-study-week2/index.html new file mode 100644 index 0000000000..1fc4558710 --- /dev/null +++ b/2022/land-surfboard-study-week2/index.html @@ -0,0 +1,497 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 陆地冲浪板学习-week2 | 贾攀的流水账 + + + + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

贾攀的流水账

+ +
+

Panmax's Blog

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+ + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ 陆地冲浪板学习-week2 +

+ + +
+ + + + +
+ + +

本周学会了通过小幅度 pumping 加速,也学了大幅度的 pumping,但是姿势很不协调,尤其是上半身,不过我自己很满意,现在已经可以不下板的情况下一直滑了。

+

昨天,也就是周六,去公司做了一天的校招面试官,所以没有去上滑板课。周日上午学习了一小时,中午训练了 20 分钟,晚上训练了一个半小时,而且找到了一个非常棒的训练地点,全程无车无人,滑的非常进行。下周继续加油,把姿势做的优雅一些。

+

今天上课的时候右脚小拇指磨了一个泡,晚上训练的时候裹了个创可贴,不那么疼了。

+

+

找到了两个入门陆地冲浪板的比较好的教程:

+ +

因为学会了新的技巧,晚上有些过度兴奋,到了后半夜才睡着,最后放个视频留个纪念。

+ +
+ + + + + + + +
+ + + + + + +
+ +
+ + + +
+ + + + + + + + +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2022/land-surfboard-study-week3/IMG_5611.jpeg b/2022/land-surfboard-study-week3/IMG_5611.jpeg new file mode 100644 index 0000000000..de954fbde4 Binary files /dev/null and b/2022/land-surfboard-study-week3/IMG_5611.jpeg differ diff --git a/2022/land-surfboard-study-week3/IMG_5612.jpeg b/2022/land-surfboard-study-week3/IMG_5612.jpeg new file mode 100644 index 0000000000..d372cdde36 Binary files /dev/null and b/2022/land-surfboard-study-week3/IMG_5612.jpeg differ diff --git a/2022/land-surfboard-study-week3/IMG_5615.jpeg b/2022/land-surfboard-study-week3/IMG_5615.jpeg new file mode 100644 index 0000000000..9d39dbebed Binary files /dev/null and b/2022/land-surfboard-study-week3/IMG_5615.jpeg differ diff --git a/2022/land-surfboard-study-week3/index.html b/2022/land-surfboard-study-week3/index.html new file mode 100644 index 0000000000..439907e46f --- /dev/null +++ b/2022/land-surfboard-study-week3/index.html @@ -0,0 +1,499 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 陆地冲浪板学习-week3 | 贾攀的流水账 + + + + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

贾攀的流水账

+ +
+

Panmax's Blog

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+ + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ 陆地冲浪板学习-week3 +

+ + +
+ + + + +
+ + +

没想到自己在 30 岁的时候找到了一个爱好,陆冲滑起来很上头,可以靠肩、跨、腿、脚的配合让板子动起来,从而不用蹬地也可以前行。

+

我为什么喜欢陆冲呢?

+

我想是因为我喜欢快速滑动时的速度感,还喜欢突然找到某种感觉、某个发力点和学会某个技巧后的喜悦感。喜欢体验一个人独处时专注沉浸在滑板上的那种心流,还有运动时带来的多巴胺。

+

这周是学习陆地冲浪板第 3 周,继续练习 pumping,姿势还不能做得特别优雅,主要是往正手侧转动的幅度太小,另一个问题是视线没有打开、头没有跟着肩膀一起转动。这周还练习了小幅度荡板,但我的重心一直保持不好,做的不是很好,平时还要多练习。自己练习荡板的时候重重的摔了一跤,多亏当时戴着护具,只把手腕顶了一下,没有大碍。

+

我在家附近找到一个体育场,在一个大院里,里边很大,有足球场、篮球场羽毛球馆等等,不过人很少,很像一个部队大院。我绕着大院最里边的一个场馆外的路练习,没有来往的车辆和行人,只有风声、树声,因为路的两边都有高墙所以很凉爽。

+



+

最后再放个视频记录下这周的练习进程:

+ + +

又找到了一个教陆冲很好的 Up 主,比之前找到的教学内容更全面、更详细:纷飞的大脚

+ +
+ + + + + + + +
+ + + + + + +
+ +
+ + + +
+ + + + + + + + +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2022/land-surfboard-study-week3/pumping.mp4 b/2022/land-surfboard-study-week3/pumping.mp4 new file mode 100644 index 0000000000..f4a2c5387b Binary files /dev/null and b/2022/land-surfboard-study-week3/pumping.mp4 differ diff --git a/2022/land-surfboard-study-week4/59_raw.mp4 b/2022/land-surfboard-study-week4/59_raw.mp4 new file mode 100644 index 0000000000..9c04376fec Binary files /dev/null and b/2022/land-surfboard-study-week4/59_raw.mp4 differ diff --git a/2022/land-surfboard-study-week4/IMG_5659.jpeg b/2022/land-surfboard-study-week4/IMG_5659.jpeg new file mode 100644 index 0000000000..125b7c57c4 Binary files /dev/null and b/2022/land-surfboard-study-week4/IMG_5659.jpeg differ diff --git a/2022/land-surfboard-study-week4/IMG_5660.jpeg b/2022/land-surfboard-study-week4/IMG_5660.jpeg new file mode 100644 index 0000000000..ee64834f59 Binary files /dev/null and b/2022/land-surfboard-study-week4/IMG_5660.jpeg differ diff --git a/2022/land-surfboard-study-week4/IMG_5663.jpeg b/2022/land-surfboard-study-week4/IMG_5663.jpeg new file mode 100644 index 0000000000..09a2bb8a5f Binary files /dev/null and b/2022/land-surfboard-study-week4/IMG_5663.jpeg differ diff --git a/2022/land-surfboard-study-week4/IMG_5675.MOV.mp4 b/2022/land-surfboard-study-week4/IMG_5675.MOV.mp4 new file mode 100644 index 0000000000..302bf4a2f3 Binary files /dev/null and b/2022/land-surfboard-study-week4/IMG_5675.MOV.mp4 differ diff --git a/2022/land-surfboard-study-week4/IMG_5677.mp4 b/2022/land-surfboard-study-week4/IMG_5677.mp4 new file mode 100644 index 0000000000..47eeef13c5 Binary files /dev/null and b/2022/land-surfboard-study-week4/IMG_5677.mp4 differ diff --git a/2022/land-surfboard-study-week4/index.html b/2022/land-surfboard-study-week4/index.html new file mode 100644 index 0000000000..5a3fccd557 --- /dev/null +++ b/2022/land-surfboard-study-week4/index.html @@ -0,0 +1,511 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 陆地冲浪板学习-week4 | 贾攀的流水账 + + + + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

贾攀的流水账

+ +
+

Panmax's Blog

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+ + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ 陆地冲浪板学习-week4 +

+ + +
+ + + + +
+ + +

这周陆冲学习了三个技能,折叠 pumping、单膝跪板转弯、slide。

+

slide⬇️:

+ + +

给各位跪一个⬇️:

+ + + +

这几个都不是一时半会能学会的,需要熟能生巧,尤其是折叠要自己找感觉,我现在做得很扭捏、肩膀无法放松,做 slide 需要一定的胆量,我在做 slide 的时候摔了两次,从滑板上下来了几次。

+

我通常是开车去学滑板的地方,单程差不多 25 到 30 分钟,在来回的路上我一般听博客,优先听最新一期的「谐星聊天会」,上周尝试坐了次地铁去上课,路上没有听,所以就攒了两期,这两期一期是讨论短视频给生活带来的影响,另一期是讨论和朋友在一起的时候能玩点什么。

+

短视频那个有一段用红楼中的贾瑞之死来做比喻,简直太棒了,我准备之后单独水一篇短文来介绍下贾瑞之死和短视频之间的关系,而且计划写一系列红楼梦带给我的启发文章。

+

和朋友一起玩什么那一期,开头问到最近和朋友在什么时间玩了什么,我想了想,我想现在几乎没有任何社交活动了,顶多偶尔和两三个同事约顿饭,频率也不会超过两周一次,而且通常选择中午时间,1 小时纯吃,晚上会耽误下班。至于上一次玩是什么时候,我想了想大概是 7 月中旬参加的一次团建,吃完饭后一起打了打德扑,也是那次学会了德扑。

+

我本身也不太喜欢很多人一起的 Social 活动,所以滑板很适合我这样的人,能多人一起练活、也能自己一个人享受滑行时的沉浸感。

+

周六我在上完课回来的路上天气突然阴了下来,不知道为什么我特别喜欢这样阴阴的天气,于是拍了几张照片记录下:

+

+

+

+

这么快一个月就过去了,截止目前一共上了 8 次课,还剩最后 4 次,预计再有 2-3 周就上完了。按照平均每次上课和课后练习一共 3 小时,再加上平时的一些练习来算的话,我在陆冲上投入差不多有 30 个小时,已经可以使用陆冲来刷街了。

+

刷街⬇️:

+ + +

今天周一,我开始尝试滑着滑板到地铁,然后下地铁后滑到公司,很顺利。为了减负,我把背包也换了个更轻便的,里边只有一个 iPad 和一把雨伞。

+ +
+ + + + + + + +
+ + + + + + +
+ +
+ + + +
+ + + + + + + + +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2022/land-surfboard-study-week5/01e2b707083d4d7401037003819b72604c_4610.mp4video.mp4 b/2022/land-surfboard-study-week5/01e2b707083d4d7401037003819b72604c_4610.mp4video.mp4 new file mode 100644 index 0000000000..15ecb0310e Binary files /dev/null and b/2022/land-surfboard-study-week5/01e2b707083d4d7401037003819b72604c_4610.mp4video.mp4 differ diff --git a/2022/land-surfboard-study-week5/01e2da2ff23dba79010370038243275db1_4610.mp4video.mp4 b/2022/land-surfboard-study-week5/01e2da2ff23dba79010370038243275db1_4610.mp4video.mp4 new file mode 100644 index 0000000000..a4793a8264 Binary files /dev/null and b/2022/land-surfboard-study-week5/01e2da2ff23dba79010370038243275db1_4610.mp4video.mp4 differ diff --git a/2022/land-surfboard-study-week5/20220815110240.png b/2022/land-surfboard-study-week5/20220815110240.png new file mode 100644 index 0000000000..e82fa8c0b1 Binary files /dev/null and b/2022/land-surfboard-study-week5/20220815110240.png differ diff --git a/2022/land-surfboard-study-week5/IMG_5757.MOV.mp4 b/2022/land-surfboard-study-week5/IMG_5757.MOV.mp4 new file mode 100644 index 0000000000..c7b23eb05b Binary files /dev/null and b/2022/land-surfboard-study-week5/IMG_5757.MOV.mp4 differ diff --git a/2022/land-surfboard-study-week5/IMG_5759.MOV.mp4 b/2022/land-surfboard-study-week5/IMG_5759.MOV.mp4 new file mode 100644 index 0000000000..fac5f6c643 Binary files /dev/null and b/2022/land-surfboard-study-week5/IMG_5759.MOV.mp4 differ diff --git a/2022/land-surfboard-study-week5/IMG_5760.MOV.mp4 b/2022/land-surfboard-study-week5/IMG_5760.MOV.mp4 new file mode 100644 index 0000000000..04f6ff6f20 Binary files /dev/null and b/2022/land-surfboard-study-week5/IMG_5760.MOV.mp4 differ diff --git a/2022/land-surfboard-study-week5/IMG_5765.jpeg b/2022/land-surfboard-study-week5/IMG_5765.jpeg new file mode 100644 index 0000000000..5f24756683 Binary files /dev/null and b/2022/land-surfboard-study-week5/IMG_5765.jpeg differ diff --git a/2022/land-surfboard-study-week5/IMG_5776.jpeg b/2022/land-surfboard-study-week5/IMG_5776.jpeg new file mode 100644 index 0000000000..d62e5b1af3 Binary files /dev/null and b/2022/land-surfboard-study-week5/IMG_5776.jpeg differ diff --git a/2022/land-surfboard-study-week5/index.html b/2022/land-surfboard-study-week5/index.html new file mode 100644 index 0000000000..2e40d48ea9 --- /dev/null +++ b/2022/land-surfboard-study-week5/index.html @@ -0,0 +1,514 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 陆地冲浪板学习-week5 | 贾攀的流水账 + + + + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

贾攀的流水账

+ +
+

Panmax's Blog

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+ + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ 陆地冲浪板学习-week5 +

+ + +
+ + + + +
+ + +

本周是学习陆地冲浪板的第五周,我对它的喜爱依然是热度不减,看来真的是找到自己的爱好了。

+

这周因为接到一个 P0 的项目,周六到公司加了天班,所以只上了一次课,这应该是我们搬到望京 SOHO 后第一次周末因为项目进度到公司加班,而且这几个月我印象中周末只加过 3 次班。第一次是 5 月份居家办公期间,也是接了个倒排期的需求,那时候是在家加班,正好小区也不能出去,加班还能换天调休。第二次是上个月到公司做校招的面试官,第三次就是昨天了。

+

这周学习了前两周学习过的荡板,课程最后十几分钟还学了 piovt 180。荡板是为 piovt 180 打基础,而 piovt 180 又是为尾刹 180 和 360 打基础,piovt 还可以跟 slide 180 连起实现 360 的旋转(如下边第一个视频),而且会了 piovt 180 就可以去刷碗池了。

+

别人的 slide180 接 piovt 180:

+ + +

我的荡板练习:

+ + +

我的 piovt 180 练习:

+ + +

刷短视频的时候看到一个用陆冲刷街的小姐姐,太帅了!

+ + +

今天北京一上午都在下雨,我上课的地方虽然是在一个地下二层的商业街,但是那个场地上边被建筑物覆盖了,练习的时候看着前后瀑布一样的雨水很惬意,而且因为下雨今天天气也格外凉爽。

+ + +

开车来回的路上还是听了「谐聊」,这周讨论的是关于浪漫的话题。我也回想了一下,自己早在几年前也是个浪漫的人,尤其是高中和大学期间,现在越来越不浪漫了。当年我也写过藏头诗、拍过 MV、折过千纸鹤,用红楼梦里的一句话就是:「甚荒唐,到头来都是为他人作嫁衣裳」,后边有机会的话会聊聊这段历史,也算得上一段青葱岁月的浪漫往事。

+

这段时间由于转岗没多久的原因,有一段时间没请过假了,我目前有 12 天调休,11 天年假,几乎是用不完的状态。打算等再过一段时间手头工作捋顺了,准备假到「谐聊」现场收听几次。

+

上滑板课回来后吃了个超豪华的螺蛳粉,螺蛳粉里放了「炸响铃」兼职是太好吃了,还用空气炸锅炸了鸡块和鸡米花,看了一集极限挑战,饭后还吃了一根梦龙和榴莲千层切角,腐败了一个下午。

+

+

+

我工位后边的墙边已经被我的东西承包了。

+

+ +
+ + + + + + + +
+ + + + + + +
+ +
+ + + +
+ + + + + + + + +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2022/land-surfboard-study-week6/20220822110611.png b/2022/land-surfboard-study-week6/20220822110611.png new file mode 100644 index 0000000000..6e46f34471 Binary files /dev/null and b/2022/land-surfboard-study-week6/20220822110611.png differ diff --git a/2022/land-surfboard-study-week6/20220822111057.png b/2022/land-surfboard-study-week6/20220822111057.png new file mode 100644 index 0000000000..4e4a638306 Binary files /dev/null and b/2022/land-surfboard-study-week6/20220822111057.png differ diff --git a/2022/land-surfboard-study-week6/20220822111122.png b/2022/land-surfboard-study-week6/20220822111122.png new file mode 100644 index 0000000000..1f8deb13d4 Binary files /dev/null and b/2022/land-surfboard-study-week6/20220822111122.png differ diff --git a/2022/land-surfboard-study-week6/20220822111132.png b/2022/land-surfboard-study-week6/20220822111132.png new file mode 100644 index 0000000000..885940e6e2 Binary files /dev/null and b/2022/land-surfboard-study-week6/20220822111132.png differ diff --git a/2022/land-surfboard-study-week6/20220822111141.png b/2022/land-surfboard-study-week6/20220822111141.png new file mode 100644 index 0000000000..5876265f81 Binary files /dev/null and b/2022/land-surfboard-study-week6/20220822111141.png differ diff --git a/2022/land-surfboard-study-week6/20220822111420.png b/2022/land-surfboard-study-week6/20220822111420.png new file mode 100644 index 0000000000..512218fead Binary files /dev/null and b/2022/land-surfboard-study-week6/20220822111420.png differ diff --git a/2022/land-surfboard-study-week6/20220822111835.png b/2022/land-surfboard-study-week6/20220822111835.png new file mode 100644 index 0000000000..70fcf1a6bd Binary files /dev/null and b/2022/land-surfboard-study-week6/20220822111835.png differ diff --git a/2022/land-surfboard-study-week6/IMG_5848.jpeg b/2022/land-surfboard-study-week6/IMG_5848.jpeg new file mode 100644 index 0000000000..a628336a9f Binary files /dev/null and b/2022/land-surfboard-study-week6/IMG_5848.jpeg differ diff --git a/2022/land-surfboard-study-week6/IMG_5860.jpeg b/2022/land-surfboard-study-week6/IMG_5860.jpeg new file mode 100644 index 0000000000..070c8c6136 Binary files /dev/null and b/2022/land-surfboard-study-week6/IMG_5860.jpeg differ diff --git a/2022/land-surfboard-study-week6/IMG_5866.jpeg b/2022/land-surfboard-study-week6/IMG_5866.jpeg new file mode 100644 index 0000000000..7ead571776 Binary files /dev/null and b/2022/land-surfboard-study-week6/IMG_5866.jpeg differ diff --git a/2022/land-surfboard-study-week6/index.html b/2022/land-surfboard-study-week6/index.html new file mode 100644 index 0000000000..72f072f891 --- /dev/null +++ b/2022/land-surfboard-study-week6/index.html @@ -0,0 +1,521 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 陆地冲浪板学习-week6(摔惨了) | 贾攀的流水账 + + + + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

贾攀的流水账

+ +
+

Panmax's Blog

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+ + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ 陆地冲浪板学习-week6(摔惨了) +

+ + +
+ + + + +
+ + +

这周是学习陆地冲浪板的第六周,正常来说也应该是最后一周了(按照每周两次课来算),不过因为之前有一周只上了一次课,所以这周上完还剩最后一次课。

+

周六上课的前半段学习挥臂肩带转,我一直找不到感觉,做起来像是在自由泳。后半节课学习初级的 drop in,用了个大概 40 厘米的台子,摔了好几次也没学会,因为我这个教练的胳膊肘前段时间骑摩托车摔了,所以他不能拉着我从上往下冲,最后是等另一个教练下课后带我做了几次次找到了感觉。

+

来看下周六学习入门 drop in 的效果:

+ + +

越恐惧越容易摔。

+

周六学习过程中受了点皮肉伤

+

+

+

下课后吃了个豪华冰淇淋聊以慰藉

+

+

然后还去 miniso 根据小红书上的推荐买了个薰衣草味的香水,打算以后没事也喷点香水让自己心情愉悦下

+

+

之后又去我常去的那个体育场练了 2 小时,这天的天气真好

+

+

然而噩梦发生在周日,本来计划周日休息一天,但是实在有些无聊,所以中午的时候和教练约了下午 2 点的课,到另一个有碗池的场地上课,这里的台子是标准 1 米 2 的,在这里练习 drop in 一次也没成功,而且一直摔,教练看我摔的是在有些惨,让我练会别的,尝试在斜面上做 pivot 180,成功了几次,也重重摔了几次,中间有两次摔的连话也说不出来(应该是震到心脏或者肺了),缓了好久。

+

+

现在心脏部位生疼,上半身不能大幅度活动,胳膊腿也又受了几处伤,胯骨的位置摔得一片紫

+

+

+

+

整个腿面上也是青一块紫一块

+

+

跟教练沟通了下,最后一节课还是用来改善体态吧,不学这高难度的了🥲

+

极限运动的归宿是骨科。🙂

+ +
+ + + + + + + +
+ + + + + + +
+ +
+ + + +
+ + + + + + + + +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2022/land-surfboard-study-week6/vue_video_cache_filtered_spliced.mp4 b/2022/land-surfboard-study-week6/vue_video_cache_filtered_spliced.mp4 new file mode 100644 index 0000000000..20abf96f7f Binary files /dev/null and b/2022/land-surfboard-study-week6/vue_video_cache_filtered_spliced.mp4 differ diff --git a/2022/linux-network-util-list/index.html b/2022/linux-network-util-list/index.html new file mode 100644 index 0000000000..eded880a2a --- /dev/null +++ b/2022/linux-network-util-list/index.html @@ -0,0 +1,538 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Linux 常用网络工具清单 | 贾攀的流水账 + + + + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

贾攀的流水账

+ +
+

Panmax's Blog

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+ + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ Linux 常用网络工具清单 +

+ + +
+ + + + +
+ + +

ping

「这些计算机还在线吗?」

+

curl

发送任何你需要的 HTTP 请求。

+

httpie

和 curl 一样,但操作更简单

+

wget

下载文件

+

tc

流量控制命令,可以降低其他人的网速

+

dig / nslookup

「这个域名的 IP 地址是多少?」(DNS 查询)

+

whois

「这个域名注册了吗?」

+

ssh

安全的 shell

+

scp

通过 SSH 协议拷贝文件

+

rsync

只拷贝有过改动的文件(通过 SSH 协议)

+

ngrep

网络版的 grep 命令

+

tcpdump

「把 80 端口的所有网络包展示给我!」

+

wireshark

通过 GUI 查看 tcpdump 抓的包

+

tshark

非常强大的网络报分析命令行工具

+

tcpflow

抓取与聚合 TCP 流

+

ifconfig

「我的 IP 地址是多少?」

+

route

查看和修改路由表

+

ip

用于代替 ifconfig、route 等其他命令

+

arp

查看你的 ARP 表

+

mitmproxy

具有 SSL/TLS 功能的交互式拦截侦听代理

+
+

MITM 是 Man-in-the-middle 的缩写。

+
+

nmap

网络连接端扫描软件

+

zenmap

nmap 的 GUI 版本

+

p0f

被动网络指纹识别工具

+

openvpn

VPN 软件

+

wireguard

新的 VPN 软件

+

nc

Netcat,手动建立 TCP 连接

+

socat

Netcat 的加强版,主要特点是在两个数据流之间建立通道

+

telnet

类似于 ssh,但不安全

+

ftp / sftp

用于文件拷贝,sftp 是基于 ssh 的。

+

netstat / ss / lsof / fuser

「服务器的哪些端口号被占用了?」

+

iptables

配置防火墙和 NAT

+

nftables

新版 iptables

+

hping3

TCP/IP 数据包组装/分析工具

+

traceroute / mtr

「数据包到达服务器的路径是什么?」

+

tcptraceroute

使用 TCP 包代替 ICMP 包的 traceroute 命令

+
+

现代网络广泛使用防火墙,导致传统路由跟踪工具发出的(ICMP应答(ICMP echo)或UDP)数据包都被过滤掉了,所以无法进行完整的路由跟踪。尽管如此,许多情况下,防火墙会准许TCP数据包通过防火墙到达指定端口,这些端口是主机内防火墙背后的一些程序和外界连接用的。通过发送TCP SYN数据包来代替UDP或者ICMP应答数据包,tcptraceroute可以穿透大多数防火墙。

+
+

ethtool

管理物理以太网连接和网卡

+

iw / iwconfig

管理无线网络设备的配置工具

+

sysctl

配置 Linux 内核的网络栈

+

openssl

用 SSL 证书做任何事

+

stunnel

为不安全的服务器做一个SSL代理

+

iptraf / nethogs / iftop / ntop

查看什么在占用带宽

+

ab / nload / iperf

基准测试工具

+

python -m SimpleHTTPServer

搭建当前目录下的文件服务器

+

ipcalc

IP 地址计算器,比如查看 13.21.2.3/15 是什么意思

+
1
2
3
4
5
6
7
8
9
10
11
~ ➜ ipcalc 13.21.2.3/15

Address: 13.21.2.3 00001101.0001010 1.00000010.00000011
Netmask: 255.254.0.0 = 15 11111111.1111111 0.00000000.00000000
Wildcard: 0.1.255.255 00000000.0000000 1.11111111.11111111
=>
Network: 13.20.0.0/15 00001101.0001010 0.00000000.00000000
HostMin: 13.20.0.1 00001101.0001010 0.00000000.00000001
HostMax: 13.21.255.254 00001101.0001010 1.11111111.11111110
Broadcast: 13.21.255.255 00001101.0001010 1.11111111.11111111
Hosts/Net: 131070 Class A
+

nsenter

进入一个容器进程的网络命名空间

+ +
+ + + + + + + +
+ + + + + + +
+ +
+ + + +
+ + + + + + + + +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2022/list-or-pleasure/index.html b/2022/list-or-pleasure/index.html new file mode 100644 index 0000000000..f7012f99c0 --- /dev/null +++ b/2022/list-or-pleasure/index.html @@ -0,0 +1,490 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 清单体与愉悦感的巧合 | 贾攀的流水账 + + + + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

贾攀的流水账

+ +
+

Panmax's Blog

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+ + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ 清单体与愉悦感的巧合 +

+ + +
+ + + + +
+ + +

今天在读《掌控习惯》和听得到拆解《清单革命》这本书的时候,发现这两本书使用了不同的视角来解释同一个现象,而且最终将达成效果的原因各自归功于自己要介绍的方案,有点公说公有理婆说婆有理的感觉,很有意思(也很巧合,同一天读到同一个case)。

+

这两本书都介绍了巴基斯坦卡拉奇这里的平民窟,由于卫生条件差导致死亡率高的问题,解决方法是培养那里的人使用香皂的习惯。

+

《掌控习惯》认为能培养起他们习惯的原因是给他们使用的香皂是「舒肤佳」这种高品质香皂,因为使用起来会产生大量泡沫、洗完手后有香味,给使用者带来极大的愉悦感,所以人们就逐渐养成了使用香皂的习惯。

+

而在《清单革命》这本书中,作者认为是专家给什么时候使用肥皂列了个包含6条内容的清单,人们只要照着做就可以了,由于清单体的有效性所以培养起了人们使用肥皂的习惯。

+

我觉得《清单革命》介绍的方法和《掌控习惯》的第三个原则:「让它简便易行」也是一个意思。

+ +
+ + + + + + + +
+ + + + + + +
+ +
+ + + +
+ + + + + + + + +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2022/load-balancing/20220208183959.png b/2022/load-balancing/20220208183959.png new file mode 100644 index 0000000000..24de2e36cc Binary files /dev/null and b/2022/load-balancing/20220208183959.png differ diff --git a/2022/load-balancing/20220208185001.png b/2022/load-balancing/20220208185001.png new file mode 100644 index 0000000000..de6ba2de52 Binary files /dev/null and b/2022/load-balancing/20220208185001.png differ diff --git a/2022/load-balancing/20220208185159.png b/2022/load-balancing/20220208185159.png new file mode 100644 index 0000000000..b3bf2d40d8 Binary files /dev/null and b/2022/load-balancing/20220208185159.png differ diff --git a/2022/load-balancing/20220208191529.png b/2022/load-balancing/20220208191529.png new file mode 100644 index 0000000000..b63b25315a Binary files /dev/null and b/2022/load-balancing/20220208191529.png differ diff --git a/2022/load-balancing/index.html b/2022/load-balancing/index.html new file mode 100644 index 0000000000..c10b8404fa --- /dev/null +++ b/2022/load-balancing/index.html @@ -0,0 +1,549 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 负载均衡方案介绍 | 贾攀的流水账 + + + + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

贾攀的流水账

+ +
+

Panmax's Blog

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+ + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ 负载均衡方案介绍 +

+ + +
+ + + + +
+ + +

本文只讨论请求进入数据中心后的负载均衡方案,DNS 负载均衡不在讨论范围内。

+

负载均衡(Load Balancing)定义:调度后方的多台机器,以统一的接口对外提供服务,承担此职责的技术组件。

+

总体来说负载均衡只有两种:

+
    +
  • 四层负载均衡
  • +
  • 七层负载均衡
  • +
+

四层负载均衡的优势是性能高,七层负载均衡的优势是功能强。

+

“四层”的来历:“四层负载均衡”其实是多种均衡器工作模式的统称,“四层”的意思是说这些工作模式的共同特点是维持着同一个 TCP 连接,而不是说它只工作在第四层,如:

+
    +
  • 通过改写 MAC 实现的负载均衡(又叫数据链路层负载)工作在二层
  • +
  • 通过改写 IP 实现的负载均衡(又叫网络层负载均衡)工作在三层
  • +
+

出于习惯和方便,现在几乎所有的资料都把它们统称为四层负载均衡。

+

如果在某些资料上看见“二层负载均衡”、“三层负载均衡”的表述,描述就是它们工作的层次。

+

对于一些大的网站,一般会采用 DNS+四层负载+七层负载的方式进行多层次负载均衡。

+

四层负载均衡

数据链路层负载均衡

数据链路层负载均衡所做的工作,是修改请求的数据帧中的 MAC 目标地址,让用户原本是发送给负载均衡器的请求的数据帧,被二层交换机根据新的 MAC 目标地址转发到服务器集群中对应的服务器的网卡上,这样真实服务器就获得了一个原本目标并不是发送给它的数据帧。

+

负载均衡服务器和集群内的真实服务器配置相同的虚拟 IP 地址(Virtual IP Address,VIP),也就是说,在网络通信的 IP 层面,负载均衡服务器变更 MAC 地址的操作是透明的,不影响 TCP/IP 的通信连接。所以真实的搜索服务器处理完搜索请求,发送应答响应的时候,就会直接发送回请求的客户端,不会再经过负载均衡服务器,避免负载均衡器网卡带宽成为瓶颈,因此数据链路层的负载均衡效率是相当高的。

+

Pasted image 20220208191529.png

+

只有请求经过负载均衡器,而服务的响应无须从负载均衡器原路返回的工作模式,整个请求、转发、响应的链路形成一个“三角关系”,所以这种负载均衡模式也常被很形象地称为 “三角传输模式”(Direct Server Return,DSR),也有叫“单臂模式”(Single Legged Mode)或者“直接路由”(Direct Routing)。

+

二层负载均衡器直接改写目标 MAC 地址的工作原理决定了它与真实的服务器的通信必须是二层可达的,通俗地说就是必须位于同一个子网当中,无法跨 VLAN。

+

数据链路层负载均衡最适合用来做数据中心的第一级均衡设备,用来连接其他的下级负载均衡器。

+

网络层负载均衡

我们可以沿用与二层改写 MAC 地址相似的思路,通过改变数据包里面的 IP 地址来实现数据包的转发。

+

有两种常见的修改方式。

+

IP 隧道

保持原来的数据包不变,新创建一个数据包,把原来数据包的 Headers 和 Payload 整体作为另一个新的数据包的 Payload,在这个新数据包的 Headers 中写入真实服务器的 IP 作为目标地址,然后把它发送出去。

+

设计者给这种“套娃式”的传输起名叫做“IP 隧道”(IP Tunnel)传输。

+

IP 隧道的转发模式仍然具备三角传输的特性,即负载均衡器转发来的请求,可以由真实服务器去直接应答,无须在经过均衡器原路返回。

+

IP 隧道工作在网络层,所以可以跨越 VLAN,因此摆脱了直接路由模式中网络侧的约束。

+

Pasted image 20220208185001.png
IP 隧道的缺点:

+
    +
  1. 要求真实服务器必须支持“IP 隧道协议)”(IP Encapsulation),就是它得学会自己拆包扔掉一层 Headers(现在几乎所有的 Linux 系统都支持 IP 隧道协议)。
  2. +
  3. 这种模式仍必须通过专门的配置,必须保证所有的真实服务器与均衡器有着相同的虚拟 IP 地址,因为回复该数据包时,需要使用这个虚拟 IP 作为响应数据包的源地址,这样客户端收到这个数据包时才能正确解析。
  4. +
+

NAT

NAT(Network Address Translation) 模式通过改变目标数据包:直接把数据包 Headers 中的目标地址改掉,修改后原本由用户发给均衡器的数据包,也会被三层交换机转发送到真实服务器的网卡上。

+

NAT 模式需要让应答流量先回到负载均衡,由负载均衡把应答包的源 IP 改回自己的 IP,再发给客户端,这样才能保证客户端与真实服务器之间的正常通信。在流量压力比较大的时候,NAT 模式的负载均衡会带来较大的性能损失,比起直接路由和 IP 隧道模式,甚至会出现数量级上的下降,此时整个系统的瓶颈很容易就出现在负载均衡器上。

+

Pasted image 20220208185159.png

+

七层负载均衡

应用层负载均衡

工作在四层之后的负载均衡模式就无法再进行转发了,只能进行代理,此时真实服务器、负载均衡器、客户端三者之间由两条独立的 TCP 通道来维持通信。

+

Pasted image 20220208183959.png
我们先对代理做个简单介绍,根据“哪一方能感知到”的原则,可以分为“正向代理”、“反向代理”和“透明代理”三类。

+
    +
  • 正向代理就是我们通常简称的代理,指在客户端设置的、代表客户端与服务器通信的代理服务,它是客户端可知,而对服务器透明的。
  • +
  • 反向代理是指在设置在服务器这一侧,代表真实服务器来与客户端通信的代理服务,此时它对客户端来说是透明的。
  • +
  • 透明代理是指对双方都透明的,配置在网络中间设备上的代理服务,譬如,架设在路由器上的透明翻墙代理。
  • +
+

七层负载均衡器它就属于反向代理中的一种。

+

言归正传,七层均衡器工作在应用层,可以感知应用层通信的具体内容,往往能够做出更明智的决策,玩出更多的花样来。

+

列举了一些七层代理可以实现的功能:

+
    +
  • CDN 可以做的缓存方面的工作,如:静态资源缓存、协议升级、安全防护、访问控制
  • +
  • 智能路由
  • +
  • 抵御安全攻击
  • +
  • 微服务链路治理
  • +
+

参考:

+ + +
+ + + + + + + +
+ + + + + + +
+ +
+ + + +
+ + + + + + + + +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2022/lose-sleep-in-2022/index.html b/2022/lose-sleep-in-2022/index.html new file mode 100644 index 0000000000..f3c22d72f0 --- /dev/null +++ b/2022/lose-sleep-in-2022/index.html @@ -0,0 +1,508 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 2022年第一次彻夜失眠 | 贾攀的流水账 + + + + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

贾攀的流水账

+ +
+

Panmax's Blog

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+ + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ 2022年第一次彻夜失眠 +

+ + +
+ + + + +
+ + +

现在时间是2022年02月05日凌晨5点10分,春节假期的倒数第二天,我经历了新年的第一次彻夜失眠。2点半的时候吃了一粒安眠药但到现在还是没睡着,索性就不睡了。

+

我基本上每周会有一次小失眠(差不多睡3、4小时),每几个月有一次大失眠。

+

我的失眠好像有规律,又好像没有过滤,昨晚睡得确实比较晚,差不多0点才上床,躺下后感觉心口疼,一直辗转反侧到现在。

+

上床晚的一个原因是晚上的时候发现了一个有趣的功能,QSpace 可以连接各个网盘,我把每个网盘进行了授权,并尝试了一下文件上传和下载。

+

另一个原因是白天的时候打开了几个页面,给自己定下了学习目标准备今天学完,但是由于拖延只进行了一半,到了晚上有些焦虑。

+

说到失眠有规律,是因为有一部分时候的失眠是因为白天喝了咖啡,但我自己总觉得跟喝咖啡关系不大,因为我几乎不会在下午喝咖啡。

+

昨天上午我喝了一个大杯美食,下午又喝了两杯啤酒,我自己本身有酒精过敏,但是为了消遣没事又想和两口🤷🏻‍♂️

+

前段时间看到一个人提到的「妈妈法则」,我也准备给自己制定几项,虽然有些是我暗自里回尽量去做的,但终究没有落到纸面上,这次索性写下来,起到监督自己的目的:

+
+

妈妈法则:你长大成人,背井离乡,没人再盯着你刷牙,洗衣服,完成作业,所以你需要成为自己的妈妈,制定一些规则,和自己约法三章。
它们是一些简单,有效的规则,让你处理好自己的生活,健康,甚至情绪。

+
+
    +
  • 晚上10:30前上床
  • +
  • 绝不再刷短视频(快手、抖音)
  • +
  • 每日游戏时间绝不超过30分钟
  • +
  • 绝不带手机进卧室
  • +
  • 少喝咖啡,并且绝不在午后喝咖啡
  • +
  • 绝不再沾酒
  • +
  • 每周至少一次5公里慢跑
  • +
  • 每天至少2小时阅读
  • +
+

今年也没做什么总结,生活上平淡无奇,平日里读了40多本书,公司里给了个优秀员工。

+

2022年,我想把 Rust 学一学,参与一些高质量、大型开源项目,多一些输出,做一项 Side Project,加入一个纯技术公司(也许是明年)。

+ +
+ + + + + + + +
+ + + + + + +
+ +
+ + + +
+ + + + + + + + +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2022/marry-with-hongloumeng/20220704125543.png b/2022/marry-with-hongloumeng/20220704125543.png new file mode 100644 index 0000000000..0b844b08c1 Binary files /dev/null and b/2022/marry-with-hongloumeng/20220704125543.png differ diff --git a/2022/marry-with-hongloumeng/index.html b/2022/marry-with-hongloumeng/index.html new file mode 100644 index 0000000000..b4b50e8d82 --- /dev/null +++ b/2022/marry-with-hongloumeng/index.html @@ -0,0 +1,535 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 如果我可以娶红楼梦中的一位女子 | 贾攀的流水账 + + + + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

贾攀的流水账

+ +
+

Panmax's Blog

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+ + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ 如果我可以娶红楼梦中的一位女子 +

+ + +
+ + + + +
+ + +

今天脑补一下,如果我能够娶红楼梦中一位女子为妻,我会选谁。用正排的方法有点困难,尝试用排除法逐个过滤掉我无法接受性格的人物。另外,红楼梦中所有女子范围太大,我从中选取一些有鲜明性格的人出来。

+

20220704125543.png

+

林黛玉

虽然本书中宝玉最钟爱的是黛玉,二人从小青梅竹马,互相将对方视为 soulmate,但我是无法接受黛玉这种性格的,她孤傲、矫情,总爱使小性子,喜欢说「暗语」不把话说明白。而且书中对黛玉外貌的描写给我的印象是娇瘦,该丰满的地方不够丰满。唔,你懂的。

+
+

态生两靥之愁,娇袭一身之病。泪光点点,娇喘微微。闲静时如娇花照水,行动处似弱柳扶风,心较比干多一窍,病如西子胜三分。

+
+

黛玉饭量很小,对吃没太大兴趣,如果我和一个妹子约饭,还没吃几口对方就说吃饱了,或者对方一开始就说今天不饿,你点你的,我会非常恼火,不能成为饭搭子的另一半我是不能接受的。

+

当然,人家黛玉是绛珠仙草转世,我一凡人也配不上她。

+

王熙凤

王熙凤性格火辣,贾母给她起了个外号「泼皮破落户儿」, 但她性格上太要强了,心机很重,小厮兴儿对她的描述是:「嘴甜心苦,两面三刀」,「上头笑着,脚底下使绊子」,「明是一盆火,暗是一把刀」,可见王熙凤手段还很毒辣。

+

凤姐这样的女人需要一位性格也很强势的夫君来配才能驾驭得住,书中懦弱的贾琏在王熙凤面前时服服帖帖,王熙凤实时监视着他的行踪,不给他半点接触其他女人的机会,连自己的陪嫁丫鬟也不可以(在古代陪嫁丫鬟默认是对方的妾),但是只要王熙凤不在跟前贾琏就抓紧时间从外边拉个女人到自己房中,贾琏的主要目的就是为了发泄欲望,想在其他女人面前获得「一家之主」的体验,在尤二姐那几回更是如此。

+

尤二姐的下场可以看出,王熙凤还是个嫉妒心非常重的人。外表贤良、内心恶毒,这种女人太恐怖了。

+

薛宝钗

书中对宝钗外貌描写和黛玉形成了鲜明对比,一个丰满、一个瘦弱。

+
+

可巧宝钗左腕上笼着一串,见宝玉问她,少不得褪了下来。宝钗生得肌肤丰泽,容易褪不下来。宝玉在旁看着雪白一段酥臂,不觉动了羡慕之心……再看看宝钗形容,只见脸若银盆,眼似水杏,唇不点而红,眉不画而翠,比黛玉另具一种妩媚风流,不觉就呆了。

+
+

宝钗的心理年龄应该比园子里其他姐妹大不少,原因是她的父亲去世的早,而她们家又是皇商,弟弟薛蟠又不务正业,只能她和母亲担起家族的事业,很小的时候就接触到了成人「污浊」的世界。

+

宝钗太冷了,缺少小姑娘们那种灵巧活泛的劲儿,在男朋友面前不会发嗲,不会要求亲亲抱抱举高高,不爱开玩笑,和她一起生活会缺少一些生活上的乐趣。

+

贾探春

太耿直,缺少风趣。处事不够圆融、爱挑刺,有些愤世嫉俗,最后她自己也是选择远离这个到处是窟窿的家族,远嫁到他乡。

+

兴儿在尤二姐面前这样描述探春:

+
+

三姑娘的浑名是‘玫瑰花’…玫瑰花又红又香,无人不爱的,只是刺戳手。……

+
+

秦可卿

撇开可卿的真实死因不谈,在我看来她有些扶弟魔,为了促成秦钟和宝玉的见面,动用了心机。而且当她得知秦钟在学堂被人欺负后把自己气得不行。

+
+

今儿听见有人欺负了她兄弟,又是恼,又是气。恼的是那群混账狐朋狗友的扯是搬非、调三惑四的那些人;气的是她兄弟不学好,不上心念书,以致如此学里吵闹。她听了这事,今日索性连早饭也没吃。

+
+

史湘云

湘云性格豪爽,平时大大咧咧的,喜欢穿男装,跟她论兄弟一定是不错的,但是缺少一些女人味,有些过于粗犷,不精致。

+

妙玉

假清高。

+
+

欲洁何曾洁,云空未必空!可怜金玉质,终陷淖泥中。

+
+

袭人

袭人给人一种大姐姐的感觉,不需要被保护,但是男生恰恰容易喜欢上自己想保护的女生。但也不得不说袭人是个非常合适的结婚对象,勤家持家、为人和善,处事方面尽量大事化小、小事化了,作者在判词中也写到娶到她的人是有福的。

+
+

堪羡优伶有福,谁知公子无缘。

+
+

作者还有意将袭人映衬为宝钗,将晴雯影射为黛玉,因为上边说过黛玉了,下边不再对晴雯进行赘述。

+
+

王夫人眼中的晴雯:水蛇腰,削肩膀,眉眼有点像林黛玉。

+
+

香菱

实话实说,香菱是我喜欢的类型,虽遭遇了各种不幸,仍然那么天真无邪。她的脾气好,模样也好,作者从头到尾都在透露对香菱的怜悯。我喜欢香菱也可能有可能出于同情,被拐卖后到薛蟠这个不懂得怜香惜玉之人身边做妾,薛蟠娶夏金桂前,香菱还高兴地东跑西跑地帮忙,从这一点看出,香菱是那种真心想让别人好的人。夏金桂过门后香菱被百般欺凌,薛蟠也不分青红皂白地打她,香菱只能忍气吞声。香菱的结局有很多说法,我们这里不展开讨论。

+

香菱嫁给薛蟠后我内心也是愤愤不已,好歹贾琏替我骂了他:「方才我见姨妈去,不妨和一个年轻的小媳妇撞了个对面,生得好齐整模样……谁知就是上京来买的那小丫头,名叫香菱的,竟与薛大傻子做了房里人,开了脸,越发出挑的标致了,那薛大傻子真玷辱了她。」

+

从香菱学诗可以看出,香菱极其聪明好学,她很羡慕那些可以读书的女孩子,不管自己处境多么糟糕也要想办法去学习,吾辈之楷模。

+

脂砚斋对香菱的评语极高,集其他人的优点于一身。

+
+

细想香菱之为人也,根基不让迎探,容貌不让凤秦,端雅不让纨钗,风流不让湘黛,贤惠不让袭平…

+
+

所以,如果让我选一个红楼梦中的女子作为妻子的话,我想我会选择香菱。

+ +
+ + + + + + + +
+ + + + + + +
+ +
+ + + +
+ + + + + + + + +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2022/miss-my-child/20220723125509.png b/2022/miss-my-child/20220723125509.png new file mode 100644 index 0000000000..9fb0bab22b Binary files /dev/null and b/2022/miss-my-child/20220723125509.png differ diff --git a/2022/miss-my-child/20220723125519.png b/2022/miss-my-child/20220723125519.png new file mode 100644 index 0000000000..85f331eea1 Binary files /dev/null and b/2022/miss-my-child/20220723125519.png differ diff --git a/2022/miss-my-child/index.html b/2022/miss-my-child/index.html new file mode 100644 index 0000000000..2c7107df9d --- /dev/null +++ b/2022/miss-my-child/index.html @@ -0,0 +1,532 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 凌晨四点想娃了 | 贾攀的流水账 + + + + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

贾攀的流水账

+ +
+

Panmax's Blog

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+ + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ 凌晨四点想娃了 +

+ + +
+ + + + +
+ + +

现在时间是北京时间 2022 年 07 月 23 日凌晨 4 点

+

我没有失眠,而是被一个噩梦惊醒了

+

梦的具体内容记不清了,最后给我的启发是珍惜眼前人

+

这让我突然特别想我的一念了

+
+

我们一家蜗居在一个不到 70 平的小房子里,平时我爸妈也都在

+

我爸喜欢出去玩,而且不喜欢在北京挤着,所以经常离京

+

他在今年年初的时候离京,后来疫情严重就一直没回来

+

直到两周前疫情好转,他回京把我妈和一念一起接回了老家

+
+

一念在北京的时候我也只能周末的时候陪陪她

+

因为我的睡眠不好,加上一些心理障碍晚上经常不和她们睡在一起

+

而是去次卧的上铺自己睡或者在客厅把沙发铺开了睡

+

但是我喜欢把一念搂在怀里或者她躺在我胳膊上的感觉

+
+

现在家里公司远了

+

如果公司到地铁的路上运气好能骑到车的话,大概要 1 小时 40 分才能到家

+

比之前在国贸附近足足多了 1 个小时

+

撇开路程,打车时间也从 9 点延后到了 9 点 30

+

而换来的只是公司给我们每个月多发的 600 元交通补助

+

还有之前的一次性 3000 元搬家补贴

+

按照当前的最佳情况 8 点下班,那也要 9 点 40 才能到家

+

之前的最佳情况是 7 点,现在的情况在以肉眼可见的速度恶化

+

每天的通勤时间 3 个多小时

+

就好像我已经成了一个差生,即便再怎么努力也改变不了现状

+
+

陈映真小说《上班族的一日》中有这么一句话:“上班,几乎没有人知道,上班,是一个大大的骗局。一点点可笑的生活的保障感,折杀多少才人志士啊。”

+

由于国内现阶段的种种问题,我们的努力往往未必能得到回报

+

这种不确定性会让人觉得看不到希望,幸福感自然不会高

+
+

「幸福生活才是目的,个人的成功不过是实现这个目的的途径和手段而已」

+

我一直在追求个人的成功,很少去想目的

+

可是我现在个人也不成功,相对幸福的生活也没有

+

懦弱而又无力的自己

+
+

+

照片拍摄于年 5 月,一念在认真的给我扎小辫。

+

一念和黛玉一样容易咳嗽,所以一般很少让她吃凉东西

+

作为我俩的小秘密,也是为了贿赂她

+

每次我带她出去玩都会和她一起吃冰淇淋

+

+
+

一日不见,甚是想念

+
+ +
+ + + + + + + +
+ + + + + + +
+ +
+ + + +
+ + + + + + + + +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2022/modify-blog-font/1.png b/2022/modify-blog-font/1.png new file mode 100644 index 0000000000..71d98a6c12 Binary files /dev/null and b/2022/modify-blog-font/1.png differ diff --git a/2022/modify-blog-font/2.jpg b/2022/modify-blog-font/2.jpg new file mode 100644 index 0000000000..7a64d10bad Binary files /dev/null and b/2022/modify-blog-font/2.jpg differ diff --git a/2022/modify-blog-font/index.html b/2022/modify-blog-font/index.html new file mode 100644 index 0000000000..e8f738de98 --- /dev/null +++ b/2022/modify-blog-font/index.html @@ -0,0 +1,505 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 将博客字体修改为「霞鹜文楷」 | 贾攀的流水账 + + + + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

贾攀的流水账

+ +
+

Panmax's Blog

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+ + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ 将博客字体修改为「霞鹜文楷」 +

+ + +
+ + + + +
+ + +

wenkai-1.png

+

之前访问过我博客的人可能会发现我博客的字体变了,这款字体是我前段时间在 Twitter 上看到一个很喜欢的博主推荐的,是一款开源字体,名字叫「霞鹜文楷」,非常适合作中文展示,阅读起来使人感受愉悦。

+

+

我也将这款字体改为了我 Drafts 上的默认字体,试用了几天感受确实不错,所以想作为博客字体来使用。因为官方仓库的下载链接只提供了 ttf 格式,我不知道如何应用在 Web 上,所以搁置了几天,今天想再次折腾下看,再次阅读官方文档看到注意事项中写着:

+

+
+

正应了我前两天说的:我想的事情其他人已经想到了

+
+

我打开那个 Issue 提供的另一个项目地址,看到里边提供了好几种安装、使用方式。因为我是在其他人模板的基础上进行修改,所以准备用引入现成 CDN 的方式来做字体修改。

+

我的博客当前使用的是 Next 主题,使用其他主题的也可参考这个方式。

+

编辑博客根目录下的 themes/next/layout/_layout.swig,在 head 中插入如下代码:

+
1
2
3
4
5
6
7
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/lxgw-wenkai-lite-webfont@1.0.0/style.css" />
<style>
body,div.post-body,h1,h2,h3,h4 {
font-family: "LXGW WenKai LITE", sans-serif;
font-size: 108%;
}
</style>
+

我对 CSS 不太精通,刚开始只在 style 中填入了 body,发现有些正文部分没有生效,我觉得应该是优先级问题:博客的正文指定了其它字体所以将我这里的设置进行了覆盖。

+

我通过 Chrome 的元素查找定位到了 div 下 的 class="post-body" 为正文部分的筛选器,于是加上了 div.post-body,接下来后发现 h1、h2 这些格式也没生效,于是有逐个进行了添加。这样基本上所有地方都能生效了,有两处我故意没做处理,一处是左上角博客标题另一处是文章标题下方的 meta 区域,这两块我觉得可以保留更正式一些的字体。

+

这个字体有些偏小,于是我还将字号做了稍许放大,也就是 style 中的 font-size: 108%

+

如果你也喜欢这款字体,不妨参考这篇文章也自己尝试修改一下。另外如果你知道如何只在 style 中指定 body 就可以让全局字体生效,请留言告诉我。

+ +
+ + + + + + + +
+ + + + + + +
+ +
+ + + +
+ + + + + + + + +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2022/modify-blog-font/wenkai-1.png b/2022/modify-blog-font/wenkai-1.png new file mode 100644 index 0000000000..e4b87a57b5 Binary files /dev/null and b/2022/modify-blog-font/wenkai-1.png differ diff --git a/2022/must-know-this-thing-to-practice/001_WC-EditVideo_1.mp4 b/2022/must-know-this-thing-to-practice/001_WC-EditVideo_1.mp4 new file mode 100644 index 0000000000..9b7f62614b Binary files /dev/null and b/2022/must-know-this-thing-to-practice/001_WC-EditVideo_1.mp4 differ diff --git a/2022/must-know-this-thing-to-practice/20220717213743.png b/2022/must-know-this-thing-to-practice/20220717213743.png new file mode 100644 index 0000000000..1bd5dc57a5 Binary files /dev/null and b/2022/must-know-this-thing-to-practice/20220717213743.png differ diff --git a/2022/must-know-this-thing-to-practice/20220717214159.png b/2022/must-know-this-thing-to-practice/20220717214159.png new file mode 100644 index 0000000000..2137aa56ac Binary files /dev/null and b/2022/must-know-this-thing-to-practice/20220717214159.png differ diff --git a/2022/must-know-this-thing-to-practice/20220717215311.png b/2022/must-know-this-thing-to-practice/20220717215311.png new file mode 100644 index 0000000000..c066b6e9b1 Binary files /dev/null and b/2022/must-know-this-thing-to-practice/20220717215311.png differ diff --git a/2022/must-know-this-thing-to-practice/20220717215324.png b/2022/must-know-this-thing-to-practice/20220717215324.png new file mode 100644 index 0000000000..bfd114f05d Binary files /dev/null and b/2022/must-know-this-thing-to-practice/20220717215324.png differ diff --git a/2022/must-know-this-thing-to-practice/20220717215336.png b/2022/must-know-this-thing-to-practice/20220717215336.png new file mode 100644 index 0000000000..8cfc99df8b Binary files /dev/null and b/2022/must-know-this-thing-to-practice/20220717215336.png differ diff --git a/2022/must-know-this-thing-to-practice/20220717220555.png b/2022/must-know-this-thing-to-practice/20220717220555.png new file mode 100644 index 0000000000..cec553e73d Binary files /dev/null and b/2022/must-know-this-thing-to-practice/20220717220555.png differ diff --git a/2022/must-know-this-thing-to-practice/index.html b/2022/must-know-this-thing-to-practice/index.html new file mode 100644 index 0000000000..668aa77f5d --- /dev/null +++ b/2022/must-know-this-thing-to-practice/index.html @@ -0,0 +1,514 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 绝知此事要躬行 | 贾攀的流水账 + + + + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

贾攀的流水账

+ +
+

Panmax's Blog

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+ + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ 绝知此事要躬行 +

+ + +
+ + + + +
+ + +

最近一个月解锁了两个新技能,一个是在几周前团建的时候学会了德州扑克,另一个是今天利用两个小时入门了陆冲板。虽然这两个目前还都是新手级别,但是比起之前只是看一些介绍、教程,通过实践来学习进步的速度可快太多了。

+

德州扑克

+

上上周的周五下午小组团建,吃了个西餐之后去了一个轰趴馆,有打台球的,有打麻将的。一开始我无所事事,后来有同事叫我打德州,因为我只听说过但从来没玩过,所以一开始说自己不会,就不参与了。后来在几个同事的鼓励下坐在了牌桌前,把规则给我讲了下,并且发给我一张图片,告诉我按照图里的规则判断自己手牌的大小就可以了。

+

+

我把图片保存到手机上,时不时看一下自己的牌有没有和这些规则对应上。因为之前玩过炸金花,所以不一会就了解规则了,但是各种套路和黑话还是不太懂,包括什么时候可以看牌什么时候可以跳过也需要问下其他人,有时候还需要让其他人帮忙算一下钱之类的。

+

这次学习德州扑克是实打实的用钱学习的,大家初金都是 200 块钱,没想到我在新手光环的照耀下不仅没有输钱,最后还赚了 150 多。

+

陆地冲浪板

因为从家到地铁站、下地铁后再到公司,这两段路程都比较远,从家到地铁可以骑自己的自行车,但是公司那边地铁站下车后通常骑不上车,所以就萌生了用滑板当代步工具的想法。

+

之前网上查了一些资料,一心想学习双翘,找了个家附近的滑板俱乐部,预约了今天的体验课,老师问我的学习目的,我说简单、能代步就可以。教练给我推荐陆地冲浪板(简称陆冲)。对于我不了解的领域,我是很相信专业和权威的,所以听了教练的话体验了一节陆冲课。

+

陆冲适合平时代步,它的轮子比较大,更适合在马路上使用,而且相对来说比较容易入门,学会后还能体验到冲浪的乐趣(这也是它名字的由来)。

+

一节课体验课结束后,感觉很有意思,而且这东西确实看起来容易,到自己滑的时候相当困难,加上陆冲板还可以来回扭动,刚开始上板平衡都很难掌握。为了更深入地学习,我报了 10 节一对一课程,一节 399,还买一块属于自己的板子。板子的话我也一步到位,买的应该是陆冲板中最好的牌子 Caver,虽然价格并不是最高的,但这块板子颜值深得我意,如下图:

+



+

这块板长度是 30.5 寸,大概是 6 斤多重,比起其他款式稍微宽一点,所以看起来很大气,售价 2200 软妹币。我还差一套护具没买,训练的时候用的店里的,准备自己从网上买一套,加上头盔大概又是 700 左右的花销。好不容易能有个爱好,该省省该花花吧。

+

肯定有朋友会说我这钱花的不值,陆冲板这么简单靠自学就够了,但我的想法是随着年龄越来越大,我们应该尽可能用更高的效率去学习,不能再花太多时间自己琢磨了,请个教练可以少走弯路,自己跟着视频练习的话根本不知道哪个姿势不对、哪里应该注意,也不知道应该加强练习哪些内容,而且容易受伤。这和软件开发时常用的空间换时间一个道理,我这算是拿钱换时间了,我几年前学习游泳也是报了 10 节私教课,学完后可以用基本标准的姿势蛙泳了,虽然很久没有游泳了,但那些注意事项我还是记得,再游的话很快就能找到感觉。

+

10 节课周六日各上一节,估计一个多月就能学完,再加上平时的练习,到时候我应该就能达到刷街的级别了。

+

作为报课的优惠,教练赠送了我一节课,所以买完板后我又上了正式的第一节课,今天一共学了两个小时,最后,再欣赏一下我的滑板初体验的视频吧。

+ + + +

这个视频是教练一手打着绷带,另一手拿着手机,脚下踩着滑板拍出来的,完整视频比较长我剪了其中十来秒出来,可以看出镜头运的很好。我准备最后一节课让教练帮我拍一组酷酷的视频作为结果汇报的材料。

+

P.S. 昨天去检查甲功,今天结果出来指标正常了,喝个酒庆祝下,干杯🍻

+

这个泰山原浆啤酒很好喝,口感非常好,而且很鲜,强烈推荐。

+ +
+ + + + + + + +
+ + + + + + +
+ +
+ + + +
+ + + + + + + + +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2022/my-app-seven-deadly-sins/index.html b/2022/my-app-seven-deadly-sins/index.html new file mode 100644 index 0000000000..bb043f9ae8 --- /dev/null +++ b/2022/my-app-seven-deadly-sins/index.html @@ -0,0 +1,495 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 我在使用软件时的七宗罪 | 贾攀的流水账 + + + + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

贾攀的流水账

+ +
+

Panmax's Blog

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+ + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ 我在使用软件时的七宗罪 +

+ + +
+ + + + +
+ + +

我打开微信的「看一看」,是想了解最近疫情发展情况,盘算下是不是又可以居家办公了,总幻想着世界可以毁灭;打开「朋友圈」是想看看在我无聊时其他人都在做什么、那些土豪们又去哪里潇洒了;我发朋友圈是想炫耀些什么。(怠惰、愤怒、嫉妒、傲慢)

+

插个题外话,这几天被「天堂超市酒吧」事件搞的又来了一波疫情反扑,严重程度不亚于上一波,但是政府已经不再提居家办公的事情了,看来国家已经意识到了恢复经济成了现阶段的重中之重的任务,经济这个烂摊子已经到了不可不救的地步了,实际也早该这样了,别再打肿脸充胖子。

+

我打开 Twitter 是想看一看真实世界的样子、技术界有哪些新的轮子,有时候会有这些见闻作为自己的谈资,还想看看有没有什么赚钱的路子。(傲慢、贪婪)

+

我打开 Youtube 为了看看那几个常看的国内美食 up 主最近出了什么新作品(我不用 bilibili),想看看国外 up 主对国内的一些事件发表了哪些(我们敢怒而不敢言)看法,偶尔也会看看我在私人目录里上传的一些(小)电影。(暴食、愤怒、色欲)

+

我打开 Telegram短信是因为总幻想着有人会通过这种方式联系我,有时候回去 Telegram 一些 Porn 的群组逛逛。(傲慢、色欲)

+

我打开脉脉是想看看职场人们现在在吐槽什么事情,在批判哪个大场,还会从那些生活在水深火热公司的人身上得到一些安抚。(嫉妒:那些在好好公司的人、愤怒、傲慢)

+

我打开小红书是为了看一些短视频杀时间,大部分视频是关于吃的,我很早之前卸载了快手,因为不想这么沉迷,因为小红书的推荐算法没这么完善,所以作为完全戒除前的过度。(怠惰、暴食、傲慢:看不起快手)

+

我打开得到知乎是因为此时此刻的焦虑,看不到方向,想在这上边找些捷径或者速成的魔法,不想慢慢提升自己。(怠惰、贪婪)

+

我打开淘宝京东是因为一时心血来潮想买那个让我心动、但可能没什么用的东西。(贪婪)

+

我打开蚂蚁财富是想看看最近又亏了多少钱,想看看国家经济究竟烂成了什么样子。(贪婪、愤怒)

+ +
+ + + + + + + +
+ + + + + + +
+ +
+ + + +
+ + + + + + + + +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2022/my-things-practice/20220601055442.png b/2022/my-things-practice/20220601055442.png new file mode 100644 index 0000000000..a05b084f0d Binary files /dev/null and b/2022/my-things-practice/20220601055442.png differ diff --git a/2022/my-things-practice/20220601055455.png b/2022/my-things-practice/20220601055455.png new file mode 100644 index 0000000000..13d6fbdc53 Binary files /dev/null and b/2022/my-things-practice/20220601055455.png differ diff --git a/2022/my-things-practice/20220601055643.png b/2022/my-things-practice/20220601055643.png new file mode 100644 index 0000000000..2017d53a66 Binary files /dev/null and b/2022/my-things-practice/20220601055643.png differ diff --git a/2022/my-things-practice/20220601055715.png b/2022/my-things-practice/20220601055715.png new file mode 100644 index 0000000000..1bcc9e546e Binary files /dev/null and b/2022/my-things-practice/20220601055715.png differ diff --git a/2022/my-things-practice/20220601055823.png b/2022/my-things-practice/20220601055823.png new file mode 100644 index 0000000000..99050312af Binary files /dev/null and b/2022/my-things-practice/20220601055823.png differ diff --git a/2022/my-things-practice/20220601060047.png b/2022/my-things-practice/20220601060047.png new file mode 100644 index 0000000000..b1de45a1cb Binary files /dev/null and b/2022/my-things-practice/20220601060047.png differ diff --git a/2022/my-things-practice/20220601060615.png b/2022/my-things-practice/20220601060615.png new file mode 100644 index 0000000000..a00ae7041e Binary files /dev/null and b/2022/my-things-practice/20220601060615.png differ diff --git a/2022/my-things-practice/20220602063734.png b/2022/my-things-practice/20220602063734.png new file mode 100644 index 0000000000..286290e81a Binary files /dev/null and b/2022/my-things-practice/20220602063734.png differ diff --git a/2022/my-things-practice/20220602065419.png b/2022/my-things-practice/20220602065419.png new file mode 100644 index 0000000000..b34c0417e2 Binary files /dev/null and b/2022/my-things-practice/20220602065419.png differ diff --git a/2022/my-things-practice/20220602070545.png b/2022/my-things-practice/20220602070545.png new file mode 100644 index 0000000000..240d87c02d Binary files /dev/null and b/2022/my-things-practice/20220602070545.png differ diff --git a/2022/my-things-practice/20220602070556.png b/2022/my-things-practice/20220602070556.png new file mode 100644 index 0000000000..5d070937d6 Binary files /dev/null and b/2022/my-things-practice/20220602070556.png differ diff --git a/2022/my-things-practice/20220602070654.png b/2022/my-things-practice/20220602070654.png new file mode 100644 index 0000000000..314a0487cd Binary files /dev/null and b/2022/my-things-practice/20220602070654.png differ diff --git a/2022/my-things-practice/20220603094351.png b/2022/my-things-practice/20220603094351.png new file mode 100644 index 0000000000..71c109ce46 Binary files /dev/null and b/2022/my-things-practice/20220603094351.png differ diff --git a/2022/my-things-practice/20220603094623.png b/2022/my-things-practice/20220603094623.png new file mode 100644 index 0000000000..b886b6aa31 Binary files /dev/null and b/2022/my-things-practice/20220603094623.png differ diff --git a/2022/my-things-practice/20220603103019.png b/2022/my-things-practice/20220603103019.png new file mode 100644 index 0000000000..4c5d765d58 Binary files /dev/null and b/2022/my-things-practice/20220603103019.png differ diff --git a/2022/my-things-practice/20220603103343.png b/2022/my-things-practice/20220603103343.png new file mode 100644 index 0000000000..e5a52d78f1 Binary files /dev/null and b/2022/my-things-practice/20220603103343.png differ diff --git a/2022/my-things-practice/20220603104835.png b/2022/my-things-practice/20220603104835.png new file mode 100644 index 0000000000..c1935ef132 Binary files /dev/null and b/2022/my-things-practice/20220603104835.png differ diff --git a/2022/my-things-practice/index.html b/2022/my-things-practice/index.html new file mode 100644 index 0000000000..d80040a2a0 --- /dev/null +++ b/2022/my-things-practice/index.html @@ -0,0 +1,588 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Panmax 的 Things 实践 | 贾攀的流水账 + + + + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

贾攀的流水账

+ +
+

Panmax's Blog

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+ + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ Panmax 的 Things 实践 +

+ + +
+ + + + +
+ + +
+

今天是端午节,祝各位端午安康。

+
+

做靠谱的人

作为一名员工,在职场中非常重要的一个品质就是要稳定输出,这很像我们监控指标中经见的 P99。

+

比如说有这样两个视频 App,一个 App 在打开视频的时候,有 99.99% 的概率会最多缓冲 3s,后面就会顺畅播放视频。另一个 App 有 80% 的概率视频一秒不卡,还有 20% 的概率每一帧都卡,卡到忧伤。如果只能选一个 App,你会选择哪个 App?我想大部分没有自虐倾向的人都会选择第一个。

+

只有长时间稳定输出,老板才放心让我们承担更多、更大的职责,同事们也更愿意和稳定靠谱的同事合作「不求有惊喜,但求无惊吓」。

+

说到这里,通过最近发生在自己身上的一件事,获得的感想是,任何寻常的跨部门合作都要认真对待,说不定日会有意想不到的结果。

+

Things3

我可以很自信的说,我绝对是一个靠谱、输出稳定的打工人,而我能做到这一点,除了一些习惯外,给我提供最大帮助的是一款叫 Things3 的任务管理工具。我现在差不多每天的工作事项都是靠 Things3 驱动,说的通俗点是靠 Todo 驱动,这有点像我们常说的 deadline 是第一生产力。

+

「君子生非异也,善假於物也。」

+
+

下文提到的 Things 皆为 Things3,为了简化称呼我将去掉 3 这个数字。

+
+

我用过很多 Todo 类的工具,比如嘀嗒清单、微软的 Todo,Sorted,最终还是留在了 Things,它的界面、交互、易用性吸引了我。

+

使用 Things 的好处是我能在每天一早就知道今天有哪些重点工作,一天中想到任何要做的事情都可以记录下来,领导安排的临时工作或者向其他人承诺的事项也可以记录下来,好记性不如烂笔头,人脑不适合存储这种临时的、用完就可以扔的记忆。

+

我使用 Things 有 4、5 年了,下面就来介绍下我是如何使用这款工具的。这里我不介绍 GTD 的方法论,只说我的实践。也不会面面俱到介绍 Things 的各种细节,你下载个 Things 跟着首次使用的入门教程走一遍就明白了。

+

以下我通过 Things 的 Mac 版本来做演示,手机上的功能完全相同,不管是哪一端体验都非常棒,我自己双端都有在高频使用,路上使用手机端、工作时使用 Mac 端,同时还在手机桌面和 Apple Watch 表盘上放了 Things 的小组件,可以在不打开 App 的时候就看到待办事项。

+

+

另外补充下,Things 的三端(Mac、iOS、iPad)都是要独立收费,手机上的价格还好,但 Mac 上的价格有些感人。

+

时间目录

+

Things 根据事项要完成的时间分了这些目录:

+
    +
  • 收件箱,临时存放或不用分类的琐碎事项;
  • +
  • 今天,时间聚焦到今天;
  • +
  • 计划,也可以理解成日程安排,有具体时间点的事项。
  • +
  • 随时,随时可抽时间完成的事项以及本周临近截至时间(Deadline)需要优先考虑的事项;
  • +
  • 某天,选择了「某天」时间标签的事项,可以用来归集不需要时间点约束的未来待办事项;
  • +
  • 日志簿(完成事项的历史记录)。
  • +
+

我自己使用的时候只会用到「今天」、「计划」、「日志簿」,其他的几乎不用。

+

新建任务到「今日」

我个人的习惯是,所有新建的任务不管分类、不管是不是一定要今天做,一律先放入「今天」,按照 GTD 的理论应该是先放收件箱。我前边提到,我是靠 Todo 驱动,也就是每天都要把今天的任务消灭掉。这里的消灭可以不必是完成,而是把任务安排到其他更合适的时间或者拆解成更小粒度的任务打散到多天完成。

+

举个例子,比如今天周一我接了一个需求,排期要周五上线,我可能因为其他事正在赶工,先在 Things 的「今天」里加上一条「周五上线 xxx 功能」。之后在我不忙的时候,再次打开 Things 就会看到我刚才记录的那件事情,这时候可以根据我的经验将这个任务拆成若干小任务,并安排到后边的时间里,比如:「完成 A 模块开发」并把它安排到周二(可以在「计划」中找到),「完成 B 模块开发」并把它安排到周三,「完成功能测试」并把它安排到周四,「xx 功能上线」并安排到周五。这样后边每一天我都有这个项目的合理进度安排,同时做每一项的时候你都可以给未来的自己留言,比如我周二周三开发完模块 AB,对应的 merge request 可以记录在周五的上线那个事项里,开发过程中修改的配置也可以记录下来,避免上线时忘记。

+

下边是一个我前段时间上线功能的截图,:

+

+

里边的 mr 地址和配置项都是在前期开发过程中记下来的,到了上线那天完全不用担心漏掉什么,也不用现去翻找我们的 mr 给其他同事 review。

+

用好循环事件

我们工作和生活中一定有很多枯燥、例行的事项要去完成,如果每个我们都靠脑子记肯定是记不过来的,这种情况下我们可以使用循环事件。

+

如下图所示:在「计划」中,事件前边带有一个循环小圆圈标记的就是循环事件,最近我们在家办公,上午下午需要使用钉钉打卡,所以我建了两个循环打卡事件,同时给每个设置了提醒时间,设置提醒时间的事项后边会带有一个小铃铛。

+

+

上边图中国年还可以看到,我有一个叫「日课」的循环项,后边有三条圆点线,表示这个事项中包含有子事项,也就是我给自己约定的每天要做的事情:

+

+

这里边的子事项我会根据近期的工作学习的测重点来进行调整,比如最近我的工作方向要偏重于信息检索相关,所以我加了一项读信息检索导论这本书。

+

我之所以没有把这几项作为独立的事项列出来,是因为不想有「红点焦虑」,前边提到我每天是靠 Todo 驱动,当看到有这么多待办项没有做时,会很焦虑,会出现为了消事项而匆忙赶工的情况。把这几项收在一个里边,也表示这几项有一定的宽容度,如果今天时间太紧张,可以只选其中的 1、2 个子项完成就行。

+

我把「多邻国学习」放到了外边而没有收入「日课」中,是因为我把这一项作为了一个必选项而不是可选项,我希望学英语这件事情不要中断。

+

提醒未来的自己

下边图中的「Apple Family 收费」和「提公积金」也是我的循环事件,循环时间不进支持每日,还支持每几日、每周几、每月几号等等。

+

比如「Apple Family 收费」我设置的是每 3 个月的 15 号提醒,我还记录了每人应收的钱,都需要找谁要钱,如果没有 Things 的帮助我肯定是记不住的。「提公积金」那项我设置了每月 20 号提醒。

+

在这张图里还能看到,我在 7 月 20 日安排了一个一次性事件,如果你在很久后有什么事情要做,一定记得要记录下来,并放入「计划」,我个人习惯是将这种很久后的事项放置在提前两三天,提前看到能给我一个预期,留个缓冲时间,毕竟很久后还需要做的事一般都不是什么小事。

+

+
+

+
+

+

因为每周三需要和组内的同事一起开周会,这之前需要写好周报,会后我需要合大家的周报,所以我给「提交周报、合周报」也建了一个每周三的循环事件,同时在里边把每个人周报的地址进行了记录:

+

+

回顾过往

职场中我们不免要写周报、月报、年度总结等等,如果自己平时有记录的习惯那还好,如果没有,到写总结的时候一定是大脑空白,仿佛自己失忆了,忘记了自己这段时间忙忙活活干了点啥。

+

如果我们用 Things 将工作事项管理起来,在需要的时候通过「日志簿」来回顾就会很方便。比如现在你问我在一月份做了些什么,我只需要翻到一月份部分,看下我当时的记录知道了。

+

这些内容有些描述过于简单,其他人也许看不懂,但因为每件事情都是我亲自做的,我看下提示就能明白这说的是什么事,由此可见我们并不需要为每个事项做特别详细的说明,能让自己能看明白就够了。人脑存储能力很强,检索能力很弱,需要借助些外力来补足检索能力。

+

+

管理我的一切

不止待办事项,实际上我用 Things 管理了我的方方面面。

+
    +
  • 待学、待读、待听、待看、待买
  • +
  • 想去的地方
  • +
  • 突发奇想的点子
  • +
  • 想写的博客内容
  • +
  • 阅读收获的金句
  • +
  • 项目中的可优化项
  • +
  • 交代给其他人的任务
  • +
  • 想要尝试的行动
  • +
  • ……
  • +
+

Things 支持将事项进行分类管理,上边那些事项我没有计划做的时间点,有些只是为了保存起来,这时候我们可以不设置时间,将它收在我们对应的分类中就可以了。

+

这里借助的是 Things 提供的创建区域和在区域中创建项目的能力。实际上 Things 的这个功能是用来做更大一些的目标管理的,不过我个人将它作为了一个分类功能来使用。

+

+

比如,我给自己分了个人和公司两大区域,每个区域中建了一些属于这个区域的分类(也就是项目):

+

+

把所有事情都记录下来还有个好处是,在我无所事事时可以看下这些事项中哪个是我现在有心情可以做的,或者读完一本书后下一本要读什么。

+

有待完善的地方

由于我会有「红点焦虑」的情况,所以想要一个在某个时间点后再出现事项的功能,有些事必须过了某个时间点才能去做。不过我觉得 Things 没有支持这个功能也有它的考虑,「今天」就应该把计划在今天做的事情都列出来,让我们可以提前规划。

+

Things 提供了一个退而求其次的方案,将晚上做的事情通过分割线放到下方,在事项上点击右键就可以操作或者使用快捷键 Command + E

+

+

+

配合日历订阅使用

我们再看一眼上边那张图,在「今天」下方显示今日天气状况和今天的节日,这是我在系统的日历中订阅了两个事件,Things 可以将系统日历中的事件列在我们自己事项的上边,就可以实现这样的效果。

+

中国节假日事件:

+

https://weather-in-calendar.com/cal/weather-cal.php?city=%E5%8C%97%E4%BA%AC%E5%B8%82&units=metric&temperature=day

+

北京天气事件:

+

https://weather-in-calendar.com/cal/weather-cal.php?city=%E5%8C%97%E4%BA%AC%E5%B8%82&units=metric&temperature=day

+

天气事件大家可以根据自己所在城市,在这个网站生成订阅链接。

+

最后

Things 还有一些功能我自己也没有深入探索过,比如标签、截止日期,大家可以自行探索。

+

通过这篇文章我梳理了一下自己使用 Things 的习惯,如果大家觉得可行、适用,可以尝试将自己的事项管理起来,做一个「靠谱」的人。

+

如果你发现文章中有观点介绍有误或者不明确的地方欢迎留言讨论。

+ +
+ + + + + + + +
+ + + + + + +
+ +
+ + + +
+ + + + + + + + +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2022/quantify-drink-water/20220622114545.png b/2022/quantify-drink-water/20220622114545.png new file mode 100644 index 0000000000..3a12a31c1e Binary files /dev/null and b/2022/quantify-drink-water/20220622114545.png differ diff --git a/2022/quantify-drink-water/20220622114618.png b/2022/quantify-drink-water/20220622114618.png new file mode 100644 index 0000000000..a2dcde0c7a Binary files /dev/null and b/2022/quantify-drink-water/20220622114618.png differ diff --git a/2022/quantify-drink-water/20220622115058.png b/2022/quantify-drink-water/20220622115058.png new file mode 100644 index 0000000000..3cb1eff69e Binary files /dev/null and b/2022/quantify-drink-water/20220622115058.png differ diff --git a/2022/quantify-drink-water/20220622115242.png b/2022/quantify-drink-water/20220622115242.png new file mode 100644 index 0000000000..f2037a88be Binary files /dev/null and b/2022/quantify-drink-water/20220622115242.png differ diff --git a/2022/quantify-drink-water/index.html b/2022/quantify-drink-water/index.html new file mode 100644 index 0000000000..27b0734908 --- /dev/null +++ b/2022/quantify-drink-water/index.html @@ -0,0 +1,501 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 量化喝水量 | 贾攀的流水账 + + + + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

贾攀的流水账

+ +
+

Panmax's Blog

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+ + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ 量化喝水量 +

+ + +
+ + + + +
+ + +

我习惯在思考的间隙或者做完一整块工作后喝水,但之前一直不知道自己一天会喝多少水,经常会因为喝水太多导致半夜被尿憋醒上厕所,从而影响睡眠。我曾经还给自己立了一个原则:下午 6 点后不要再喝水,发现效果不大,晚上该去厕所还是要去,后来改成 5 点后不要喝水效果也不太大,我觉得根源是其他时间喝水量太多了。

+

最近我自创了一个方法来量化我的喝水量,已经尝试了两周,效果很不错,而且比较简单、易操作,我定期用叮咚买菜购买 4-5 桶怡宝 1.555 升装的纯净水,每天就定量喝这一桶水。因为我平时都在有空调的环境中办公,而且也没有特别大的运动消耗,所以 1.555 升是个比较合理的量。而且这个水的价格也并不贵,每天不到 3 块钱花在喝水上很值,并且因为有了这个水,我就不会再去买其他饮料了,反而节省了一笔开支。

+

+

之前早上到公司后我会吃个凉的水煮蛋并喝一杯水,吃完之后八成会拉肚子,我曾怀疑是因为没喝热水所以才拉肚子,但换成热水还是如此。最近两周刚好没有再吃煮鸡蛋,水也换成了纯净水,反而不拉肚子了。但我这里没有控制好变量,两个变量(凉鸡蛋、水)都变了所以其实不太好说是哪里的问题,但我觉得不太可能是公司水质问题,应该就是凉鸡蛋导致的,如果是水的问题,其他人应该也会有拉肚子的情况。况且公司的水使用的商用的滤水装置,定期检查,不会有什么问题。

+

我在喝怡宝这个水时确实喝出了甘冽都口感,这种口感会让我心情愉悦起来。我现在每天早上到公司后,会先用公司的咖啡机接一份意式浓缩(之前是直接接美式),然后兑上怡宝的纯净水,口感会好很多,也会好喝很多。

+


+

P.S. 通过图片可以看出,我司的咖啡豆还是不错的,油脂很丰富。

+

经过两周的观察,我发现我一上午能喝掉将近三分之二的水,照这么来看之前下午喝的会比这个量还大,远远超过 1.555 升,估计接近 2.5 升了。

+

下边是我这几天的战果:

+

+

施行这种量化方式后,最近一段时间半夜没有再上过厕所了。

+ +
+ + + + + + + +
+ + + + + + +
+ +
+ + + +
+ + + + + + + + +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2022/read-notes-life-is-worth-living/66c2e16cfa96485cadf31ed001d2b319.jpg b/2022/read-notes-life-is-worth-living/66c2e16cfa96485cadf31ed001d2b319.jpg new file mode 100644 index 0000000000..a1b385189e Binary files /dev/null and b/2022/read-notes-life-is-worth-living/66c2e16cfa96485cadf31ed001d2b319.jpg differ diff --git a/2022/read-notes-life-is-worth-living/index.html b/2022/read-notes-life-is-worth-living/index.html new file mode 100644 index 0000000000..5aa68f21a6 --- /dev/null +++ b/2022/read-notes-life-is-worth-living/index.html @@ -0,0 +1,549 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 《人间值得》摘抄 | 贾攀的流水账 + + + + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

贾攀的流水账

+ +
+

Panmax's Blog

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+ + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ 《人间值得》摘抄 +

+ + +
+ + + + +
+ + +

+

第 1 章:工作是为了什么

从本质上来说,人就是为了生活而工作。

+

在我看来,为了钱而工作,并不是可耻的事情,这是理所当然的事,我认为是非常了不起的。

+

赚多少钱倒没那么重要,如果能够支撑自己和家人的日常生活,这就足够了。人生,就是这样活着而已。

+

至于“人生价值”“自我成长”之类,是要等自己立足安稳后,在闲暇之余慢慢思考的问题。人生很长,慢慢思考就好了。

+

有些人痛苦地向我倾诉,说自己“在现在的公司没有发展”,或者“失去了工作的目标”。我认为,这些可能都是想得太多的缘故。

+

如果被权力、地位、名誉之类的东西紧紧束缚住,在工作中一味地在意别人的眼光,很快就会疲于应对。要是这般勉强度过几十年,迟早会被工作击垮。

+

我不关心头衔和职位,这些都如过眼云烟。如果自己和家人健康、精力充沛,有几个知己可以交谈,还有什么其他奢望呢?

+

相反,如果你拼命工作,身体状况因此变得糟糕,自己和家人关系疏远,即使你挣了很多钱,那又有什么幸福可言?

+

不要把自我价值全部建立在工作上,带着“为身边的人略尽绵力”的想法去工作,或许会更好。

+

工作中的人际关系比工作内容重要得多。从我的经验来看,不喜欢工作的大多数是人际关系出了问题。对有些人而言,不管他们做什么工作,他们都讨厌工作,这也许是与人交往上出现了问题。

+

过多的“空闲”,有时会带来负面影响,“适当忙碌”的状态反而更好。

+

如果工作让你一直做出巨大的牺牲,那一定要果断离开,毫不犹豫。

+

我并不提倡过度工作,甚至过劳死。公司不过是“别人赚钱的工具”,如果这个工具紧紧地束缚住了自己的宝贵生命或家人的幸福,那么逃离也无妨。一旦决定“逃离”,你应该自信地离开。

+

习惯遇事不抱怨,依靠自己解决,无论发生什么事情,你都能想办法解决。

+

如果工作让你一直做出巨大的牺牲,那一定要果断离开,毫不犹豫。

+

第 2 章:不要期待过多,对生活中的小事心存感激

过于强调“应该如此”而拼命努力,多半是因为欲望过高。此外,欲望过高的本质,或是“想被人称赞自己努力上进真厉害”

+

在人生中,很多事情不会按照你的想法发生,这会让我们感觉很痛苦。

+

人生不可思议之处在于,即使去了新环境,也会遇到讨厌的人、合不来的人,尽管程度不同,但或多或少都会出现。

+

不要试图通过改变他人来获得快乐,而是想“自己如何做才会快乐”或“怎么努力让自己在这里心情愉快地度过”,我觉得这才是应该考虑的关键。

+

人生的本质就是一个人活着。

+

人际关系是无法预测的。人与人之间可能因为一些小事而结缘,也会因一些小事分离。人会快速地向着有利于自己的方向前行,由于时间或距离的原因不能见面,缘分也会渐渐变浅。这就是人际关系。

+

“情”这个东西看起来是一件好事,但从另一方面来说,它会让你对别人产生期待、执着,让你在关系中变得“自私”。

+

不要有太多的期望。

+

只要是别人给予的东西,自己就应该感谢对方

+

在一生中,任何人都会遇到几次大的转折点,也就是人生的十字路口。

+

总想着得失,那么就会觉得勉强自己,甚至产生心结。与其如此,还不如率性而为,跟随心的决定。

+

第 3 章:恰到好处的人际关系

即使你不能给出建议,没有提供令人豁然开朗的方法,就是简简单单设身处地地倾听,对方也会轻松许多。

+

如果被人说了不好的话,就不妨想想“那家伙在家里有什么惹他烦心的事吗?”,也许心情就会好转。

+

无论是费劲地想要主动交往,还是试图引起对方的关注,都显得不自然、不正常。

+

当你想到“自己这么努力,为什么没有得到回报”,也许对待别人就会变得苛刻。

+

第 4 章:让心归于平静

人为什么会感到不安?大多数情况下,这种不安是因为对未来考虑太多。

+

我认为,只要想清楚今天一天的事情就可以了。

+

任何事物都有两面性,痛苦的经历可以扩展人的本性,就像肌肉可以锻炼、拉伸一样。

+

如果总也不顺利,那么你就要意识到,“人生本来就是这样”。

+

保持心平气和的另一个有效方法,就是“工作时间以外,不考虑工作上的事”

+

在非工作时间,尽量不要考虑工作上的事。

+

自信绝非一成不变的,我们只能在某一段时间或某一领域经历它。

+

总而言之,不被负面情绪影响的最大秘诀就是好好生活

+

痛苦与伤心,其实也是与生俱来的东西。人活着,肯定会经历苦难。

+

不要事事都想咬紧牙关挺过去,只要抱着“今天这样做基本就可以了”的态度,日复一日地坚持积累。

+

第 5 章:生活和工作的平衡之道

在我看来,与其追求完美而挫折不断,不如以笨拙的方式坚持下去。

+

如果母亲的情绪不稳定,孩子的精神状态就会受到影响。

+

父母的心情会扰乱孩子的内心,孩子的波动反过来又会反弹给父母。

+

有多种选择的时候我们往往左右瞻顾,当“只有一个选择”的时候,反而会意外地突破现状。

+

生活如果没有目标,就会变得懒散。一旦决定“今天这样做”,生活一下子就会张弛有度。

+

家庭问题能忍耐就忍耐,工作方面能放松就放松。

+

我非常推荐大家和同事一起出去玩,你可以发现同事在工作之外的真性情,也可以与趣味相投的人成为好朋友。

+

提醒别人的事情,自己如果做不到,更加不好。即使是孩子,也会看穿大人的一言一行。因此,要想改变孩子,首先得改变自己。这样,通过育儿,也会注意到自己一些为人处事的方式。

+

育儿基本的原则是,对待大人和孩子一视同仁。

+

孩子成长的每个过程,比任何一部动漫或电影都令人感动。

+

最关键的是,父母应该真心为孩子的幸福考虑,并付诸行动。这样做,才能将爱传递给孩子

+

父母和孩子的人生车轮虽然驶向不同的方向,由于桥梁的存在,你们可以随时往来。

+

担心死亡来临、提前做好计划终究无济于事。把最基本的要求告诉家人,其余的事情顺其自然就可以。

+

第 6 章:简单生活每一天

在追求的过程中,一定要分清自己是自己,他人在实践他人的人生,我们不需要追寻别人的脚步。

+

越是对别人讨厌、反感,这些情绪就越容易在自己的表情和态度上反映出来,进而传达给对方。

+

由于过于害怕孤独,就会迎合别人,或者对别人妥协,从而使自己痛苦不已。

+

若想人际关系变好,就更应该珍惜一个人的时光。也许,这才是最根本、最重要的事情。

+

“对患者来说,能接待他们的医生很多;但对孩子而言,母亲只有一个

+

和周围的人交往要保持适当的距离,这是维系和谐关系的关键。我们也是有感情的人,在不知不觉中平衡就会被打破。
或许是对别人期待太多,或许是对自己太过严苛,总之与他人交往总会有感觉不舒服的时候。

+

人生的满足感并非由别人决定,也绝不应该追求和别人同样的生活。

+ +
+ + + + + + + +
+ + + + + + +
+ +
+ + + +
+ + + + + + + + +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2022/read-notes-microservices-patterns-1/20220701140535.png b/2022/read-notes-microservices-patterns-1/20220701140535.png new file mode 100644 index 0000000000..ff75e9075a Binary files /dev/null and b/2022/read-notes-microservices-patterns-1/20220701140535.png differ diff --git a/2022/read-notes-microservices-patterns-1/20220701140557.png b/2022/read-notes-microservices-patterns-1/20220701140557.png new file mode 100644 index 0000000000..33ecc363c9 Binary files /dev/null and b/2022/read-notes-microservices-patterns-1/20220701140557.png differ diff --git a/2022/read-notes-microservices-patterns-1/20220701140610.png b/2022/read-notes-microservices-patterns-1/20220701140610.png new file mode 100644 index 0000000000..868339e339 Binary files /dev/null and b/2022/read-notes-microservices-patterns-1/20220701140610.png differ diff --git a/2022/read-notes-microservices-patterns-1/20220701140624.png b/2022/read-notes-microservices-patterns-1/20220701140624.png new file mode 100644 index 0000000000..1e914add07 Binary files /dev/null and b/2022/read-notes-microservices-patterns-1/20220701140624.png differ diff --git a/2022/read-notes-microservices-patterns-1/index.html b/2022/read-notes-microservices-patterns-1/index.html new file mode 100644 index 0000000000..0992d6ddbb --- /dev/null +++ b/2022/read-notes-microservices-patterns-1/index.html @@ -0,0 +1,535 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 【阅读笔记】微服务架构设计模式—第1章 | 贾攀的流水账 + + + + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

贾攀的流水账

+ +
+

Panmax's Blog

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+ + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ 【阅读笔记】微服务架构设计模式—第1章 +

+ + +
+ + + + +
+ + +

软件架构对功能性需求的影响

软件架构对功能性需求影响并不大,架构的重要性在于它影响了应用的非功能性需求,

+

扩展立方体

20220701140535.png
之前只听过垂直扩容和水平扩容,本书中将微服务比喻成一个立方体,X、Y、Z 三个轴表示对应用扩展的 3 种方式:

+
    +
  • X 轴扩展和我们之前了解的水平扩容意思相同
      +
    • 20220701140557.png
    • +
    +
  • +
  • Z 轴扩展表示根据请求属性(如 userId)进行路由,作者称之为流量分区
      +
    • 20220701140610.png
    • +
    +
  • +
  • Y 轴表示根据功能将请求路由到不同的服务,如订单服务、商品服务
      +
    • 20220701140624.png
    • +
    +
  • +
+

作者对微服务的定义

把应用程序功能性分解为一组服务的架构风格。

+

「两个披萨」原则

「两个披萨」原则是指某个事情的参与人数不能多到两个披萨饼还不够他们吃饱的地步。亚马逊CEO贝索斯认为事实上并非参与人数越多越好,他认为人数多不利于决策的形成,并会提高沟通的成本,这被称为「两个披萨」原则。

+

微服务好处

    +
  • 持续交付、持续部署
  • +
  • 容易维护
  • +
  • 独立部署
  • +
  • 独立扩展
  • +
  • 团队自治
  • +
  • 新技术
  • +
  • 容错性
  • +
+

微服务弊端

    +
  • 服务的拆分和定义
  • +
  • 开发、测试和部署更困难
  • +
  • 部署时需要协调更多开发团队
  • +
  • 考虑什么阶段使用微服务架构
  • +
+

马拉法拉利比喻

采用微服务架构以后,如果仍旧沿用瀑布式开发流程,那就跟用一匹马来拉法拉利跑车没什么区别。如果你希望通过微服务架构来完成一个应用程序的开发,那么采用类似 Scrum 或 Kanban 这类敏捷开发和部署实践就是必不可少的。

+

人们对变化做出情绪化反应的三个阶段

    +
  1. 结束、失落和放弃:当人们被告知某种变化,这类变化会把他们从舒适区中拉出,这类情绪开始滋生和蔓延。人们会念叨失去之前的种种好处。
  2. +
  3. 中立区:处理新旧任务工作方式交替过程中,人们普遍会对新的工作方式无所适从。人们开始纠结并必须学习处理新工作的方式。
  4. +
  5. 新的开始:最终阶段,人们开始发自内心地热情拥抱新的工作方式,并且开始体验到新工作方式所带来的种种好处。
  6. +
+ +
+ + + + + + + +
+ + + + + + +
+ +
+ + + +
+ + + + + + + + +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2022/read-notes-microservices-patterns-2/20220712100828.png b/2022/read-notes-microservices-patterns-2/20220712100828.png new file mode 100644 index 0000000000..b8297b4e01 Binary files /dev/null and b/2022/read-notes-microservices-patterns-2/20220712100828.png differ diff --git a/2022/read-notes-microservices-patterns-2/20220712181240.png b/2022/read-notes-microservices-patterns-2/20220712181240.png new file mode 100644 index 0000000000..5319c6a776 Binary files /dev/null and b/2022/read-notes-microservices-patterns-2/20220712181240.png differ diff --git a/2022/read-notes-microservices-patterns-2/20220712181300.png b/2022/read-notes-microservices-patterns-2/20220712181300.png new file mode 100644 index 0000000000..f0308e7a26 Binary files /dev/null and b/2022/read-notes-microservices-patterns-2/20220712181300.png differ diff --git a/2022/read-notes-microservices-patterns-2/20220712181309.png b/2022/read-notes-microservices-patterns-2/20220712181309.png new file mode 100644 index 0000000000..41130a07f7 Binary files /dev/null and b/2022/read-notes-microservices-patterns-2/20220712181309.png differ diff --git a/2022/read-notes-microservices-patterns-2/20220712181316.png b/2022/read-notes-microservices-patterns-2/20220712181316.png new file mode 100644 index 0000000000..dd26d76781 Binary files /dev/null and b/2022/read-notes-microservices-patterns-2/20220712181316.png differ diff --git a/2022/read-notes-microservices-patterns-2/index.html b/2022/read-notes-microservices-patterns-2/index.html new file mode 100644 index 0000000000..0abd0c021f --- /dev/null +++ b/2022/read-notes-microservices-patterns-2/index.html @@ -0,0 +1,576 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 【阅读笔记】微服务架构设计模式—第2章 | 贾攀的流水账 + + + + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

贾攀的流水账

+ +
+

Panmax's Blog

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+ + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ 【阅读笔记】微服务架构设计模式—第2章 +

+ + +
+ + + + +
+ + +

软件架构定义

抽象定义:计算机系统的软件架构是构建这个系统所需要的一组结构,包括软件元素、它们之间的关系以及两者的属性。

+

更容易理解的定义:应用程序的架构是将软件分解为元素(element)和这些元素之间的关系(relation)。

+

4+1 视图模型

应用程序的架构可以从多个视角来看:

+
    +
  • 逻辑视图:开发人员创建的软件元素。
  • +
  • 实现视图:构建编译系统的输出。
  • +
  • 进程视图:运行时的组件。
  • +
  • 部署视图:进程如何映射到机器。
  • +
  • +1 是指场景,它负责把视图串联在一起。
  • +
+

20220712181316.png

+

应用程序两个层面的需求

功能性需求:决定一个应用程序做什么
非功能性需求:决定一个应用程序在运行时的质量

+

六边形架构风格

六边形架构风格选择以业务逻辑为中心的方式组织逻辑视图。

+

20220712181309.png

+

出入站端口

    +
  • 入站端口的一个实例是服务接口,它定义服务的公共方法。
  • +
  • 出站端口是业务逻辑调用外部系统的方式。
  • +
+

出入站适配器

    +
  • 入站适配器通过调用入站端口来处理来自外部世界的请求。
      +
    • Spring MVC Controller
    • +
    • 订阅消息的消息代理客户端
    • +
    +
  • +
  • 出站适配器实现出站端口,并通过调用外部应用程序或服务处理来自业务逻辑的请求。
      +
    • 访问数据库的操作的数据访问对象(DAO)类
    • +
    • 调用远程服务的代理类
    • +
    • 发布事件
    • +
    +
  • +
+

六边形架构风格的一个重要好处是它将业务逻辑与适配器中包含的表示层和数据访问层的逻辑分离开来。业务逻辑不依赖于表示层逻辑或数据访问层逻辑。

+

松耦合

松耦合服务是改善开发效率、提升可维护性和可测试性的关键。小的、松耦合的服务更容易被理解、修改和测试。

+

保证数据的私有属性是实现松耦合的前提之一。

+

服务的定义

服务是一个单一的、可独立部署的软件组件,它实现了一些有用的功能。

+

服务的 API 封装了其内部实现。

+

微服务架构模式

将应用程序构建为松耦合、可独立部署的一组服务。

+

定义应用程序架构的三步式流程:

+
    +
  1. 定义系统操作
      +
    1. 创建由关键类组成的抽象领域模型
        +
      • 用户故事中提及的名词
      • +
      +
    2. +
    3. 确定系统操作
        +
      • 用户故事中提及的动词
      • +
      +
    4. +
    +
  2. +
  3. 定义分解服务
      +
    • 方法 1:采用业务能力进行服务拆分
    • +
    • 方法 2:根据子域进行服务拆分
    • +
    +
  4. +
  5. 定义服务 API 和写作方式
  6. +
+

定义应用程序架构过程图

20220712181300.png

+

定义系统操作过程图

20220712181240.png

+

服务分解后的几个障碍

    +
  1. 网络延迟
  2. +
  3. 可用性
  4. +
  5. 数据一致性
  6. +
  7. 上帝类
  8. +
+

两类系统操作

    +
  • 命令型:创建、更新或删除数据的系统操作。
  • +
  • 查询型:查询和读取数据的系统操作。
  • +
+

根据子域进行服务拆分

领域驱动为每一个子域定义单独的领域模型。

+
    +
  • 子域是领域的一部分
  • +
  • 领域是 DDD 中用来描述应用程序问题域的一个术语
  • +
+

DDD 把领域模型的边界称为限界上下文(bounded context)。

+

我们可以通过 DDD 的方式定义子域,并把子域对应为每一个服务,这样就完成了微服务架构的设计工作。

+

微服务架构应遵循单一职责原则和闭包原则:

    +
  • 单一职责原则:设计小的、内聚的、仅仅含有单一职责的服务。这会缩小服务的大小并提升它的稳定性。
  • +
  • 闭包原则:把根据同样原因进行变化的服务放在一个组件内。这样做可以控制服务的数量,当需求发生变化时,变更和部署也更加容易。理想情况下,一个变更只会影响一个团队和一个服务。
  • +
+ +
+ + + + + + + +
+ + + + + + +
+ +
+ + + +
+ + + + + + + + +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2022/read-notes-microservices-patterns-3/20220906181622.png b/2022/read-notes-microservices-patterns-3/20220906181622.png new file mode 100644 index 0000000000..2ba71268ab Binary files /dev/null and b/2022/read-notes-microservices-patterns-3/20220906181622.png differ diff --git a/2022/read-notes-microservices-patterns-3/20220907100739.png b/2022/read-notes-microservices-patterns-3/20220907100739.png new file mode 100644 index 0000000000..2add4339b6 Binary files /dev/null and b/2022/read-notes-microservices-patterns-3/20220907100739.png differ diff --git a/2022/read-notes-microservices-patterns-3/20220907100828.png b/2022/read-notes-microservices-patterns-3/20220907100828.png new file mode 100644 index 0000000000..957f1c636f Binary files /dev/null and b/2022/read-notes-microservices-patterns-3/20220907100828.png differ diff --git a/2022/read-notes-microservices-patterns-3/20220908182530.png b/2022/read-notes-microservices-patterns-3/20220908182530.png new file mode 100644 index 0000000000..279453231a Binary files /dev/null and b/2022/read-notes-microservices-patterns-3/20220908182530.png differ diff --git a/2022/read-notes-microservices-patterns-3/index.html b/2022/read-notes-microservices-patterns-3/index.html new file mode 100644 index 0000000000..8f8cc10c34 --- /dev/null +++ b/2022/read-notes-microservices-patterns-3/index.html @@ -0,0 +1,789 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 【阅读笔记】微服务架构设计模式—第3章 | 贾攀的流水账 + + + + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

贾攀的流水账

+ +
+

Panmax's Blog

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+ + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ 【阅读笔记】微服务架构设计模式—第3章 +

+ + +
+ + + + +
+ + +

一个理想的微服务架构应该是在内部由松散耦合的若干服务组成,这些服务使用异步消息相互通信。REST 等同步协议主要用于服务与外部其他应用程序的通信。

+

考虑交互方式将有助于你专注于需求,并避免陷人特定进程间通信技术的细节。

+

交互方式的选择会影响应用程序的可用性,交互方式还可以帮助你选择更合适的集成测试策略。

+

有多种客户端与服务的交互方式

它们可以分为两个维度。

+

第一个维度关注的是一对一和一对多:

    +
  • 一对一:每个客户端请求由一个服务实例来处理。
  • +
  • 一对多:每个客户端请求由多个服务实例来处理。
  • +
+

一对一的交互方式有以下几种类型:

+
    +
  • 请求/响应:一个客户端向服务端发起请求,等待响应;客户端期望服务端很快就会发送响应。
      +
    • 在一个基于线程的应用中,等待过程可能造成线程阻塞。这样的方式会导致服务的紧耦合
    • +
    +
  • +
  • 异步请求/响应:客户端发送请求到服务端,服务端异步响应请求。
      +
    • 客户端在等待响应时不会阻塞线程,因为服务端的响应不会马上就返回。
    • +
    +
  • +
  • 单向通知:客户端的请求发送到服务端,但是并不期望服务端做出任何响应。
  • +
+

一对多的交互方式有以下几种类型:

+
    +
  • 发布 / 订阅方式:客户端发布通知消息,被零个或者多个感兴趣的服务订阅。
  • +
  • 发布 / 异步响应方式:客户端发布请求消息,然后等待从感兴趣的服务发回的响应。
  • +
+

第二个维度关注的是同步和异步:

    +
  • 同步模式:客户端请求需要服务端实时响应,客户端等待响应时可能导致堵塞。
  • +
  • 异步模式:客户端请求不会阻塞进程,服务端的响应可以是非实时的。
  • +
+

20220906181622.png

+

一个设计良好的接口会在暴露有用功能同时隐藏实现的细节。

+

„„你应该努力只进行向后兼容的更改。向后兼容的更改是对 API 的附加更改或功能增强:

+
    +
  • 添加可选属性。
  • +
  • 向响应添加属性。
  • +
  • 添加新操作。
  • +
+

「严以律己,宽以待人」:服务应该为缺少的请求属性提供默认值;客户端应忽略任何额外的响应属性。

+
+

进程间通信的本质是交换消息。消息通常包括数据,因此一个重要的设计决策就是这些数据的格式。消息格式的选择会对进程间通信的效率、API 的可用性和可演化性产生影响。

+

消息的格式可以分为两大类:文本和二进制。

+
    +
  • 第一类是 JSON 和 XML 这样的基于文本的格式。
      +
    • 这类消息格式的好处在于,它们的可读性很高,同时也是自描述的。
        +
      • JSON 消息是命名属性的集合。
      • +
      • 相似地,XML 消息也是命名属性的集合。
      • +
      +
    • +
    • 使用基于文本格式消息的弊端主要是消息往往过度冗长,特别是 XML。另外一个弊端是解析文本引入的额外开销,尤其是在消息较大的时候。
    • +
    +
  • +
  • 有几种不同的二进制格式可供选择。常用的包括 Protocol Buffers) 和 Avro
  • +
  • Protocol Buffers 使用 tagged fields(带标记的字段),
  • +
  • Avro 的消费者在解析消息之前需要知道它的格式。
  • +
  • 因此,实行 API 的版本升级演进, Protocol Buffer 要优于 Avro
  • +
+

REST 是一种使用 HTTP 协议的进程间通信机制

+
    +
  • REST 中的一个关键概念是资源,它通常表示单个业务对象。
  • +
  • REST 使用 HTTP 动词来操作资源,使用 URL 引用这些资源。
  • +
+

REST 成熟度模型

    +
  • Level 0:Level 0 层级服务的客户端只是向服务端点发起 HTTP POST 请求,进行服务调用。每个请求都指明了需要执行的操作、这个操作针对的目标(例如,业务对象) 和必要的参数。
  • +
  • Level 1: Level 1 层级的服务引入了资源的概念。要执行对资源的操作,客户端需要发出指定要执行的操作和包含任何参数的 POST 请求。
  • +
  • Level 2:Level 2 层级的服务使用 HTTP 动词来执行操作,譬如 GET 表示获取、 POST 表示创建、PUT 表示更新。请求查询参数和主体(如果有的话)指定操作的参数。这让服务能够借助 Web 基础设施服务,例如通过 CDN 来缓存 GET 请求。
  • +
  • Level 3: Level 3 层级的服务基于 HATEOAS (Hypertext As The Engine Of Application State)原则设计,基本思想是在由 GET 请求返回的资源信息中包含链接,这些链接能够执行该资源允许的操作。例如,客户端通过订单资源中包含的链接取消某一订单,或者发送 GET 请求去获取该订单,等等。HATEOAS 的优点包括无须在客户端代码中写入硬链接的 URL。此外,由于资源信息中包含可允许操作的链接,客户端无须猜测在资源的当前状态下执行何种操作。
  • +
+

REST 最初没有 IDL。幸运的是,开发者社区重新发现了 RESTful API 的 IDL 价值。最流行的 REST IDL 是 Open API 规范

+
+

REST 好处和弊端

好处

    +
  • 它非常简单,并且大家都很熟悉。
  • +
  • 可以使用浏览器扩展(比如 Postman 插件)或者 curl 之类的命令行(假设使用的是 JSON 或其他文本格式)来测试 HTTP API。
  • +
  • 直接支持请求 /响应方式的通信。
  • +
  • HTTP 对防火墙友好。
  • +
  • 不需要中间代理,简化了系统架构。
  • +
+

弊端

    +
  • 它只支持请求 /响应方式的通信。
  • +
  • 可能导致可用性降低。
      +
    • 由于客户端和服务直接通信而没有代理来缓冲消息,因此它们必须在 REST API 调用期间都保持在线。
    • +
    +
  • +
  • 客户端必须知道服务实例的位置(URL)。
      +
    • 这是现代应用程序中的一个重要问题。客户端必须使用所谓的服务发现机制来定位服务实例。
    • +
    +
  • +
  • 在单个请求中获取多个资源具有挑战性。
  • +
  • 有时很难将多个更新操作映射到 HTTP 动词。
      +
    • 考虑 gRPC
    • +
    +
  • +
+

gRPC API 由一个或多个服务和请求/响应消息定义组成。服务定义类似于 Java 接口,是强类型方法的集合。除了支持简单的请求 /响应 RPC 之外,gRPC 还支持流式 RPC。

+

gRPC 使用 Protocol Buffers 作为消息格式。

+
    +
  • Protocol Buffers 是一种高效且紧凑的二进制格式。
  • +
  • 它是一种标记格式:Protocol Buffers 消息的每个字段都有编号,并且有一个类型代码。消息接收方可以提取所需的字段,并跳过它无法识别的字段。因此,gRPC 使 API 能够在保持向后兼容的同时进行变更。
  • +
+

gRPC 好处和弊端

好处

    +
  • 设计具有复杂更新操作的 API 非常简单。
  • +
  • 它具有高效、紧凑的进程间通信机制,尤其是在交换大量消息时。
  • +
  • 支持在远程过程调用和消息传递过程中使用双向流式消息方式。
  • +
  • 它实现了客户端和用各种语言编写的服务端之间的互操作性。
  • +
+

弊端

    +
  • 与基于 REST/JSON 的 API 机制相比,JavaScript 客户端使用基于 gRPC 的 API 需要做更多的工作。
  • +
  • 旧式防火墙可能不支持 HTTP/2。
  • +
+
+

服务保护自己的方法包括以下机制的组合:

+
    +
  • 网络超时:在等待针对请求的响应时,一定不要做成无限阻塞,而是要设定一个超时。使用超时可以保证不会一直在无响应的请求上浪费资源。
  • +
  • 限制客户端向服务器发出请求的数量:把客户端能够向特定服务发起的请求设置一个上限,如果请求达到了这样的上限,很有可能发起更多的请求也无济于事,这时就应该让请求立刻失败。
  • +
  • 断路器模式:监控客户端发出请求的成功和失败数量,如果失败的比例超过一定的阈值,就启动断路器,让后续的调用立刻失效。
      +
    • 如果大量的请求都以失败而告终,这说明被调服务不可用,这样即使发起更多的调用也是无济于事。在经过一定的时间后,客户端应该继续尝试,如果调用成功,则解除断路器。
    • +
    • 断路器是一个远程过程调用的代理,在连续失败次数超过指定阀值后的一段时间内,这个代理会立即拒绝其他调用。
    • +
    +
  • +
+
+

服务发现在概念上非常简单:其关键组件是服务注册表,它是包含服务实例网络位置信息的一个数据库。

+

服务实例启动和停止时,服务发现机制会更新服务注册表。当客户端调用服务时,服务发现机制会查询服务注册表以获取可用服务实例的列表,并将请求路由到其中一个服务实例。

+

实现服务发现有以下两种主要方式:

+
    +
  • 应用层服务发现模式:服务及其客户直接与服务注册表交互。
      +
    • 这种服务发现方法是两种模式的组合:
        +
      • 第一种模式是自注册模式
          +
        • 自注册:服务实例向服务注册表注册自己。
        • +
        +
      • +
      • 第二种模式是客户端发现模式
          +
        • 客户端发现:客户端从服务注册表检索可用服务实例的列表,并在它们之间进行负载平衡。
        • +
        +
      • +
      +
    • +
    • 好处:
        +
      • 可以处理多平台部署的问题
      • +
      +
    • +
    • 弊端:
        +
      • 需要为你使用的每种编程语言(可能还有框架)提供服务发现库
      • +
      • 开发者负责设置和管理服务注册表,这会分散一定的精力
      • +
      +
    • +
    +
  • +
  • 平台层服务发现模式:通过部署基础设施来处理服务发现。
      +
    • 这种方法是以下两种模式的组合:
        +
      • 第三方注册模式:由第三方负责(称为注册服务器,通常是部署平台的一部分)处理注册,而不是服务本身向服务注册表注册自己。
          +
        • 第三方注册:服务实例由第三方自动注册到服务注册表
        • +
        +
      • +
      • 服务端发现模式:客户端不再需要查询服务注册表,而是向 DNS 名称发出请求,对该 DNS 名称的请求被解析到路由器,路由器查询服务注册表并对请求进行负载均衡。
          +
        • 服务端发现:客户端向路由器发出请求,路由器负责服务发现
        • +
        +
      • +
      +
    • +
    • 好处:
        +
      • 服务发现的所有方面都完全由部署平台处理
      • +
      +
    • +
    • 弊端:
        +
      • 仅限于支持使用该平台部署的服务
      • +
      +
    • +
    +
  • +
+

服务发现示例图

应用层服务发现模式

20220907100739.png

+

平台层服务发现模式

20220907100828.png

+
+

有以下几种不同类型的消息:

+
    +
  • 文档:仅包含数据的通用消息。接收者决定如何解释它。
      +
    • 对命令式消息的回复是文档消息的一种使用场景。
    • +
    +
  • +
  • 命令:一条等同于 RPC 请求的消息。它指定要调用的操作及其参数。
  • +
  • 事件:表示发送方这一端发生了重要的事件。
      +
    • 事件通常是领域事件,表示领域对象 (如 order 或 customer)的状态更改。
    • +
    +
  • +
+

有以下两种类型的消息通道:点对点发布-订阅

+
    +
  • 点对点通道向正在从通道读取的一个消费者传递消息。
      +
    • 服务使用点对点通道来实现前面描述的一对一交互方式。例如,命令式消息通常通过点对点通道发送。
    • +
    +
  • +
  • 发布-订阅通道将一条消息发给所有订阅的接收方。
      +
    • 服务使用发布一订阅通道来实现前面描述的一对多交互方式。例如,事件式消息通常通过发布-订阅通道发送。
    • +
    +
  • +
+

无代理消息 vs 基于代理的消息

无代理消息

好处:

+
    +
  • 允许更轻的网络流量和更低的延迟,因为消息直接从发送方发送到接收方,而不必从发送方到消息代理,再从代理转发到接收方。
  • +
  • 消除了消息代理可能成为性能瓶颈或单点故障的可能性。
  • +
  • 具有较低的操作复杂性,因为不需要设置和维护消息代理。
  • +
+

弊端:

+
    +
  • 服务需要了解彼此的位置,因此必须使用服务发现机制。
  • +
  • 会导致可用性降低,因为在交换消息时,消息的发送方和接收方都必须同时在线。
  • +
  • 在实现例如确保消息能够成功投递这些复杂功能时的挑战性更大。
  • +
+

基于代理的消息

消息代理是所有消息的中介节点。

+

好处:

+
    +
  • 松耦合:客户端发起请求时只要发送给特定的通道即可,客户端完全不需要感知服务实例的情况,客户端不需要使用服务发现机制去获得服务实例的网络位置。
  • +
  • 消息缓存:消息代理可以在消息被处理之前一直缓存消息。
      +
    • 像 HTTP 这样的同步请求/ 响应协议,在交换数据时,发送方和接收方必须同时在线。然而,在使用消息机制的情况下,消息会在队列中缓存,直到它们被接收方处理。这就意味着,例如,即使订单处理系统暂时离线或不可用,在线商店仍旧能够接受客户的订单。订单消息将会在队列中缓存(并不会丢失)。
    • +
    +
  • +
  • 灵活的通信:消息机制支持前面提到的所有交互方式。
  • +
  • 明确的进程间通信:基于 RPC 的机制总是企图让远程服务调用跟本地调用看上去没什么区别(在客户端和服务端同时使用远程调用代理)。然而,因为物理定律(如服务器不可预计的硬件失效)和可能的局部故障,远程和本地调用还是大相径庭的。消息机制让这些差异交得很明确,这样程序员不会陷人一种“太平盛世”的错觉。
  • +
+

弊端:

+
    +
  • 潜在的性能瓶颈:消息代理可能存在性能瓶颈。幸运的是,许多现代消息代理都支持高度的横向扩展。
  • +
  • 潜在的单点故障:消息代理的高可用性至关重要,否则系统整体的可靠性将受到影响。幸运的是,大多数现代消息代理都是高可用的。
  • +
  • 额外的操作复杂性:消息系统是一个必须独立安装、配置和运维的系统组件。
  • +
+

选择消息代理时,你需要考虑以下各种因素:

+
    +
  • 支持的编程语言:你选择的消息代理应该支持尽可能多的编程语言。
  • +
  • 支持的消息标准:消息代理是否支持多种消息标淮,比如 AMQP 和 STOMP,还是它仅支持专用的消息标准?
  • +
  • 消息排序:消息代理是否能够保留消息的排序?
  • +
  • 投递保证:消息代理提供什么样的消息投递保证?
  • +
  • 持久性:消息是否持久化保存到磁盘并且能够在代理崩溃时恢复?
  • +
  • 耐久性:如果接收方重新连接到消息代理,它是否会收到断开连接时发送的消息?
  • +
  • 可扩展性:消息代理的可扩展性如何?
  • +
  • 延迟:端到端是否有较大延迟?
  • +
  • 竞争性(并发)接收方:消息代理是否支持竞争性接收方?
  • +
+
+

使用多个线程和服务实例来并发处理消息可以提高应用程序的吞吐量。但同时处理消息的挑战是确保每个消息只被处理一次,并且是按照它们发送的顺序来处理的。

+

现代消息代理(如 Apache Kafka 和 AWS Kinesis) 使用的常见解决方案是使用分片(分区)通道。该解决方案分为三个部分。

+
    +
  1. 分片通道由两个或多个分片组成,每个分片的行为类似于一个通道。
  2. +
  3. 发送方在消息头部指定分片键,通常是任意字符串或字节序列。消息代理使用分片键将消息分配给特定的分片。例如,它可以通过计算分片键的散列来选择分片。
  4. +
  5. 消息代理将接收方的多个实例组合在一起,并将它们视为相同的逻辑接收方。例如, Apache Kafka 使用术语消货者组。消息代理将每个分片分配给单个接收器。它在接收方启动和关闭时重新分配分片。
  6. +
+

20220908182530.png

+
+

处理重复消息有以下两种不同的方法:

+
    +
  • 编写幂等消息处理程序。
  • +
  • 跟踪消息并丢弃重复项。
  • +
+

程序的幂等性,是指即使这个应用被相同输入参数多次重复调用时,也不会产生额外的效果。

+
    +
  • 例如,取消一个已经被取消的订单,就是一个幂等性操作。
  • +
+

跟踪消息并丢弃重复消息方案:

+
    +
  1. 消息接收方使用 message id 跟踪它已处理的消息并丢弃任何重复项。
  2. +
  3. 在应用程序表,而不是专用表中记录 message id。
  4. +
+
+

服务通常需要在更新数据库的事务中发布消息。

+

确保消息的可靠发送的机制:

+
    +
  • 使用数据库表作为消息队列
  • +
  • 通过轮询模式发布事件
  • +
  • 使用事务日志拖尾模式发布事件
  • +
+
+

领域事件是聚合 (业务对象)在创建、更新或删除时触发的事件。服务使用 DomainEventPublisher 接口发布领域事件。

+

如果你想最大化一个系统的可用性,就应该设法最小化系统的同步操作量。

+
    +
  • 即:应该尽可能选择异步通信机制来处理服务之间的调用。
  • +
+

消除同步交互的方法:

+
    +
  • 使用异步交互模式
  • +
  • 复制数据
      +
    • 弊端:
        +
      • 有时候被复制的数据量巨大,会导致效率低下
      • +
      • 复制数据并没有从根本上解决服务如何更新其他服务所拥有的数据这个问题
      • +
      +
    • +
    +
  • +
  • 先返回响应,再完成处理
      +
    1. 仅使用本地的数据来完成请求的验证。
    2. +
    3. 更新数据库,包括向 OUTBOX 表插人消息。
    4. +
    5. 向客户端返回响应。
    6. +
    +
  • +
+ +
+ + + + + + + +
+ + + + + + +
+ +
+ + + +
+ + + + + + + + +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2022/read-notes-microservices-patterns-7/20220713130806.png b/2022/read-notes-microservices-patterns-7/20220713130806.png new file mode 100644 index 0000000000..a91bcec56f Binary files /dev/null and b/2022/read-notes-microservices-patterns-7/20220713130806.png differ diff --git a/2022/read-notes-microservices-patterns-7/20220713204935.png b/2022/read-notes-microservices-patterns-7/20220713204935.png new file mode 100644 index 0000000000..2e46884991 Binary files /dev/null and b/2022/read-notes-microservices-patterns-7/20220713204935.png differ diff --git a/2022/read-notes-microservices-patterns-7/index.html b/2022/read-notes-microservices-patterns-7/index.html new file mode 100644 index 0000000000..b8d8396102 --- /dev/null +++ b/2022/read-notes-microservices-patterns-7/index.html @@ -0,0 +1,563 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 【阅读笔记】微服务架构设计模式—第7章 | 贾攀的流水账 + + + + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

贾攀的流水账

+ +
+

Panmax's Blog

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+ + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ 【阅读笔记】微服务架构设计模式—第7章 +

+ + +
+ + + + +
+ + +

两种查询模式

在微服务架构中实现查询操作有两种不同的模式:

+
    +
  • API 组合模式:这是最简单的方法,应尽可能使用。它的工作原理是让拥有数据的服务的客户端负责调用服务,并组合服务返回的查询结果。
  • +
  • 命令查询职责隔离(CQRS)模式:它比 API 组合模式更强大,但也更复杂。它维护一个或多个视图数据库,其唯一目的是支持查询。
  • +
+

API 组合模式

API 组合模式有两种类型的参与者

    +
  • API 组合器:它通过查询数据提供方的服务来实现查询操作。
  • +
  • 数据提供方服务:拥有查询返回的部分数据的服务。
  • +
+

API 组合模式是否可使用的几个因素:

    +
  • 数据分区方式
  • +
  • 拥有数据的服务公开 API 的功能
  • +
  • 服务使用数据库的功能
  • +
+

担任 API 组合器的三个选择

    +
  1. 由服务的客户端扮演 API 组合器的角色
  2. +
  3. 实现应用程序外部 API 的 API Gateway 来扮演 API 组合器的角色
  4. +
  5. 将 API 组合器实现为独立的服务
  6. +
+

API 组合器应尽可能地并行调用提供方服务,最大限度地缩短查询操作的响应时间。

+

API 组合模式好处和弊端

好处

    +
  • 简单直观
  • +
+

弊端

    +
  • 增加了额外的开销
  • +
  • 带来可用性降低的风险
      +
    • 提高可用性方案:返回缓存数据或不完整数据
    • +
    +
  • +
  • 缺乏事务数据一致性
  • +
+

CQRS 模式

CQRS 是命令查询职责隔离 (Command Query Responsibility Segregation) 的简称,它涉及隔离或问题的分隔。它将持久化数据模型和使用数据的模块分为两部分:命令端和查询端。

+
    +
  • 命令端模块和数据模型实现创建、更新和删除操作(缩写为 CUD,例如:HTTP POST、PUT 和 DELETE)。
  • +
  • 查询端模块和数据模型实现查询(例如 HTTP GET)。查询端通过订阅命令端发布的事件,使其数据模型与命令端数据模型保持同步。
  • +
+

+

CQRS 好处与弊端

好处

    +
  • 在微服务架构中高效地实现查询。
  • +
  • 高效地实现多种不同的查询类型。
  • +
  • 在基于事件湖源技术的应用程序中实现查询。
  • +
  • 更进一步地实现问题隔离。
  • +
+

弊端

    +
  • 更加复杂的架构。
  • +
  • 处理数据复制导致的延迟。
  • +
+

CQRS 设计

CQRS 视图模块包括由一个或多个查询操作组成的 APl。它通过订阅由一个或多个服务发布的事件来更新它的数据库视图,从而实现这些查询操作。

+

+
    +
  • 数据访问模块实现数据库访问逻辑。
  • +
  • 事件处理程序查询 API 模块使用数据访问模块来更新和查询数据库。
      +
    • 事件处理程序模块订阅事件并更新数据库。
    • +
    • 查询 API 模块负责实现查询 API。
    • +
    +
  • +
+

NoSQL

NoSQL 数据库通常具有有限的事务模式和较少的查询功能。在一些情况下, NOSQL 数据库比 SQL 数据库更有优势,包括更灵活的数据模型以及更好的性能和可扩展性。

+

NoSQL 数据库通常是 CQRS 视图的一个很好的选择,CQRS 可以利用它们的优势并忽略其弱点。CQRS 视图受益于 NoSQL 数据库更丰富的数据模型和性能。它不受 NoSQL 数据库事务处理能力的限制,因为 CQRS 只需要使用简单的事务并执行一组固定的查询即可。

+

判断视图未及时更新的一个思路

命令和查询模块 API 可以使客户端使用以下方法检测不一致性。

+
    +
  1. 命令端操作将包含已发布事件的 ID 标记返回给客户端。
  2. +
  3. 客户端把这个事件有关的 ID 传递给查询操作,如果该事件尚未更新视图,则返回查询错误。视图模块可以使用重复事件检测机制来实现这样的功能。
  4. +
+

检测重复事件

OrderHistoryDaoDynamoDb DAO 可以使用名为 «aggregateType>><<aggregateId>> 的属性跟踪从每个聚合实例接收的事件,其值是接收到的最高事件 ID。如果属性存在且其值小于或等于事件 ID,则事件是重复的。

+

增量式构建 CQRS视图

    +
  1. 基于其先前的快照和自创建快照以来发生的事件,定期计算每个聚合实例的快照
  2. +
  3. 使用快照和任何后续事件创建视图
  4. +
+ +
+ + + + + + + +
+ + + + + + +
+ +
+ + + +
+ + + + + + + + +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2022/recent-breakfast-burger-king/IMG_6133.jpeg b/2022/recent-breakfast-burger-king/IMG_6133.jpeg new file mode 100644 index 0000000000..7c5b25a428 Binary files /dev/null and b/2022/recent-breakfast-burger-king/IMG_6133.jpeg differ diff --git a/2022/recent-breakfast-burger-king/IMG_6147.jpeg b/2022/recent-breakfast-burger-king/IMG_6147.jpeg new file mode 100644 index 0000000000..29b3b6c185 Binary files /dev/null and b/2022/recent-breakfast-burger-king/IMG_6147.jpeg differ diff --git a/2022/recent-breakfast-burger-king/IMG_6238.jpeg b/2022/recent-breakfast-burger-king/IMG_6238.jpeg new file mode 100644 index 0000000000..0175059836 Binary files /dev/null and b/2022/recent-breakfast-burger-king/IMG_6238.jpeg differ diff --git a/2022/recent-breakfast-burger-king/IMG_6295.jpeg b/2022/recent-breakfast-burger-king/IMG_6295.jpeg new file mode 100644 index 0000000000..881bb17228 Binary files /dev/null and b/2022/recent-breakfast-burger-king/IMG_6295.jpeg differ diff --git a/2022/recent-breakfast-burger-king/IMG_6406.jpeg b/2022/recent-breakfast-burger-king/IMG_6406.jpeg new file mode 100644 index 0000000000..12bdede4d4 Binary files /dev/null and b/2022/recent-breakfast-burger-king/IMG_6406.jpeg differ diff --git a/2022/recent-breakfast-burger-king/index.html b/2022/recent-breakfast-burger-king/index.html new file mode 100644 index 0000000000..b140d9c800 --- /dev/null +++ b/2022/recent-breakfast-burger-king/index.html @@ -0,0 +1,523 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 最近一个月的「汉堡王」早餐心得分享 | 贾攀的流水账 + + + + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

贾攀的流水账

+ +
+

Panmax's Blog

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+ + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ 最近一个月的「汉堡王」早餐心得分享 +

+ + +
+ + + + +
+ + +

今天这篇博客和之前的有些不同,主要区别是这篇是我用语音写的。也就是把「飞书妙记」打开录音,之后转成文字整理出来。

+

为什么要拿语音写呢?我之前写博客都是通过电脑编写,写的过程中会浏览一些其他网页,找找资料之类的,而且打字的话很容易打断思绪。所以我这次尝试用说的形式来整理一篇博客出来。

+

用语音写的另一个原因是它会逼着我一直的去往下不停地去说,会尽量的不去中断,尽量的去保持思考状态,往外输出,也许还能提升我的口语表达能力。如果是打字的话,一会儿看看这个一会儿看看那个很容易被中断。

+

使用语音录入完转文字后,再通过电脑把段落顺序做些调整,把口语化表达改为书面表达就可以发布了。我发现整理所花费的时间比说的时间还要长,录了 20 分钟,整理了 1 小时

+

明天就是国庆节了,周围有小一半的同事请了假,我下午也请了半天假,所以利用中午的时间把这篇博客发完也就要下班了,祝大家国庆节快乐。

+

这篇博客想聊一下我近一个月来如何解决上班时间早饭和午饭的问题。

+

我们公司(望京 Soho)附近有很多餐厅、便利店,我看到公司对面有一个店面很大的「汉堡王」,我之前对汉堡王的印象或者说对汉堡的印象,是一个不是特别健康的食品。不过我依然抱着试一试的态度搜了一下他家的菜单,顺便看一看有没有优惠之类的,毕竟打工人还是想找一些经济实惠的东西解决温饱问题。

+

经过我一番调查后发现,「汉堡王」有一个有 3 种会员:

+
    +
  • 19.9 元: 早餐半价
  • +
  • 9.9 元:每天可以 8 元买一杯咖啡
  • +
  • 29.9 元:包括了上面两个优惠,还额外多一个每天减免一次是外卖送餐费用。
  • +
+

看了一下他家的早餐,早餐里边有一个汉堡特特别吸引我,名字叫「双蛋双牛堡」。它的厉害之处就在于上下两层的面包皮换成了两个煎蛋,中间是两片牛肉。所以这是个对健身人士非常友好的汉堡,当然我也不是健身人士,我只是说它里面的蛋白质非常的丰富。

+

我办了 29.9 的那个会员,因为我还喜欢喝咖啡。平时的话我不会频繁去买外边的咖啡,而是喝公司免费的美式。外边的咖啡比如瑞幸 15 起,星巴克 30 起,我一周也就喝一次外边的咖啡,通常是周五。但是有了这个会员,我每天都可以喝一杯咖啡,8 块钱不心疼,虽然和之前相比花的更多了,但是心理更愉悦了。

+

我大概已经用使用这个会员一个月了,基本上每天早晨都是买「双蛋双牛堡」套餐,套餐包括一个汉堡再加一个小杯的美式,我一般换成豆浆,因为公司有免费的美式。偶尔想解解馋,或者是想吃一些别的汉堡的话也会换一换。今天主要就说「双蛋双牛堡」,我觉得我一个男生一次性把它吃完都会有些顶,女生应该是不太好一次性吃完的。我刚开始是每天早晨就把汉堡吃完,中午吃一块鸡胸肉或者就不吃了,这样我发现下午会有些饿。最近这两周我换了个办法:把汉堡分成两份,因为它是上下各一个煎蛋、中间两片牛肉,我就从中间均分,早晨吃一片牛肉加一个煎蛋,中午吃一片牛肉加一个煎蛋,而且还有一小杯口味不错的热豆浆,这样的话我的蛋白质也是够的,并且还能保证下午在吃晚饭前不那么饿。

+

+

这种方式,早饭加午饭也只花了 10 块钱,中午的时候我还会去买一杯咖啡,8 元咖啡可以在所有的「汉堡王」销售的咖啡里任选一种,包括杯型、口味都是任选,不管多少钱,最后都会减成 8 元。所以我一定是考虑着自身利益最大化的原则,我每次都是买最贵的澳白,而且是买大杯。

+

+

「汉堡王」的大杯就是「星巴克」的超大杯,它没有从中杯开始算,用的小杯、中杯、大杯三种规格,它的大杯跟星巴克的超大杯是一样的。「星巴克」超大杯的澳白我记着好像是 38,「汉堡王」 8 块钱,瞬间就省了 30。口味的话我觉得差的不是特别多。我现在就是边喝「汉堡王」的咖啡,边写这篇博客的。

+

这个汉堡叫「帕斯雀牛肉可颂堡」,也特别好吃的,偶尔想解一次馋了的话我也会去买这个。

+

+

还有一个汉堡叫「牛肉蛋可颂」,也是在早餐里我也是觉得比较好吃。

+

+

其实我也没吃太多种,目前吃的每一种都觉得很好吃。其他早餐还有咸蛋黄鸡肉粥、老北京烤鸡卷、咸蛋黄鸡肉卷等等。这些我还没有尝试过,后边有机会的话可以尝试一下。

+

我中午就不用再去出去排队去吃饭了,多出来的时间就可以看看书或者是出去玩一会陆地冲浪板,陆冲这个东西是特别的上头,建议大家有机会都试一试。

+

下边这张图是我今天早晨买的,因为下午要请假,所以中午就不再单独去买杯咖啡喝了,早晨就一起把咖啡买了,可以看到一个大杯的澳白,加一个小杯美式(今天没换豆浆),再加一个汉堡(今天汉堡卖相不太好),这一套下来才 18 块钱。在其他地方,这些钱也就只能买一杯咖啡,同样的价格能在「汉堡王」买下三件套,真的是能开心一整天。

+

+

可以算一下,每个月我们按照 22 天工作日算,每天花费 18 块钱,也就是早饭+午饭再加一杯咖啡(晚餐公司提供),一共是 396 的话再加上一个月的会员费 30 元。这样的话一个月只需花费 426 就能把早饭和午饭解决掉。我觉着是比较划算的,而且蛋白质摄入一定是没有太大问题的,而且还能让我解咖啡的馋,让我每天都可以通过一杯超大澳白续命,提升工作的幸福感。工作已经很苦了,不如再来一杯好喝的咖啡让自己幸福一下。

+

关于打工人的早午餐就分享到这里,如果你附近有汉堡王的话,也可以试一试。因为我是喜欢喝咖啡又想解决双餐的问题,所以选的是 29.9 的会员。如果你只想喝咖啡,可以办 9.9 的。如果你只需要早餐,也可以办 19.9 的。

+

我觉得 29.9 的这个会员是很值。如果「汉堡王」的「双蛋双牛堡」不下架,且我不换公司的话,我应该会去一直续费。

+

今天入职「探探」两周年,过的真快,下班了,拜了个拜。👋🏻

+ +
+ + + + + + + +
+ + + + + + +
+ +
+ + + +
+ + + + + + + + +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2022/recent-new-habit/index.html b/2022/recent-new-habit/index.html new file mode 100644 index 0000000000..df7434a89e --- /dev/null +++ b/2022/recent-new-habit/index.html @@ -0,0 +1,500 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 最近养成的新习惯 | 贾攀的流水账 + + + + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

贾攀的流水账

+ +
+

Panmax's Blog

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+ + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ 最近养成的新习惯 +

+ + +
+ + + + +
+ + +

又到了每个打工人都喜闻乐见的周五,我今天想聊一个关于我最近一个多月养成的一个习惯。

+

先介绍下背景,在工作之前,也就是上学期间,我的贴身衣服,比如内裤、袜子并没有换的很勤,平均三四天换一次。在工作后不管是内裤还是袜子,不管冬天还是夏天都是一天一换。

+

虽然卫生问题解决了,但并没有一天一洗,这些衣物我不使用洗衣机而是手洗,这就导致我要把换下来的衣服堆起来,等到没得换了或者周末的时候再统一去洗,这样会经常性出现资源紧张的情况,或者突然发现没得换了迫不得已再穿一天前一天的。这样还会导致的另一个问题是每次洗的时候工作量都很大,比如我要洗 5 条内裤、5 双袜子,每个内裤 3 分钟,每个袜子 2 分钟,这就要花去 25 分钟的时间。

+

最近我看了一本书叫《福格行为模型》,这本书不是完全讲习惯养成的,但也有一些习惯养成的内容,我通过这本书受到一些启发,使我最近养成了可以每天洗掉当日穿过的内裤和袜子的习惯。

+

这本书里提到一个概念:「我们在多数情况下没有去做一件事,并不是因为缺乏动机,而是缺乏一个很好的提示」。提示很重要,作者在书内将提示称作「锚点」。再早之前我读的另一本书《掌控习惯》里也有类似的概念,作者将之成为「触发器」。我有洗袜子的动机,但是每次脱下来时顺手堆起来忘记去洗,我需要给自己一个洗袜子的触发器,而且最好在合适的时机触发我。

+

我给自己设置的触发器是每晚洗澡淋浴开启的时候。不管什么季节我都会每晚冲个澡,每次冲澡开启淋浴后都需要一些时间等热水,我发现可以利用这个空挡用淋浴出来的凉水去洗衣服,这个时候只有手接触凉水也不会特别冷。我会快速用凉水把衣服湿润,然后打肥皂搓一搓,这个时候差不多就要出热水了,我拿着衣服一起站在水里,把衣服上残留的泡沫冲洗掉。

+

书中另一个观点是「先从小习惯开始、先从简单的习惯开始、从最紧迫的开始」。所以我并没有再一开始就洗内裤和袜子,因为我担心双倍的工作了会让我知难而退,一开始我只洗内裤,然后培养了三周洗内裤的习惯后把袜子也叠加上了。

+

我最近很长一段时间都是两条内裤和两双袜子换着穿,因为每天都会把当日的洗了,晾一天一夜肯定可以干。而且我也不再需要周末腾一块时间来干这个活了,之前也是由于堆的衣物太多想想就头大所以总不想去做。

+

我想我成功养成这个习惯的很重要的原因是我选择对了一个很合适的触发器,这个触发器的时间合适、场合合适,甚至还提供了我要养成习惯所需的资源(水)。

+

其实触发器这个概念我之前就一直在用,只是通过这本书我才知道我用了一个培养习惯合适的方法。比如我每天上大号的时间固定是早上洗漱完后,我会将坐在马桶上的这个事件作为一个触发器来学习英语,正好 10 分钟左右可以把当天要学的内容学完,我最近半年使用的「多邻国」,从而也避免了我在马桶上刷短视频的情况;我还将上地铁作为一个阅读触发器,上了地铁我就会掏出 Pad 或者纸质书来阅读。

+

这本书里还有个比较有趣的观点,我也在这里讲一讲吧。之前我一直将自己标榜为工具党,并且了解我的人也知道我工具党的习惯,就是说不管做什么我先把配套工具准备好、搞一套好装备或者时不时的折腾些工具,工具不限于实体工具和电子工具。这本书里提到「如果一种行为会让你感到沮丧,那它就很难成为习惯。从买一套好用的厨房刀具到准备一双舒适的运动鞋,借助任何工具都有可能让行为变得更容易做到」。这么看来工具党并没有错,为了给自己执行一个行为增添点乐趣,准备一套好的工具无可厚非。

+

比如我之前特别不喜欢拖地,每次拖地都要用手清理拖把上的很多毛发而且涮拖把的时候也很费力。前段时间我买了一个非常好用的拖把,拖把自带一个刷洗的桶,有两个槽,一个用来清洗拖把,另一个用来刮掉拖把上的水,清洗和刮水的时候就可以把上边粘的毛发刮下来,而且很干净,不用再去上手处理一次了,这就让我觉得拖地这件事没有那么困难。为了再增加些愉悦感,我会在拖地的水里放些「滴露」,不知道为什么我很喜欢闻滴露的味道。

+

再比如我现在玩的陆地冲浪板,我不确定如果几个月前我在刚学陆冲时买一块四五百块钱的普通板子现在还是不是这么有热情,我在刚学的时候就买了一块上等的板子,体验极佳,我现在对陆冲热情不减这块好板子有很大的贡献。

+

以上,关于工具党,在我看来并不是一件糟糕的事情,如果成为工具党后可以驱动自己把事情做下去,那就是有益的。

+

《福格行为模型》这本书里还有一些其他有用的内容,后边我会做一些摘抄单独再发一篇 blog。

+ +
+ + + + + + + +
+ + + + + + +
+ +
+ + + +
+ + + + + + + + +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2022/recommend-hongloumeng-zhipinghuijiaoben/20221119110205.png b/2022/recommend-hongloumeng-zhipinghuijiaoben/20221119110205.png new file mode 100644 index 0000000000..38f463768b Binary files /dev/null and b/2022/recommend-hongloumeng-zhipinghuijiaoben/20221119110205.png differ diff --git a/2022/recommend-hongloumeng-zhipinghuijiaoben/20221119110213.png b/2022/recommend-hongloumeng-zhipinghuijiaoben/20221119110213.png new file mode 100644 index 0000000000..5f1622e2f1 Binary files /dev/null and b/2022/recommend-hongloumeng-zhipinghuijiaoben/20221119110213.png differ diff --git a/2022/recommend-hongloumeng-zhipinghuijiaoben/index.html b/2022/recommend-hongloumeng-zhipinghuijiaoben/index.html new file mode 100644 index 0000000000..baa9a4f7b0 --- /dev/null +++ b/2022/recommend-hongloumeng-zhipinghuijiaoben/index.html @@ -0,0 +1,495 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 推荐一本红楼梦 -《红楼梦脂评汇校本》 | 贾攀的流水账 + + + + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

贾攀的流水账

+ +
+

Panmax's Blog

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+ + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ 推荐一本红楼梦 -《红楼梦脂评汇校本》 +

+ + +
+ + + + +
+ + +

好久没有更新博客了,前段时间忙于开发一个新 App,等正式上架后而且用户量还不错的话我再透漏相关信息吧。

+

今天想推荐一本红楼梦,可能有人会奇怪为什么是推荐一本红楼梦呢,红楼梦不是本来就是一本书吗?红楼梦有很多脂评本,每版脂评本上都有不同年代的人对红楼梦的指点。我要推荐这一版本叫《红楼梦脂评汇校本》,它把之前所有版本红楼梦中的评语汇集到了一起,边读原文边看古人写的评语仿佛在和古人对话,他们的评语也很朴实有些还很俏皮,偶尔还有剧透的情况,读一句原文看一句评语,非常好玩,也平添了几分乐趣。

+

比如下边截图中有个很贫嘴的评语,在第一回里提到宝玉的那块玉石是女娲补天剩下没有使用的那一块,评语抱怨到就因为这多出来的一块石头,生出来这么多鬼话,还不如把这块石头拿去补地,让地面平坦一些。有的评语就像相声里的捧哏,比如原文写石头对僧道说:「大师,弟子蠢物」,评语写到:「岂敢岂敢」,原文继续写:「弟子质虽粗蠢」,评语有写:「岂敢岂敢」,用现代的话说就是碎嘴子。

+

+

评语还会把文中的谐音梗做个解释,比如下边这一页中解释贾化就是「假话」,贾雨村是「粗言粗语」,胡州对应「胡诌」。

+

+

上边截图中还对香菱身世的那首诗做了注解,比如第二句「菱花空对雪澌澌」,暗示香菱后边要嫁给薛蟠,评语写到「生不遇时,遇又不偶」。诗的最后一句「便是烟消火灭时」,评语也写出这是为后文埋下的伏笔。

+ +
+ + + + + + + +
+ + + + + + +
+ +
+ + + +
+ + + + + + + + +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2022/revere-god/IMG_5717.jpeg b/2022/revere-god/IMG_5717.jpeg new file mode 100644 index 0000000000..151236bd30 Binary files /dev/null and b/2022/revere-god/IMG_5717.jpeg differ diff --git a/2022/revere-god/IMG_5741.png b/2022/revere-god/IMG_5741.png new file mode 100644 index 0000000000..2810dcdbd0 Binary files /dev/null and b/2022/revere-god/IMG_5741.png differ diff --git a/2022/revere-god/index.html b/2022/revere-god/index.html new file mode 100644 index 0000000000..5b27a29a07 --- /dev/null +++ b/2022/revere-god/index.html @@ -0,0 +1,496 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 敬畏上天的启示 | 贾攀的流水账 + + + + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

贾攀的流水账

+ +
+

Panmax's Blog

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+ + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ 敬畏上天的启示 +

+ + +
+ + + + +
+ + +

我是个无神论者,但是有时候也会在意一些外界给我启示,比如最近发生在自己身上的事。

+

众所周知,我在一个多月前开始玩陆冲,这周开始用陆冲代步上下班地铁站之间的通勤,为了轻装上阵就没有戴护具。

+

+

周三下班后,公司楼下的地面刚刚被擦地机器人打扫过,还有些潮湿,我当时注意到这个情况了,就比较小心单腿滑着走,结果还是打滑了,直接摔了个四仰八叉,不过伤的不重,扯了一下大腿,右手直撑地面的时候手腕顶了一下。当时我就在想要不要下次滑的时候戴上护具。是的,也只是想了一想,不然就不会有后文了。

+

昨天晚上,也就是周五下班出地铁后往家滑,天已经很黑了,路上有一块小半个砖头那么大的石头没有看到,板子直接冲了上去,石头卡住轮子,我也顺势飞了出去。正常来说陆冲是不用担心小石子的,因为它的轮子相对来说比较大,而且有可以容错的桥,但是那块石头太大了。

+

后果是把胳膊肘、膝盖擦破了,前两天刚刚顶过的手腕再次受到冲击,大拇指下边有个小软骨突了出来,一碰还很疼。我是由于板子突然停止,身体因为惯性飞了出去爬到地上的,手腕、胳膊、膝盖着地,但装在我背包里的玻璃饭盒还是被震的稀碎,可想而知力度有多大。不过最后还好,没有什么大碍,而且幸好是直着摔出去,落地的地方还是非机动车道,如果是斜着出去摔倒机动车道后果不堪设想,当时刚好有车在我旁边经过。

+

上天通过这种越来越严重的方式启示我注意安全、佩戴护具,我要敬畏他,以后出行一定要佩戴护具,不再抱有侥幸心理,而且我的护具也很漂亮,是一套复古风的护具。

+

+ +
+ + + + + + + +
+ + + + + + +
+ +
+ + + +
+ + + + + + + + +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2022/say-bye-to-set-upstream/20220818100224.png b/2022/say-bye-to-set-upstream/20220818100224.png new file mode 100644 index 0000000000..7db1b5bb72 Binary files /dev/null and b/2022/say-bye-to-set-upstream/20220818100224.png differ diff --git a/2022/say-bye-to-set-upstream/index.html b/2022/say-bye-to-set-upstream/index.html new file mode 100644 index 0000000000..a654256b21 --- /dev/null +++ b/2022/say-bye-to-set-upstream/index.html @@ -0,0 +1,497 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 告别 git push --set-upstream | 贾攀的流水账 + + + + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

贾攀的流水账

+ +
+

Panmax's Blog

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+ + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ 告别 git push --set-upstream +

+ + +
+ + + + +
+ + +

当我们使用 git 命令将本地新开分支的代码推到远端仓库时,需要先使用 --set-upstream 命令声明要推到远端的哪个分支,比如:

+
1
git push --set-upstream origin test && git push
+

当我们忘记使用 --set-upstream 时就会报如下错误:

+
1
2
3
4
5
git push

fatal: The current branch test-branch has no upstream branch.
To push the current branch and set the remote as upstream, use
git push --set-upstream origin test -branch
+

大多数情况下我们都是将本地同名分支推到远端仓库,那么有没有办法可以让我们在 push 时自动使用本地分支名作为远端的分支呢?当然有!

+

我们可以如下配置 git,将 push 到远端的分支名自动使用当前本地分支名:

+
1
git config --global --add push.autoSetupRemote true
+

如此配置后,就可以跟 --set-upstream 说再见了。

+

补充:git 官方文档对 pushautoSetupRemote 的介绍:https://git-scm.com/docs/git-config#Documentation/git-config.txt-pushautoSetupRemote

+

+ +
+ + + + + + + +
+ + + + + + +
+ +
+ + + +
+ + + + + + + + +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2022/sleepless-is-not-harmful/1.jpg b/2022/sleepless-is-not-harmful/1.jpg new file mode 100644 index 0000000000..d991a118c4 Binary files /dev/null and b/2022/sleepless-is-not-harmful/1.jpg differ diff --git a/2022/sleepless-is-not-harmful/index.html b/2022/sleepless-is-not-harmful/index.html new file mode 100644 index 0000000000..01ed5513ac --- /dev/null +++ b/2022/sleepless-is-not-harmful/index.html @@ -0,0 +1,519 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 睡眠少究竟有没有危害? | 贾攀的流水账 + + + + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

贾攀的流水账

+ +
+

Panmax's Blog

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+ + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ 睡眠少究竟有没有危害? +

+ + +
+ + + + +
+ + +

1.jpg

+

我在今年 2 月份的时候读了一本书叫《我们为什么睡觉》,作者提出了很多睡眠相关的研究成果,比如充足睡眠的必要性、失眠对大脑的永久不可逆损伤和睡眠少会大大提高人们犯错的概率等。本身我的睡眠就不是特别好,看这本书是想从这本书中找寻能让我睡得更好的方法,虽然这本书中也有涉猎,但篇幅不多,总的来说读完这本书后因为书中罗列的那些睡眠不足带来的负面影响,反而使我的睡眠压力更大了,但我不否认这是一本很好的科普书和畅销书。

+

但是这几天读到的 这篇文章 让我对睡眠这件事有了新的思考,这是一篇刷新率很高的文章,颠覆了我们大多数人之前对睡眠的认知。

+
+

刷新率这个词是我今天早上在地铁上读「写作是门手艺」这本书中新学到的,指的是读者看完你的研究后,想法改变了多少。

+
+

这篇文章认为睡眠少并没有我们常见科普文中描写的那么多危害,文中提到急性睡眠剥削,也就是突然减少睡眠,对健康有益,可以提升我们的睡眠效率。就我自己来说确实是这样,我在一个晚上失眠后,后边几天会睡得比较好,自我感觉睡眠效率也有挺高。

+

作者用断食来做对比,一些宗教和追求健康的人都会定期进行断食,睡眠少和断食一样,都会让我们有不适感,比如怕冷、注意力难以集中,但人们从来不认为断食是坏事,它能激发细胞的自噬,对我们的健康有利,同理「断」睡眠也不应该被认为是不好的。另一个对比是运动,我们运动后会出现肌肉疼痛和其他不适感,但这并不代表运动对我们有害。

+

作者还认为睡眠剥削并不会影响我们的认知能力,他还拿自己做过实验,作者尝试每天只睡 4 小时,然后正常上班,一周后询问他的同事有没有发现什么异常,他的同事们表示没有,作者本人也没找到任何变化。而且作者写这篇文章用了 38 个小时,期间只睡了 1.5 小时。

+

马斯克也曾经表达过自己曾每周工作 120 小时,剩余时间如果全拿来睡觉每天也只有 6.8 小时。

+

我自己感受到的情况也大致如此,在前一晚没睡或睡眠不足的情况下,对第二天的工作实际并没有什么影响,更多的是自己心理的不适感,多少次我在失眠的第二天上班,没有任何同事表达过我这天不对劲。

+

这篇文章作者认为睡眠少不仅不会减少寿命,反而会增加寿命,以每天睡 6 小时为例,每年可以增加 33 天生命、每 11 年可以增加 1 年生命、每 55 年增加 5 年生命。我也经常用这样的比喻开玩笑:我每天都能比别人多活几小时,看来是真的。应了中国那句老话:生前何必久睡死,后自会长眠。

+

我们祖先并没有我们这么好的睡眠环境,我们有事适宜的睡眠温度、柔软的床垫。一万年前我们的祖先睡在山洞里、小屋里或者天空下,周围有掠食者和敌对部落,所以他们不可能肆无忌惮的去睡觉,就像食物一样,虽然我们现在食物充足,但大家都知道应该避免暴食,但是对于睡眠却认为多多益善。

+
    +
  • 出现饥饿感是正常的,并不一定意味着你没吃饱。永远不饿只能说明我们吃得太多了。
  • +
  • 出现困倦感也是正常的,并不意味着你睡眠不足。从不犯困意味着我们睡得太多了。
  • +
+

作者甚至认为睡得太多更容易患抑郁症,现在确实在医疗界会用限制睡眠来缓解抑郁症,《我们为什么睡觉》这本书里也有介绍。

+

还有一个与我们之前认知向背的观点:作者认为记忆力的巩固并不需要睡眠,这点我也有体会,比如我前一天背了英语、Anki,在失眠一晚后的第二天再次复习那些内容时依然可以背下来,没有任何影响。

+

很多人(包括我自己)认为睡眠不足会影响第二天的情绪,但我觉得心理因素的影响比身体因素影响要大的多。我低落的原因大多是因为前一晚翻来覆去睡不着而恼火影响了第二天的情绪,回想一下初高中时候逃课去网吧通宵,第二天也没觉得情绪有啥影响。

+

说到逃课去网吧这里跑个题,我的初恋是高中同学,我俩确定关系是有次我们学校搞活动,搞到晚上 10 点多,她是走读生,我是住宿生,我知道她家离学校很远,坐公交要 1 小时,想要做次护花使者送她回家。我记得当时我们打了个车把她送到她家附近,这时候回学校已经进不了宿舍了,学校大门也进不了,索性我就没回去(也多亏了那个活动结束后宿舍比较混乱,没有查寝),我在她家附近找了个网吧玩了一晚上,当然我的另一个目的也是第二天早上能和她一起乘公交去学校,第二天早上我们在公交车上汇合,她给我带了一包牛奶,那一天是我的高光时刻,觉得世界上其他事情都不重要了,虽然前一晚没睡觉,但心情反而乐到极点,课上也睡得死去活来。

+

我在睡眠少的时候更容易亢奋,更容易在这一天发朋友圈或者写博客,我觉得这也符合我们祖先的特征:缺乏睡眠大多是因为周围有危险情况导致的,他们为了活下来需要更警觉。作者在文章中也补充了几件发生在其他人身上的轶事,其中有一个叫 Brian Timar 的提到:自己在本科阶段每次重大考试前一晚都不睡觉,第二天会超级兴奋和敏锐。

+
+

sleep 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:

+
    +
  • miserable, then
  • +
  • absurd and in a good humor, weirdly elated, then
  • +
  • Super PumpedTM, and
    really sharp when the test (or whatever) actually started.
  • +
+
+

很多人表达过睡觉多的孩子学习更好,我们是不是可以从另一个角度去解释这个现象:学生们睡觉多了学习时间就少了,他们需要集中精力去学习,这样的话效率自然就高了;那些睡觉少的学生因为时间多,所以做事总是磨磨蹭蹭,效率不高,效率不高更不容易学会,就不愿意学,这样就形成了负向螺旋下降。如果那些睡眠少的学生把大量时间都用在学习上我相信不会比其他学生差吧。

+

我大部分失眠情况可能都是因为担心失眠而失眠了,担心自己睡不好影响第二天的表现,读完这篇文章我觉得自己以后可以不这么紧张了,我要告诉自己:睡眠少可以让我更亢奋,发挥的可以更好。

+ +
+ + + + + + + +
+ + + + + + +
+ +
+ + + +
+ + + + + + + + +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2022/start-leetcode/20220825110115.png b/2022/start-leetcode/20220825110115.png new file mode 100644 index 0000000000..db524cd2ce Binary files /dev/null and b/2022/start-leetcode/20220825110115.png differ diff --git a/2022/start-leetcode/20220825110849.png b/2022/start-leetcode/20220825110849.png new file mode 100644 index 0000000000..875c143855 Binary files /dev/null and b/2022/start-leetcode/20220825110849.png differ diff --git a/2022/start-leetcode/index.html b/2022/start-leetcode/index.html new file mode 100644 index 0000000000..cfdcc5cadd --- /dev/null +++ b/2022/start-leetcode/index.html @@ -0,0 +1,503 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 开始刷 leetcode 了 | 贾攀的流水账 + + + + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

贾攀的流水账

+ +
+

Panmax's Blog

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+ + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ 开始刷 leetcode 了 +

+ + +
+ + + + +
+ + +

我从上个月 26 号开始每天做 1-2 道 leetcode 算法题,到今天刚好一个月时间。当时在 github 上建了个私有仓库,今天也公开了,其中一个目的也是为了每天督促自己。之前不想公开的原因是担心后边真的有面试的时候面试官认为我是个刷题选手,但是 whatever 无所谓了,反正短期内也没打算换工作

+

https://github.com/Panmax/go-leetcode

+

我从毕业刚工作开始面试就没有在刷算法题上下过功夫,觉得意义不是很大,到现在也是这么认为。

+

这次开始做题的目的是考虑到人过了 30,记忆力和思维力都不如年轻时候了,所以需要一些刻意练习,在这里记录一下做算法题的过程。每天写写题倒不是为了去面试,而是为了保持思维的敏捷,语法的熟练,以及对算法的理解。

+

做题的另一个契机是公司每隔一段时间会抽一些人去写三道算法题,评估下公司研发人员的平均水平,这给了我一个开始的提示。最近刚好在读一本书:《福格行为模型》,里边提到开始一个行为需要是三个要素:动机+能力+触发器。我的触发器是公司的水平测验,动机是提升自己思维敏感度,能力方面自己还是不错的,所以最终促成了这个行为的实施。

+

我不是每天疯狂的做新题,毕竟不是为了突击面试,而是每天做 1-2 道新题(根据题的难易程度而定),回顾 3-4 道之前的题目。所以在每个目录下可以看到一个 main.go 文件是我第一次做这道题的代码,其他 review_<日期>.go 文件是我复习的代码。

+

比如 206-reverse-linked-list 目录,意思是 leetcode 里的第 206 道题,我在 8 月 2 号、8 月 5 号、8 月 15 号复习过。

+
1
2
3
4
5
.
├── main.go
├── review_20220802.go
├── review_20220805.go
└── review_20220814.go
+

当天该复习哪道题是我通过 Anki 来记录的,Anki 根据我对每道题的掌握程度会在不同的时间点提醒我复习。

+

做题的顺序是找了一个 leetcode 组合好的题库,目前做的是这个库: https://leetcode.cn/problem-list/2cktkvj/

+

我每做一道题就在 Anki 里新增一道,同时给这个题标记不同的颜色,绿色为 easy、橙色为 middle、红色为 hard。

+

+

做完一道题后我会给这道题一个主观的难易评价,Anki 会决定我下一次的复习时机。

+

+

尝试了一个月,每天抽出来一小时写写题,目前已经转起来了,我相信自己可以一直做下去。

+ +
+ + + + + + + +
+ + + + + + +
+ +
+ + + +
+ + + + + + + + +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2022/static-site-very-fun/20220728185703.png b/2022/static-site-very-fun/20220728185703.png new file mode 100644 index 0000000000..328e095aaf Binary files /dev/null and b/2022/static-site-very-fun/20220728185703.png differ diff --git a/2022/static-site-very-fun/20220728185941.png b/2022/static-site-very-fun/20220728185941.png new file mode 100644 index 0000000000..0e05536c0b Binary files /dev/null and b/2022/static-site-very-fun/20220728185941.png differ diff --git a/2022/static-site-very-fun/20220728190704.png b/2022/static-site-very-fun/20220728190704.png new file mode 100644 index 0000000000..6aee39feba Binary files /dev/null and b/2022/static-site-very-fun/20220728190704.png differ diff --git a/2022/static-site-very-fun/20220728192004.png b/2022/static-site-very-fun/20220728192004.png new file mode 100644 index 0000000000..caa6d50316 Binary files /dev/null and b/2022/static-site-very-fun/20220728192004.png differ diff --git a/2022/static-site-very-fun/20220728192149.png b/2022/static-site-very-fun/20220728192149.png new file mode 100644 index 0000000000..e11dc488ad Binary files /dev/null and b/2022/static-site-very-fun/20220728192149.png differ diff --git a/2022/static-site-very-fun/index.html b/2022/static-site-very-fun/index.html new file mode 100644 index 0000000000..c059cb8cbb --- /dev/null +++ b/2022/static-site-very-fun/index.html @@ -0,0 +1,518 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 静态网站也很好玩 | 贾攀的流水账 + + + + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

贾攀的流水账

+ +
+

Panmax's Blog

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+ + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ 静态网站也很好玩 +

+ + +
+ + + + +
+ + +

最近几天用 Cloudflare 的 Pages 部署了几个纯静态的网站,也就是完全不需要后端,只需要 HTML+JS+CSS 驱动的网站,发现静态站也很强大,再配上十分方便的 Cloudflare 域名托管,秒级搭建起一个网站并关联上自己的域名,同时支持 HTTPS 访问。

+

itty-bitty

https://write.jiapan.me

+

匿名发布内容,不需要后端存储内容,而是把内容编码到 URL 上,比如打开这个链接 就可以看到我写的这段话了。

+

+

还可以给字体改颜色,加超链接、插入图片等富文本功能。

+

excalidraw

https://draw.jiapan.me/

+

一个制作手绘图的网站,功能很强大,自己部署的纯静态版本除了不支持多人协作,其他功能都是完整的。

+

不支持登录,你绘制的内容保存在你的浏览器里,只要不清浏览器内容,下次访问内容不会丢失。

+

+

password-generator

https://password.jiapan.me/

+

给一个你常用、好记的密码,并填写一个区分场景,这个工具会帮你转成一个强度很高的密码,每次你在输入密码时可以来这里转换出你的密码。

+

比如下图中,我生成了一个用于 QQ 登录的密码,下次登录 QQ 时来这里查我的密码是什么就可以了。密码是纯前端生成的,没有任何后端逻辑。

+

+

ddia

https://ddia.jiapan.me

+

ddia 那本书中文翻译的开源版本,没什么用,放在自己域名下就是好玩🤗。

+

+

Blog

最后我索性把自己当前这个博客也托管到了 Cloudflare 的 Pages,之前是把静态页放在一个海外的服务器上,前边用 Cloudflare 的 DNS 做一次代理,现在一身轻松了。

+

以上这些我都是让 Cloudflare Pages 关联了我 GitHub 上的 Repo,当那些 Repo 有更新这些网站也会跟着自动更新,对应的 Repo 分别是:

+ +

+ +
+ + + + + + + +
+ + + + + + +
+ +
+ + + +
+ + + + + + + + +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2022/storm-in-ward-select/index.html b/2022/storm-in-ward-select/index.html new file mode 100644 index 0000000000..105ad6e011 --- /dev/null +++ b/2022/storm-in-ward-select/index.html @@ -0,0 +1,495 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 《暴雨下载病房里》节选 | 贾攀的流水账 + + + + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

贾攀的流水账

+ +
+

Panmax's Blog

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+ + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ 《暴雨下载病房里》节选 +

+ + +
+ + + + +
+ + +

五月初的时候读了一本短篇小说集《暴雨下载病房里》,作者叫苏方,之前并没有读过她的其他书,这本书是在「小宇宙」中一档叫「文化有限」的节目听到的,感觉苏方的写作风格和王烁的有点像,都带着一股子痞劲。

+

这本书作者写于疫情期间,有些内容我们很有感触。不过我这里要节选的不是那些隔离的情结,而是一段情话,这段话我当时反复多了好几遍,作者一定是为心上人写过这样的话或者收到过这样的情书才能写出这样的句子吧。

+

下文摘自《暴雨下载病房里》的「十三封情书」一节:

+
+

我当然爱你,像白纸爱笔尖一样爱你,像空旷爱拥挤一样爱你,像海浪爱山崖一样爱你,像母亲爱她未来的孩子一样爱你。一只喜鹊哗啦啦飞来,站定在窗边,长尾巴一扫,又一扫,比往日多了神气,像质问:人呢?人到哪里去了?

+

我在家里,我一直在家里,可是我越来越不见了。我记得旧我,旧我自私多疑,虚伪虚荣,贪恋过去,贪图将来,独不把目前放在眼里。可原来只有目前,是我们不断在失去。

+

请你原谅,我爱你,这没什么了不起,你笑一笑吧。 只要是爱,就没什么不同,爱不争夺,爱也不等待,爱不抱希望,爱本来就是希望。

+

所以我不得不写,不得不记,我写下来只为告诉自己,却万不能告诉你。这爱是我的,这勇气和力量却来自你,我在你中看到我,已有无限的无限的感激。远远地有人拨琴,远远地有人哼唱,这就是伙伴,这就是人生的意义。

+

愿你们好。

+

愿这酒后呓语永不为人知晓。

+ +
+ + + + + + + +
+ + + + + + +
+ +
+ + + +
+ + + + + + + + +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2022/sublime-replace-multi/20220217054253.png b/2022/sublime-replace-multi/20220217054253.png new file mode 100644 index 0000000000..8705929a83 Binary files /dev/null and b/2022/sublime-replace-multi/20220217054253.png differ diff --git a/2022/sublime-replace-multi/20220217054701.png b/2022/sublime-replace-multi/20220217054701.png new file mode 100644 index 0000000000..71139943c6 Binary files /dev/null and b/2022/sublime-replace-multi/20220217054701.png differ diff --git a/2022/sublime-replace-multi/20220217055240.png b/2022/sublime-replace-multi/20220217055240.png new file mode 100644 index 0000000000..772f3861b1 Binary files /dev/null and b/2022/sublime-replace-multi/20220217055240.png differ diff --git a/2022/sublime-replace-multi/20220217055303.png b/2022/sublime-replace-multi/20220217055303.png new file mode 100644 index 0000000000..69f54c4dc9 Binary files /dev/null and b/2022/sublime-replace-multi/20220217055303.png differ diff --git a/2022/sublime-replace-multi/20220217055403.png b/2022/sublime-replace-multi/20220217055403.png new file mode 100644 index 0000000000..9978b48758 Binary files /dev/null and b/2022/sublime-replace-multi/20220217055403.png differ diff --git a/2022/sublime-replace-multi/20220217060046.png b/2022/sublime-replace-multi/20220217060046.png new file mode 100644 index 0000000000..3b82e50495 Binary files /dev/null and b/2022/sublime-replace-multi/20220217060046.png differ diff --git a/2022/sublime-replace-multi/20220217061456.png b/2022/sublime-replace-multi/20220217061456.png new file mode 100644 index 0000000000..0813c80f72 Binary files /dev/null and b/2022/sublime-replace-multi/20220217061456.png differ diff --git a/2022/sublime-replace-multi/index.html b/2022/sublime-replace-multi/index.html new file mode 100644 index 0000000000..2f2c3b60bc --- /dev/null +++ b/2022/sublime-replace-multi/index.html @@ -0,0 +1,528 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + sublime 实现多处文本替换 | 贾攀的流水账 + + + + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

贾攀的流水账

+ +
+

Panmax's Blog

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+ + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ sublime 实现多处文本替换 +

+ + +
+ + + + +
+ + +

最近接到一个任务,配合 DBA 进行数据库升级,其实也没有多大难度,就是将依赖这个库的服务配置文件中的 URI、端口号替换下就好了,但我们这个库由于历史原因依赖的服务非常多,有 30 多个,而且每个服务的配置文件可能都有些许差别,还有就是有可能采用了不同的连接池,每个配置文件中即便是同一个字段也可能会声明在多处,修改时要处理多处。

+

前期我使用人肉查找替换的方式来做这件事,但由于要做两轮上线,第一轮是先将配置文件中的从库修改后上线,第二轮再修改主库配置。

+

由于工作太机械化,所以我准备发挥程序员的最大美德:懒惰。

+

先来分析下任务,其实就是把文本中命中的多个字段进行替换,如:

+
    +
  • master.db.com 替换为 new.master.db.com
  • +
  • username 替换为 new_username
  • +
  • password 替换为 new_password
  • +
+

普通的文本编辑器只支持单个字段的替换,上边这种替换多个的情况需要人工手动进行多次操作。

+

我在一开始准备写个 Python 脚本,把每个服务的配置文件复制下来保存成文件,然后用脚本遍历这些文件,将里边的内容替换掉。

+

分析后觉得有些用杀鸡用宰牛刀,不能拿着锤子找钉子,于是就想探索下 sublime 中有没有类似的插件可以实现这个需求。于是就找到了这个 RegReplace 插件。

+

下面我记录下我使用这个插件的过程:

+
+

这里避免数据敏感,我用个其他例子做为演示:将一个文档中的 <p></p> 替换为 <h1></h1>

+
+

安装

command+shift+p,输入 install

+

20220217054253.png
从列表中搜索 RegReplace 回车安装就可以了。

+

自定义替换配置

20220217054701.png

+

编辑上边打开的配置文件,添加以下配置:

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
{
"replacements": {
"replace_opening_ps": {
"find": "<p>",
"replace": "<h1>",
"greedy": true,
"case": false
},
"replace_closing_ps": {
"find": "</p>",
"replace": "</h1>",
"greedy": true,
"case": false
}
}
}
+

估计一眼就能看明白,replace_opening_ps<p> 替换为 <h1>replace_closing_ps</p> 替换为 </h1>

+

自定义触发命令

20220217055403.png

+

填入:

+
1
2
3
4
5
6
7
8
9
10
11
12
[
{
"caption": "Reg Replace: Replace P to H1",
"command": "reg_replace",
"args": {
"replacements": [
"replace_opening_ps",
"replace_closing_ps"
]
}
}
]
+

也是一看就懂,这里不多解释了。

+

使用

    +
  1. 准备一段文本:
  2. +
+
1
2
<p>hello,world</p>
<p>你好,世界</p>
+
    +
  1. command+shift+p 输入 replace ps 定位到我们配置的命令上,回车即可完成多处替换工作:
  2. +
+

20220217061456.png

+

替换后的效果如下:

+
1
2
<h1>hello,world</h1>
<h1>你好,世界</h1>
+

现在时间凌晨 5.35 分,先写到这,准备去切换主库了。

+ +
+ + + + + + + +
+ + + + + + +
+ +
+ + + +
+ + + + + + + + +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2022/surge-soft-router-perimeter/20220606130430.png b/2022/surge-soft-router-perimeter/20220606130430.png new file mode 100644 index 0000000000..f0aea071bb Binary files /dev/null and b/2022/surge-soft-router-perimeter/20220606130430.png differ diff --git a/2022/surge-soft-router-perimeter/20220606130449.png b/2022/surge-soft-router-perimeter/20220606130449.png new file mode 100644 index 0000000000..352c223897 Binary files /dev/null and b/2022/surge-soft-router-perimeter/20220606130449.png differ diff --git a/2022/surge-soft-router-perimeter/Snipaste_2022-06-06_08-05-18.png b/2022/surge-soft-router-perimeter/Snipaste_2022-06-06_08-05-18.png new file mode 100644 index 0000000000..80f90b4c10 Binary files /dev/null and b/2022/surge-soft-router-perimeter/Snipaste_2022-06-06_08-05-18.png differ diff --git a/2022/surge-soft-router-perimeter/Snipaste_2022-06-06_08-05-24.png b/2022/surge-soft-router-perimeter/Snipaste_2022-06-06_08-05-24.png new file mode 100644 index 0000000000..f3d14965d8 Binary files /dev/null and b/2022/surge-soft-router-perimeter/Snipaste_2022-06-06_08-05-24.png differ diff --git a/2022/surge-soft-router-perimeter/Snipaste_2022-06-06_08-06-02.png b/2022/surge-soft-router-perimeter/Snipaste_2022-06-06_08-06-02.png new file mode 100644 index 0000000000..046b00a078 Binary files /dev/null and b/2022/surge-soft-router-perimeter/Snipaste_2022-06-06_08-06-02.png differ diff --git a/2022/surge-soft-router-perimeter/Snipaste_2022-06-06_08-13-03.png b/2022/surge-soft-router-perimeter/Snipaste_2022-06-06_08-13-03.png new file mode 100644 index 0000000000..df13155a4b Binary files /dev/null and b/2022/surge-soft-router-perimeter/Snipaste_2022-06-06_08-13-03.png differ diff --git a/2022/surge-soft-router-perimeter/index.html b/2022/surge-soft-router-perimeter/index.html new file mode 100644 index 0000000000..25244177e5 --- /dev/null +++ b/2022/surge-soft-router-perimeter/index.html @@ -0,0 +1,534 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 在 Mac 上 使用 Surge 做旁路由的周边搭配 | 贾攀的流水账 + + + + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

贾攀的流水账

+ +
+

Panmax's Blog

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+ + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ 在 Mac 上 使用 Surge 做旁路由的周边搭配 +

+ + +
+ + + + +
+ + +

这段时间在家办公,家里的网络无法进行科学上网,不过我的所有设备上都装有 Surge,所以科学上网这件事对我倒是没太大影响,但有一个不方便的点是在需要访问公司内网的一些资源时(比如 Gitlab、有敏感数据的后台),需要先连接上 EasyConnect(即 VPN) 才可以访问。我平时使用 Surge 习惯开启「增强模式」,这样 Surge 可以接管我的全部网络,就不用再在一些软件中单独配置代理了,比如 iTerm、GoLand、Telegram。不过 EasyConnect 和 Surge 的「增强模式」有冲突,在开启「增强模式」时是无法使用 EasyConnect 的,每次都要先停用「增强模式」才行。具体冲突的原因在原理上我不是很清楚,我猜测是因为它们两个都是要接管所有网卡流量导致的。

+

为了解决这个问题,也为了让家里所有设备都能实现无感知地科学上网,同时还可以做一些广告屏蔽和隐私保护,我准备使用我那台早已配淘汰了的 15 年 13 寸 Mac 做一个软路由。我所参考的教程是:https://qust.me/post/MacSurgeRouter/ ,博主还非常贴心的录制了视频:https://www.youtube.com/watch?v=68lcT7ItyP4 。不管是文章还是视频,都将如何配置软路由介绍的非常详细了,我在本文中补充几点配置好后我们还可以做的那些辅助工作。

+

让 Mac 合盖后不休眠

我一直使用 Amphetamine 这个小工具来让我的电脑在我需要的时候保持不休眠状态。

+

如果我们需要合盖后继续保持让设备不休眠,需要取消掉「当显示器关闭时允许系统休眠」的选项。

+

+

在取消这个选项时,Amphetamine 会提醒我们安装一个增强工具(Amphetamine EnHancer),用来保护我们的电脑,这里我也建议安装,安装后需要将增强工具内置的两个组件也要安装上才算启用成功。

+

+

设备断网和低电量提醒

由于我的 13 寸 Mac 和公司配的 15 寸 Mac 电源适配器相同,所以我经常会让两个设备使用同一个电源,哪个没电了充哪个——我日常都是用自己的M1 Pro 所以不会太频繁给公司电脑充电,用一个电源足够。由于作为软路由的 13 寸 Mac 长时间处于合盖状态,我不知道它的剩余电量,有一次充 15 寸 Mac 后忘了充回去,导致晚上设备因没电关机了。我一开始只是发现手机无法连上 WIFI,以为是信号弱,但是到路由器旁边依然连不上,后来才想到是电脑关机了。因为电脑接管了 DHCP 服务,手机分配不到 IP 自然无法连上。

+

为了避免这种情况,同时也为了避免不小心报错网线,我简单写了一个监控脚本来监控设备的状态。

+

脚本实现功能如下:

+
    +
  1. 判断网络状况:定期 ping 一个地址,这个地址我跑在 AWS 的 Lambda 上,服务收到请求后记录最新请求时间,服务自身也会有定时任务检查上一次请求时间和当前时间的差值,如果超过一个阈值,则通过 Bark 给我发个推送。
  2. +
  3. 监控设备电量:当低于某个阈值时通过 Bark 提醒我。
  4. +
+

可以看出这个监控需要两组脚本,一个是部署在 Lambda 上的,用来判断设备网络情况,另一个是客户端本地用来 ping 服务和监控电量。

+

先来看启动在 Lambda 上的 handler:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
def mac_health(event, context):  
update = False
if event.get('queryStringParameters') and event.get('queryStringParameters').get('update'):
update = True

t = time.time()
ts = int(t)

if update: # 我的Mac发送的携带 ?update=1 的请求,只更新最新时间
with open('/tmp/now.txt', 'w') as f:
print("====")
f.write(str(ts))
print(ts)
else: # 有 cronjob 触发的请求,只判断健康状态
try:
f = open('/tmp/now.txt', 'r')
tmp_ts = f.read()
if tmp_ts:
tmp_ts = int(tmp_ts)
if ts - tmp_ts > 3 * 60:
print("no heart beat")
requests.get('https://api.day.app/YOUR_KEY/' + '请注意:你的Mac离线了' + '/' + '请检查网络状态')
else:
print("status health")
print(tmp_ts)
f.close()
except Exception:
print("read error")
with open('/tmp/now.txt', 'w') as f:
print("====")
f.write(str(ts))
print(ts)
return {
"statusCode": 200,
"body": json.dumps(
{
"time": ts
}
)
}
+

serverless.yml:

+
1
2
3
4
5
6
7
8
functions:
mac_health:
handler: handler.mac_health
events:
- schedule: cron(*/2 * * * ? *)
- http:
path: mac_health
method: get
+

这里我不再介绍 Lambda 如何使用,可以参考我之前的其他文章:

+ +

上边代码我做个补充说明:

+
    +
  • Lambda 是完全无状态的服务,而且随时有可能被 kill 然后启动在其它实例上,所以我们最好不要将最后 ping 时间记录在内存中,如果也不想借助外部存储的话,我们可以借助 /tmp 目录来临时存储这个数据,AWS 为每个 Lambda 提供 500MB /tmp 下的存储空间。
  • +
  • 我在用 Mac 访问 Lambda endpoint 的时候会带上 ?update=1 这个参数,这样就可以把最新时间记录下来,而系统通过 cronjob 调用自己的时候不记录时间,只进行时间差判断。
  • +
+

再来看下Mac断的脚本:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import os
import time
import requests

if __name__ == '__main__':
while True:
try:
res = requests.get("https://xxxxxxxx.execute-api.ap-east-1.amazonaws.com/dev/mac_health?update=1")
except Exception:
print("requests error")
time.sleep(10)
continue
print(res.status_code)
battery = os.popen('pmset -g batt | grep -Eo "\d+%" | cut -d% -f1').read()
battery = int(battery)
print(battery)
if battery < 50:
requests.get('https://api.day.app/YOU_KEY/' + '请注意:你的Mac电量过低' + '/' + '请检查电源状态,剩余电量: %d%%' % (battery,))
else:
print("ok")
time.sleep(60)
+

逻辑也比较简单,这里是通过一个系统 shell 调用来获取的电量。

+

将电脑电源拔掉,把报警阈值调到 97 的效果:

+

拔掉网线后的效果:

+

将最大充电量设置为 70%

电脑像这样长期插着电源并保持开机状态对电池的损耗非常大,为了避免对电池损耗过快,建议将最大充电量设置为 60%-80% 之间,我们可以借助 bclm 这个小工具实现,工具名是 Battery Charge Level Max 的缩写。安装方式参考官方文档,我自己是将二进制包下下来放到 bin(/usr/local/bin) 目录来直接使用的。

+

bclm 提供命令非常简单:

+
1
2
3
4
5
6
7
8
# 获取当前电池的最大充电量
bclm read

# 设置最大充电量,一定要在最前边加 sudo
sudo bclm write 70

# 持久化最大充电量设置,避免覆盖和重启失效
sudo bclm persist
+

+

当前我的剩余电量是 84%,通过其他监控工具可以看到即使我插上充电器,也不会进入充电状态:

+

+

(电池已经要不行了 👋🏻)

+

最后

软路由用了两周多了,给我最明显的体验是,电视上之前每次开机都要等待的 60 秒广告没有了;因为在我的工作 Mac 上已经不用再运行 Surge,在连公司网络 VPN 时也不用先去关闭 Surge 的 「增强模式」了。

+

也存在不方便的地方,我在外边使用手机,通过流量上网的时候要手动打开 Surge,而到家连上 WIFI 后又要手动关闭,如果忘记操作就会有打不开网站的情况。不知道能否通过快捷指令将个操作自动化,我目前还没有找到自动化解决方案。

+ +
+ + + + + + + +
+ + + + + + +
+ +
+ + + +
+ + + + + + + + +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2022/system-design-cache/20220830134522.png b/2022/system-design-cache/20220830134522.png new file mode 100644 index 0000000000..9c7fcb4b7d Binary files /dev/null and b/2022/system-design-cache/20220830134522.png differ diff --git a/2022/system-design-cache/20220830134809.png b/2022/system-design-cache/20220830134809.png new file mode 100644 index 0000000000..81b6c72b30 Binary files /dev/null and b/2022/system-design-cache/20220830134809.png differ diff --git a/2022/system-design-cache/20220830135724.png b/2022/system-design-cache/20220830135724.png new file mode 100644 index 0000000000..051ff8e519 Binary files /dev/null and b/2022/system-design-cache/20220830135724.png differ diff --git a/2022/system-design-cache/20220830135848.png b/2022/system-design-cache/20220830135848.png new file mode 100644 index 0000000000..6715c18fa9 Binary files /dev/null and b/2022/system-design-cache/20220830135848.png differ diff --git a/2022/system-design-cache/20220830141332.png b/2022/system-design-cache/20220830141332.png new file mode 100644 index 0000000000..9aeb208fec Binary files /dev/null and b/2022/system-design-cache/20220830141332.png differ diff --git a/2022/system-design-cache/20220830141357.png b/2022/system-design-cache/20220830141357.png new file mode 100644 index 0000000000..fb7ca061c7 Binary files /dev/null and b/2022/system-design-cache/20220830141357.png differ diff --git a/2022/system-design-cache/index.html b/2022/system-design-cache/index.html new file mode 100644 index 0000000000..b348a45833 --- /dev/null +++ b/2022/system-design-cache/index.html @@ -0,0 +1,564 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 日拱一卒 - 缓存 | 贾攀的流水账 + + + + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

贾攀的流水账

+ +
+

Panmax's Blog

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+ + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ 日拱一卒 - 缓存 +

+ + +
+ + + + +
+ + +
+

“计算机科学只存在两个难题:缓存失效和命名。” ——Phil KarIton

+
+

20220830134522.png

+

缓存的主要目的是通过减少对底层慢速存储层的访问,提高数据的检索性能,以空间换取时间,缓存通常是临时存储一个数据的子集,而数据库中的数据通常是完整且持久的。

+

缓存利用了「最近被请求的数据很可能再次被请求」的原则。

+

缓存和内存

与计算机的内存类似,缓存是一种紧凑的、高性能内存,它以层的方式存储数据,从第一层开始,依次递进,这些层被标记为 L1、L2、L3……,依此类推。在需要时,缓存可以写入数据,比如在更新场景下,新的内容需要写入到缓存中替换掉旧内容。

+

无论读缓存还是写缓存,都是一次执行一个块。每个块都有一个标签,这个标签表示数据在缓存中的存储位置。当从缓存中请求数据时,会通过标签进行搜索,首先在第一层(L1)内存中搜索,如果没有找到,就会在拥有更多数据的 L2 中进行搜索。如果在 L2 中也没有找到数据,就继续在 L3 搜索,然后是 L4,以此类推,直到找到数据为止,然后读取并加载数据。如果在缓存中没有找到数据,那么就把它写进缓存中,以便下次快速检索。

+

缓存命中和缓存缺失

缓存命中

「缓存命中」描述的是内容成功从缓存中找到的情况。

+

标签在内存中快速查询,当数据被找到并成功读取时,我们称之为「缓存命中」。

+

冷、温、热缓存

缓存命中还可以区分为冷、温、热,不同情况表示不同的数据读取速度。

+

「热缓存」是指以最快的速度从内存中读取数据的情况,发生在数据从 L1 检索的时候。

+

「冷缓存」是以最慢的速度读取数据,尽管如此,它仍然是成功从缓存中读出的(数据只是在内存层次中的较低位置被发现,比如在 L3,或者更低的位置),所以仍然被认为是一次缓存命中。

+

「温缓存」是用来描述在 L2 或 L3 找到数据的情况。温缓存没有热缓存那么快,但要比冷缓存快。一般来说,称一个缓存为温缓存是用来表达它比热缓存慢,更接近于冷缓存。

+

缓存缺失

「缓存缺失」指的是在搜索内存时没有找到数据的情况。当这种情况发生时,内容会被转移并写入缓存。

+

缓存失效

「缓存失效」是一个过程,计算机系统将缓存项声明为无效,并将其删除或替换。如果数据被修改了,就应该在缓存中失效,否则会造成应用行为的不一致。

+

有三种缓存系统:

+

写入式缓存(Write-through)

20220830134809.png
数据同时被写入缓存和相应的数据库中。

+

优点:快速检索,缓存和存储之间的数据完全一致。

+

缺点:写操作的延迟较高。

+

绕写式缓存(Write-around or Write-aside)

20220830141332.png

+

直接写到数据库或永久存储,绕过缓存。

+

优点:可以减少写操作延迟。

+

缺点:增加了缓存失效。在缓存失效的情况下,缓存系统必须从数据库中读取信息。因此,在应用程序快速写入和重新读取信息的情况下,这可能导致更高的读取延迟。读取发生在较慢的后端存储中并经历较高的延迟。

+

回写缓存(Write-back or Write-behind)

20220830141357.png

+

只对缓存层进行写入,一旦写入缓存完成,就会确认写入。之后,缓存异步地将这个写入同步到数据库中。

+

有点:降低写密集应用的延迟并提高吞吐量。

+

缺点:在缓存层崩溃的情况下,存在数据丢失的风险。我们可以通过让一个以上的副本确认缓存写入成功改善这个问题。

+

淘汰策略

以下是一些最常见的缓存淘汰策略:

+
    +
  • 先入先出(FIFO),缓存优先淘汰最早访问的项,而不考虑它之前被访问的频率或次数。
  • +
  • 后进先出(LIFO),缓存优先淘汰最近访问的项,而不考虑它之前被访问的频率或次数。
  • +
  • 最近最少使用(LRU),优先淘汰最近使用最少的项。
  • +
  • 最近使用(MRU),与 LRU 相反,首先淘汰最近使用的项。
  • +
  • 最不经常使用(LFU),计算一个项的使用频率,优先淘汰那些使用频率最低的项。
  • +
  • 随机淘汰(RR),随机选择一个候选项,必要时淘汰它以腾出空间。
  • +
+

分布式缓存

20220830135848.png

+

分布式缓存是一种系统,它将多台联网计算机的随机存取存储器(RAM)集中到一个单一内存数据存储中,用作数据缓存,以提供对数据的快速访问。

+

虽然传统中的大部分缓存都在一个物理服务器或硬件组件中,但通过将多台计算机连接在一起,分布式缓存可以超越单个计算机的内存限制。

+

全局缓存

20220830135724.png

+

顾名思义,我们将有一个单一的共享缓存,所有的应用节点都访问这个缓存。当请求的数据在全局缓存中找不到时,缓存负责从底层数据存储中查找缺失的数据。

+

使用案例

在现实世界中缓存有许多使用场景,如:

+
    +
  • 数据库缓存
  • +
  • 内容分发网络(CDN)
  • +
  • 域名系统(DNS)缓存
  • +
  • API 缓存
  • +
+

什么时候不使用缓存?

我们也来看看在哪些情况下不应该使用缓存:

+
    +
  • 当访问缓存的时间和访问主数据存储的时间一样长时,缓存就没有用了。
  • +
  • 当请求的重复性较低(随机性较高)时,缓存的作用就不大了,因为缓存的高性能来自于重复的内存访问。
  • +
  • 当数据经常变化时,缓存没有帮助,因为当缓存版本不同步时就需要访问主数据存储。
  • +
+

另外需要注意的是,缓存不应该被用作永久的数据存储。大部分缓存都是在易失性内存中实现的,因为它的速度更快,因此缓存应该被认为是临时性的。

+

优点

以下是缓存的几个优点:

+
    +
  • 提高性能
  • +
  • 减少延时
  • +
  • 减少数据库的负载
  • +
  • 降低网络成本
  • +
  • 增加读吞吐量
  • +
+

实例

下面是一些常用的缓存技术:

+ + +
+ + + + + + + +
+ + + + + + +
+ +
+ + + +
+ + + + + + + + +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2022/system-design-cdn/20220902121333.png b/2022/system-design-cdn/20220902121333.png new file mode 100644 index 0000000000..b81d1fdec9 Binary files /dev/null and b/2022/system-design-cdn/20220902121333.png differ diff --git a/2022/system-design-cdn/20220902121342.png b/2022/system-design-cdn/20220902121342.png new file mode 100644 index 0000000000..8950c58891 Binary files /dev/null and b/2022/system-design-cdn/20220902121342.png differ diff --git a/2022/system-design-cdn/index.html b/2022/system-design-cdn/index.html new file mode 100644 index 0000000000..c02eea6ed5 --- /dev/null +++ b/2022/system-design-cdn/index.html @@ -0,0 +1,514 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 日拱一卒 - 内容分发网络(CDN) | 贾攀的流水账 + + + + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

贾攀的流水账

+ +
+

Panmax's Blog

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+ + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ 日拱一卒 - 内容分发网络(CDN) +

+ + +
+ + + + +
+ + +

内容分发网络(CDN)是一组在地理上广泛分布的服务器,它们一起工作以提供互联网内容的快速交付。通常静态文件,如 HTML/CSS/JS、照片和视频,都是由 CDN 提供的。

+

20220902121333.png

+

为什么使用 CDN?

内容分发网络(CDN)增加了内容的可用性和冗余度,同时降低了带宽成本并提高了安全性。通过 CDN 获取内容可以显著提高性能,因为用户从靠近他们的数据中心接收内容,我们的服务器也不必为 CDN 满足的请求提供服务。

+

CDN 是如何工作的?

20220902121342.png

+

在 CDN 中,源服务器包含内容的原始版本,边缘服务器分布在世界各地并且数量众多。

+

为了最大限度地减少访问者与服务器之间的距离,CDN 将其内容的缓存版本存储在多个地理位置,称为边缘位置。每个边缘位置包含一些缓存服务器,负责向其附近的访问者提供内容。

+

一旦静态资源被缓存在特定位置的所有 CDN 服务器上,之后所有网站访问者对静态资源的请求都将由这些边缘服务器提供(而不是源站),从而减少源站负载并提高可扩展性。

+

假如一名位于英国的用户请求本站,本站服务器当前托管在美国,他们将从最近的边缘位置(如伦敦)获得服务。这比让访问者向源站服务器发出完整的请求要快得多,后者会增加延迟。

+

类型

CDN 通常分为两种类型。

+

推模式(Push)

当服务器上内容发生更改时,使用了推模式的 CDN 会收到新内容。我们完全负责提供内容,直接上传至 CDN,并重写 URL 以指向 CDN。我们可以配置内容何时过期、何时更新。内容只有在新的或改变的时候才会被上传,最大程度地减少流量,最大限度地提高存储。

+

流量小的网站或内容不经常更新的网站使用推模式效果很好。内容被放置在 CDN 上一次,而不是定期被重新拉取。

+

拉模式(Pull)

在拉模式的情况下,缓存是根据请求更新的。当客户端发送一个要求从 CDN 获取静态资源的请求时,如果 CDN 中没有,那么它将从源站服务器获取新的资源,并用这个新资源填充其缓存,然后将这个新的缓存资源发送给用户。

+

与推模式 CDN 相反,拉模式需要较少的维护,因为 CDN 节点上的缓存更新是基于客户端对源站服务器的请求进行的。流量大的网站使用拉模式效果很好,因为流量分散地更均匀,只有最近请求的内容留在 CDN 上。

+

缺点

    +
  • 额外费用:使用 CDN 可能是昂贵的,特别是对于高流量的服务。
  • +
  • 限制:一些组织和国家已经封锁了流行的 CDN 的域名或 IP 地址。
  • +
  • 地点:如果我们的大部分受众位于没有部署 CDN 服务器的国家,他们访问我们网站上的数据,可能比不使用任何 CDN 的情况下延迟更高。
  • +
+

例子

以下是一些广泛使用的 CDN:

+ + +
+ + + + + + + +
+ + + + + + +
+ +
+ + + +
+ + + + + + + + +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2022/think-change-a-environment/index.html b/2022/think-change-a-environment/index.html new file mode 100644 index 0000000000..62ea1bc4ba --- /dev/null +++ b/2022/think-change-a-environment/index.html @@ -0,0 +1,500 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 是不是该考虑换个环境了? | 贾攀的流水账 + + + + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

贾攀的流水账

+ +
+

Panmax's Blog

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+ + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ 是不是该考虑换个环境了? +

+ + +
+ + + + +
+ + +
+

本文使用了 emoji 对部分内容进行了加密,可以看我这篇文章了解详情。

+
+

马老师说过,离职无非两个原因:1、钱,没给到位;2、心,委屈了。

+

最近读到 MacTalk 的一篇文章,这篇文章将离职原因分成了三点,前两点是把马老师的第二点做了个个拆分:

+
    +
  • 第一,你确实在这公司没成长了,你每天在空转,你在消耗自己,你本是一把利剑,现在快被磨成废铜烂铁了。
  • +
  • 第二,你讨厌所在团队的人文环境,大家做一些看起来无厘头、很可笑的事情,这已经影响了你的身心健康。
  • +
  • 第三,工资和实力不匹配,沟通无果,无法共鸣。
  • +
+

最近遇到一些烦心事,所以考虑是不是该换个有利于身心健康的环境了,我有些厌恶现在的团队文化,不喜欢每天被推着做事情,先列举下这些让我不舒服的事吧:

+

😸😸🙂🙃🙆😷👵👴👕👺👤😵🙅😴😲👯👚👧👒👡👚👲😫👵👵🙇👏😲👵👕🙃👰👓👤👯👴👫👫🙉👸😷👥🙆👣👷👒👶👙😶😶👐👕😶👪😸👶👥🙎🙇😯😯👦👣👌👗👱👶👒👰👪😷👙👭👕👭👬👫😲👨😴🙄👮👨😸🙂👦👐👪👩🙅🙂👘🙈👨👐🙁👘👴👬👦👤👵👸😱👮👘👶🙁🙉👡👔😳👬👚🙋👧👗😰👴👹👡😫😷👪👲😰😳😲👙🙋🙆👯👴👥🙊👗👙🙂👶👭👏😫👦👢😱😳😵👥👵😷🙎👣👖😳🙎🙅👳👹👧👖👏😷👏👱👡😱👙👶👌😯😱😱😹🙇👏🙄👵👺🙊🙍🙆👨😳👌👬👓😸👥👰🙅👳😴😳😫👷👩👗😳😶👱👫👮🙃👓👨🙇👓👮👘😯👗👐👺🙁👚👓👷👬🙁🙉😲👩👮🙇👭🙄👰👒🙃🙄😲😵😫👔👵👧🙃👧👮👑👑👺😹👳😲👤🙆🙂🙋👷👓👦🙂🙃😳👮😳👩😷😲👙👧👓🙋👬👙👯👮👢👔🙋👮👹👳👩😰👔👵👒🙍👤😳👌👯👸👷👫👒😯👗😸👧😯😶👯👌👲👶😲🙃🙃👘👚😷👦👕😸👬👣👙🙆👧😷🙍👚🙁👫👚👕👏👰👯👵😶🙅😫👙😷😰👶🙇👴🙍👓👥😳😰👺👨😱🙋👧👺😵🙉👌🙈😯🙄😸👖👤👪👭👚👡👲🙉😹👚👒👐👏😲👵👴👬😵👵👨👮👬👤😵👘🙈🙁😫🙅😯👢😹🙂👕🙃😴👦👺👨😯👣👤👯👧👐👡👡👫🙈🙁👹👫🙄👏😯👣😰👯👓👔😹👵😰😸👨🙄🙂😳👘🙂👌🙃😴👹🙃👑🙅👬👨🙉🙊👢😴😹👨👘👖👧😷👴😴😯🙁👡👹👏🙁👰🙈👴😳👫🙈😵👥🙆😳👱👙😸👲🙈👪👘👩👷😴👧👷👒👲👲😸👷👥👒👖👮🙋👤😰😸👒👵👫😲🙂👵👑👣👥👭😳👺🙁👱👦👸🙍👷🙇👡🙆🙂👚👘😱😴😫👌👚👳🙉😶👒😲👥😵👌👶😵👸👬👗😹👪😫🙍👑👓👤👩👙👬👨👚🙎😱🙇🙉😵🙉😲🙂🙍👑👬👖😷👲🙅👚👬😫👬👰👫👡👹😶🙂👵👡😵🙄👱🙂👌👌👰🙃👶👲🙁👕😲🙍👯😶😷🙎👑🙈👏👖👕😯🙁😳👑🙍😳😷😹👲👵👧🙈🙋👶👱👏😫😵😯🙆👑👐🙋🙉👬👗🙃👱👦👺🙊👩👐👲👏👔👌🙈👙🙋👚👺👒👭👳👥😸👯🙆👡👶👺🙁🙃👚😶👓👩👡👘👏👏👰😶👏😳🙆👘🙂👶😯👳👳👥👥👡👦👳👓😸👑👯👲👬👌👤👔🙃👓🙆👕👳👖👡👱😳👴🙁👶👔👌👖😱😲🙅👏👌😵👌😲😫🙋👯👨🙁👗😵😶👫👚🙂👏👢🙅👺🙈👮😷🙃👚👸👘😳🙇🙄👒👺👨👭👱🙇😵👖🙍👢👗👏👚👭🙍👕😫👣😵🙍👦👱👓👷👓😸👶👧👳😯👐😷👓🙉😹👸👐👕😸👣🙉🙊😸👚👨🙁👣🙋👫👸👬👣👒👵😶😸😹😲👗👑👲👚👶👏👲😹👱👡👑🙊👲👬😴👡👓👰👵👮👖😶👸👐👹😷👭😷👵👳🙄🙇👢👚👰🙈🙉👯👥👡👭👗😸👙👩😶👭👸👗😶👸👹👑🙈👥🙉🙂😳🙊👸🙈🙄👔👚🙅👹😳👴👒👏👏😹👗👲👩👤👮👨👮👯🙂👵👥🙇😳👴😵👺👚😱👖👦👷😹👤🙋👗👱👪👑🙆👯👵😫👤😰🙂😵👯👕👮👸👹👹👕😷👡👧😴👷😹👷🙄👺👺👏👏👘👷👤👧👓😶👶👳🙍🙋👥👥👕😴😲👵🙆👥👳🙅👡🙋😷🙇👪🙄👚👥👦🙉🙎🙂🙎👑👺👖😷👤👙👘👪👥😰👢👚👹🙇👤🙄👗🙉😱🙇😫👨🙅😴🙊👮👨👳👣😰👔😱👹😫👮😷👖👡👐🙋🙁👳👴👚👪👏👓🙃😵👣😲👌👰🙅👖😴👥👳😴👘👔😴👙👫👺👐👙👦👑🙆🙊👗🙆👘🙃👺👒👓👥👧👐👺🙂😱👢🙉🙇👡😰😱👳👳👒🙆👭🙃👐👳👨😷👗👥😫👖👸🙎🙃👘👖🙆😴😰👨👦👨😷😹😷👕👩🙎👐😯😰👖👤👱👹👸👮😴👧👤😲👸🙎🙍👔👶👐👳👌👖👺👺👷👘👒👡👥👕😶😱🙋👏🙎👺😯🙁🙁👣👢😰👡👰😱😯😸👶🙂😳😴👏👙👮👐🙇👴👨👶😷👴😵👷😲👪🙆👥😰🙍👌👨😶👏👕🙋👤👡👷👌👬👹👡👯🙅😸🙊😶🙂🙂👒👺😲👴👷😵👨👓👒👦👹👶😱😹😷😯😹😯👬👌😫👬👘👪👭👸🙄😱🙎👔👣👔🙋😷👑👫😯👫😹🙊👐😫😰😴👯🙊👙👣👹👏🙆👱👗😰👢😱👔😯😳👸👙🙁🙄👖👘👯👫🙁👓😰👔😯👬😲👣👙🙎👙👌👒🙄👧👚😳😰👏🙂👒🙈👌👧👴👱👙🙈👯🙈👗👖😹👕👩👚😶👰😷🙂👹👹😴👖👭👱🙃🙎👦👚👫👕👷👫👡👏😫👣👺👤👪👸👴😷😫😰👕👒👶👦🙆👔🙁🙃👖👔👘😳👏👓🙋🙈👶👭👷👚🙂👖👯👢🙍👭👰🙂🙋🙉👷👣👶👙🙉👹🙍👯👺👲👓👩😹👒👲😹👹😴👷🙇👣😳👶👴👤🙆👏👯👌😲👷👌👔👌👑👶👘👰👭👚🙃😶🙍🙈👢🙂👱🙅👴👥👧👱👑👫👺👰👗👺🙁🙍😳👑👱👩🙃😯😰👕🙉😯👧🙃👬👨👚👗🙇😹👘🙅😷👓😫🙎👲👸🙁🙂👢😫👏😰👙🙄👷😯🙍👤😲👯👪😷👹👌👥👌👵👚👗👓😸😰👱👨👨😲👲👣😵😶👴🙂👌👗😷🙅😯😷😰👬🙊👳🙂👬👙🙊👓🙍😴🙃👤👹👷👨👘😲👸👷🙅😵🙊👴😳😵🙊👗😷👤👡👗👙👰👪👗👩🙇👦🙊👶👥😰😱😳🙍😲👲🙈👺👩👖👰👗😹👷👺👖😫🙈😷👚👫🙍😫🙋👰👗👴👚😸🙉😯👚👚👐🙄😫👷👷😫👔😹🙎👬👚👗👧🙆👬🙇👔😵👑👪😸👗👌😱👣🙇👶🙉👶👌😴🙍👤🙋😱😰😶👯👰👐😫👣🙂👭👚😱👳👷🙋👧🙎👑🙎👸👮👣🙉👘😰👙👸👖🙊🙉👹👹👺👗👧👗😱👴👶🙆👓👲😵👖👚👙👖😵👺👱😳🙊👭😴👵👢🙅👥👘👴👨👲👦👘👸👫👒😶👨👮😴👲🙋👕👕🙁👪👲👚👡👕👐😱👭👸🙊😶👺🙊😱👩🙇👑😷👤👬👨👸👴😸👕👬👦🙉👕👕👒🙁👥👯👘👐👘👌👱👩👰👏👤🙃👮🙃👱🙂👹🙆🙍🙇😹👗🙍👏🙊😷👩🙉👌🙋🙁😫👢😴👚🙎🙇😶👒👓👩👰👱👳👥😱👗😲👙👕👚👤👢😸😫👚😱👚🙆😰🙁😯😲👱🙁🙆👏🙆👕👵🙁👑👷👙😰👙👦🙊😶👣👺👘👸😱🙋👰👵👴😵🙍👨😵😵🙄👢😲👩👱😱👴🙄👓👹👙👘👮🙃😯👐👷👑👰🙈🙍👑😳👩👕👱👢🙉👑👦👪👦😰👵🙈🙊🙊👱🙍👲👮😷👖😰😵😷🙇😫👏😵👨😯👭👖😳👦👧👒😲👥😯🙆👴🙎👶👫👺👒😳😳👷👭👸👗👵😹👲😫😹😹👣👩👣👗👳🙋👮🙄👪👌👴👔👰👪😷👫👶👔👒👢👚👶👰👪👤🙄😫👺👖👥👔👖👥👪👏👨👐👥😲😱👪👕👥👙👬🙉🙋👵😱🙊👕👓👬👨👑😳😳👌👺👌👹🙇👪👙🙊🙊🙂👱🙆🙊👗👩👴👦👵🙎👒👕👥😲🙂👪👡👹👪🙉👌👵👫👨🙅👹🙇👌👐👚👣🙋😹👓👪👔😲👹👷👨🙇🙈👡👖😶👲🙇👶👌👭😹👧👗👲👘👣🙅😰😫🙄😳👣🙍🙈👙🙈😷🙂😷👡👔👳🙁🙃👣👡👭👵👕👲🙆👚👲😯👵👯😫👪👪👴👢👙👴😲🙄🙁👑👹😳😫😱👤🙆😯👪👔👱👨🙉😫👚👬👲👡👹👓👕👪👱👌👢👰👸🙁😳😳😲🙁👳👏👲👺👬😵👫👗👡👷😫👨😳😱👧👫👥🙋😷👧👌👒🙍😲👤😷👷🙄😴👲👢🙇👸👗🙂🙅👳👮👓👙👺👵👖👥👱🙇👳👦👭🙄🙋👴👤👐🙃👔👑👙👣🙍🙃😯👕👙🙆👶👬👶👷👴😳🙊😶👺🙇👘👬🙎👦👳👧👪👦👷👷👺👭👐👱😵👷😳👶🙍👬😶🙋👺👰👵👌😵🙃🙈👢👱👺🙂🙈👐😷🙉👔😴🙉😯👐👳👱😯🙅👓👒😳👷👵😶👢😷😷🙂👮👏👗👑🙎🙅👺😸😷👒👢😱👰🙆🙅😶👨👴👨👐😴👩👦😳👤👪😸👭👪😸🙉😹👸👫👲😳👥👰🙃🙄😯👧👷😸👒👩👳👴👭👣👚👭😫👫👕👶👑👓👔😯👓🙁👺👡😯👌👶👡👺👌👷👦😹👩🙊😶😵🙇👶👔👖🙁👫🙁👔🙊👖👕👐👤👏👏👺👴👶😸🙉👘👌🙎👗👪👥🙇👶👷👑👗😳😵👚👷👗😲😶👷🙄😳👯😱👹👨👳🙈👔😯👔😶🙊👸👡😸👨👵👪👫👓👏👸👣👢🙈👔👘👘🙉👢👪🙃🙂👺🙄👤👭🙇👵👑👮😷😯👣👡👯🙍👗👭🙆👑👓🙊😱🙊🙈👱👪👦😹👡👬👐👡👥👯👥👶👢😰👰😹👩👹😷👏👴👢🙈👑👬🙂🙈😵👙🙆👶👑👸🙅👤👲😯👒👏👴🙄🙊👯👕👵😳🙃😶👘🙎😵👷🙅👣👥🙁👗😵👏👶👧👏👔🙇👌😷👙👮👷👹😷👘👗👑😶👶👐😵👹👕👌🙄👨👑😳👸👮😹👐😴😸🙍😴🙊👚🙈👫🙇🙆👩👔👐👣🙂😵👕😷👑😳😹👮👰👸👕😶👧😹😰👦👶👱👰👢🙉👺👶👭👬👙👱👳🙆🙃🙇😲👲👳🙍👶👶👬👤😳🙍👘🙆👧😴👲👶👨🙇🙈😰👢😸😯👑👨👩🙆😱👢🙆👔👕👑👢😫👏👡👙👰👤😹👒👨🙆🙄👱😲👩👙👮👔😶👯🙎🙁👦😳👡🙋👫👔👖👳😴😲👷👪👌👨😶😶👡😳👨👏🙋😰😳🙃😹👥👱👮👨👶👤👸👴👷👗😹😴🙇👴👔😵👱👫😫👣🙂👯👰👷👚🙈👢👕😷👔👨🙋🙆🙎👪👱👢🙄🙅👖👡🙍🙉👯🙁👓👹🙍🙇😯👴👐👌🙎😹👨👶👨🙂👚👙👑😯👒👺😱👚👗😰👳😷🙃😲👌🙋🙍👲👗🙍😰🙂👩🙄👢👸👵😳👱👮👪👖😲👩👪😷👓🙆😱🙍🙆👶👬🙁😶🙅😱👹👯😹👗👬😯😷👘👕👭😲🙃😸👮🙂🙁👒👨🙃🙅🙎👺🙉👏🙈😴👳😴👺👬👺👲😰👲🙁👌👫🙃🙃👘😯🙄😵👔😴👷🙋😲👹🙅😱👲🙍👭👧👗👫👢😫👫👏😲👓🙉👲👗👘👴👣👣😫👵🙃👖👵👴👢👳👔👳🙊👢👌🙃👭👙😫🙉👫👹👚🙇🙈😯👱😱👴👔👓👓😲👘👩🙄🙊🙃🙆😵🙋👷😰👧😱😹👏👱😫😴🙆👺👑👢👐🙃👣👬😵👘👩👔🙂👢😯👌🙍👘👑🙁😳👗🙈👴🙋👐👔👯👢👰😫👘👌👤👓👬👏👵😶😹👖👙👪😱👡😷👚🙋🙋👣👩👷👪👦👐🙁👣👚😫👫👕👳🙆😯👔😳👪👒👗🙍👯👣👑👒👘👖👘😵😶👭😰🙂👬👐🙆👣😲🙉👭👯👫👣👷🙋🙎👏👲👔😲👳👧👑😹🙎👭🙍😴🙉👘👧😹👗🙆😴👒👲🙂👭👵👦👯🙂👌😵👌👦👸👒😵👺👶👣🙉🙂😷👕👷👣😲👫👖🙇👹👸🙄😱🙂👐😸👨👮👕👓👷👳👶👸👨🙇👪👒👏😫👓👭😫👕😱👡😯👱👤👧👏👧👹👧👒👘👲👌🙎👏👢👖👹👹🙎👏🙇👤🙆👺👣🙃🙃👨👏👢🙉👔👌😵👭😸🙊👧👭👪👑👗👺🙍👓👷👬👷👶😱👯👡👘👧👒🙉👐👑👥👯👷👒👶😲👩👥👫👩👘👚🙉😹👢😫👶👔👩👶👥😷👶😸👙👥👩👷👗🙇👏👒👭👣👲👌👩👡👢👹👨👵👌👲👢😲👤👡👣🙈👑👰😹👰👨😵😸👥👑🙄👴🙉👰👷🙁🙁👣👕👒👬👏😯😵👹👭👱👮👮🙉🙈👑👫😱👒👑👑👦👲👦🙁👹😱👷👖👲😫🙈👣🙇👺👖👑👫👔👦👲😶👚👌🙊👘👭👓🙊🙍😴👵🙁👧😫🙈🙈👸👸👢👘👸👳👫👺🙅👐👣👴😶🙄👓😰🙈👩👵😵👯🙅👬😲👸👦🙇👸👨😷👦👶😫👣👡😹😯👶👢👶🙍👧🙂🙇👮😳👖😶👌👨🙅👏👬👕🙍👏👨👩😵😳😸🙈👡👭👱👱👩👙😳🙅🙂😱👭😲🙇👲🙊👭👙👢👚🙆👘😶👏👑🙉👥👢👕😳🙇😶😱🙍🙍😷👳👨👸😯🙋😰👙😵🙉🙈👢👱👲🙎👳🙆👳😹🙈🙄🙇👏🙇👖😹🙅👘😯👒👲👒👸👬👪👗🙋👶👮👮😸👨👳👵👤👧👶👫😫👱😷🙉👓🙋🙆🙍👑😶👬🙁👹👙👐👚👕👲😯👥🙃👭👭👸🙄🙈👯👺🙇👓🙊🙉🙁😵🙂😲😰😰👶👏👗🙋👱😫👭👣👙👶😲👖👣👡😰👯👰🙎👶🙉👐👬😴👐👷👲👐😯👗👨👓🙆👺👏👙👩👨👵👩👪😷👴👸🙆👪👯👹👑🙍👧😸😰🙋👺🙁👹😹👤👖👣👷😲👒👣😴👘😰🙆👥😸🙄😷😷😷👰🙋👐😫😷👑👐👷😲😯👖👴🙂👮👶🙇👺👴👧👓👐😳👳👸😫👫👮👬👦😰😫😱🙄👖👏👵😳😶🙁😹🙁👶👭👗👔🙊👢👮👷😲😹👔🙊👐👐👮👤👶🙃😯😱👩👢👸😳👶👒👒👸👗🙎👶👣😱🙅🙎👏👰👪🙋👚👔🙅👰👏👸👐🙆👲😴🙂🙇👓😯🙅👷👹😵👐👯😶😰👺🙄👚😷🙉👕👌👳🙂👙👳👩👪🙅🙊👕👭🙃👒👶👷👪👹👚👤👲👢👙👭😴👌👨🙍😹👓👚👥👙😴🙆👙👡😲😱👥😯👶😰🙋👡👡😳🙂🙅👧🙍👮👰😱👥👰👬👴👌😷👴🙈👸🙍👐🙉👺👘🙋👒🙈👷😷👙👓👚😶🙎🙆🙄👴👑👦😴😶👖😰👒🙈🙊🙆🙆👮👺👭🙄🙅👢👖😹👴👢👩🙁😴👓🙉👓😵👕👌👙😶👵👒🙁😲👓👚👹👸😲👮👤👡🙂👷🙂👌👗👙👥👔🙊👷👸🙈😫🙁😴👮👴👒👲😰😴👴🙆😫🙋🙇👳😹👗👘👓😶👱👙👬👵👷😵👴🙇😰👥👤👡👚🙇😫👢👣🙃👚👥👺👩😳👩👴👣👫👑👔👚👔👚👏👚👩🙎👑👯😴👭😲👵😳🙎🙎👩👳👩😲👡👐😶🙈😫👖😫👙😯👬👢👤🙄🙇🙉🙆🙍😴🙁😲🙆👵🙄😫👮🙎👙🙃😶🙂👡👨👐🙋😴👒👚🙄😳👌😳👒👬👥😴🙅👴👷👔🙃👩👦👺👘😯🙎👘👮🙄👕👢😱👰👮😳👡🙍😳👌😶👹🙊😹😰👳👣👯👶🙎👲👚😳👓👮😯🙊👧👧🙂😶🙊👧🙈😰👡🙎🙂😴👧🙄🙈👺👒👱👫👢👗🙂👬👤👫👨👗👗👗👹👹👴👖👏👖😳👖👰👤👲😫👶👴👢👩🙋😳👰👴👣👐🙍😴👧👢👭👌👬👌🙃👶🙎👵👫🙅🙇👕👭👚👓👣😯😷😲👑😶👲👩👌👬😳👐😴🙊👘🙉👹👱😯🙍👣👖👨👕👳👹👐👧👲👢👗👺👕🙎😰😶👹👮👱😵😫👯😷👘🙅😰👚🙊🙆😹👗👌👯👲👖👴🙊🙅👖👧👺👮👮👘🙊👷👗👳👒👘🙃🙇🙍👫👦👩👷👬😷👲👹😳🙂👘👬👹😹👢🙎😹😲👘👺👗👌🙇😫🙍👯🙄😰👏👩👳👵👙👒👕👤😹😶👫👰👌👸👑🙈🙋👓👓👰👔👦🙊🙇🙄👹👺👘👑🙉👣👥🙅👤😵😯🙅😴😵👓👯😱🙇👺👴😴🙁😲🙂👡👤🙃👑👨👤🙄👖🙉🙊🙆🙍👱👤👷😰👦👮😰👰👰👭🙁👡👵👴👴😱👌👩👧🙁👏👡🙁👐👧👗👙👩👱🙁😱🙆👢🙍👓🙇👗👢👔👏👚👑👒👔👓👵👌👷👧👓👰🙊😳😱😴😴🙃👫👬👚🙅🙅👱😶👯🙆👚😶👴😴🙍👤🙎👘👘👔😯😳👷🙈😶😷👴👤👙🙋👕😹👸👮👚👴👣👵👔👱👩👢👤😴🙄👱👮😴🙁🙅👧👐😶😫👓👰🙃😯👨🙎👷🙂👧👩😱👵😷👚👯👘👦👧👴👗😯👙👹👒👸👗👙👭😯👕🙅😫👸👌👖👔😷😵👩👒👔👰🙎👧😱👸😱😴👖👭🙄🙋😱👰😲👪👐👮🙈🙈👐👩😳👥👙👚😷😫😸👳😯👴😶👮👖👨👢👫👡😰👺👴🙂👭👚🙂👨😶👮😴👢👺👕😴👬😫👧👵👖👰😯🙄👲🙉😳👲👱🙁👣👐🙁👱👪👷👒😳👳👢👑😫🙋👗👱👮🙊🙋🙂🙆👗🙎😰👓👥👕😲🙋😹👗👐👡👫🙇😹😳😴👰🙄🙊👢👣👸👱🙎👤👯😹👢👧👏👲👙😷👸👶👱👺👳👕👕😫👺👣👱👲👕😰🙁😫🙄👦🙄👳🙉👵😴👪👸🙍👓👐😹😶👨😰👤👲😰🙁👏👚👑👬🙎😸👗👳🙋👪👶😶🙅👧👏👲👵👭🙅👖😶👖👴👹👐👡👑😵👓👩👙🙂👓👤😶😷👵😸😲😱👦👌👥👗🙋😵👖👱😱🙋👦👚👙👑😽😽

+

综上,现在的环境让我待的有些挺难受,于是有了换个环境的打算。

+

有没有预期的公司?倒是有两个,一个是 Tubi,一家外企,做海外免费影视剧的;另一个是 MegaEase,做基础设施研发的。之前读过一些介绍他们公司的价值观的文章,很合我的口味。MegaEase 的创始人是陈皓,也是引领我入开发这个门的一位大牛。

+

这里有一个 Tubi 的介绍:https://mp.weixin.qq.com/s/ZCQerV2HKPq9k9EhDocOhA

+ +
+ + + + + + + +
+ + + + + + +
+ +
+ + + +
+ + + + + + + + +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2022/use-aws-lambda-delete-s3-regularly/1.png b/2022/use-aws-lambda-delete-s3-regularly/1.png new file mode 100644 index 0000000000..252a1a27b1 Binary files /dev/null and b/2022/use-aws-lambda-delete-s3-regularly/1.png differ diff --git a/2022/use-aws-lambda-delete-s3-regularly/3.png b/2022/use-aws-lambda-delete-s3-regularly/3.png new file mode 100644 index 0000000000..280e1d73b3 Binary files /dev/null and b/2022/use-aws-lambda-delete-s3-regularly/3.png differ diff --git a/2022/use-aws-lambda-delete-s3-regularly/4.png b/2022/use-aws-lambda-delete-s3-regularly/4.png new file mode 100644 index 0000000000..62e91b5222 Binary files /dev/null and b/2022/use-aws-lambda-delete-s3-regularly/4.png differ diff --git a/2022/use-aws-lambda-delete-s3-regularly/5.jpg b/2022/use-aws-lambda-delete-s3-regularly/5.jpg new file mode 100644 index 0000000000..7e5ba914af Binary files /dev/null and b/2022/use-aws-lambda-delete-s3-regularly/5.jpg differ diff --git a/2022/use-aws-lambda-delete-s3-regularly/6.png b/2022/use-aws-lambda-delete-s3-regularly/6.png new file mode 100644 index 0000000000..186b80b416 Binary files /dev/null and b/2022/use-aws-lambda-delete-s3-regularly/6.png differ diff --git a/2022/use-aws-lambda-delete-s3-regularly/index.html b/2022/use-aws-lambda-delete-s3-regularly/index.html new file mode 100644 index 0000000000..a8a4f9e071 --- /dev/null +++ b/2022/use-aws-lambda-delete-s3-regularly/index.html @@ -0,0 +1,548 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 利用 AWS Lambda 定期清理 S3 文件 | 贾攀的流水账 + + + + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

贾攀的流水账

+ +
+

Panmax's Blog

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+ + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ 利用 AWS Lambda 定期清理 S3 文件 +

+ + +
+ + + + +
+ + +

背景

因为我的 bossku ,需要定期将全量数据库数据进行备份,我之前写过一篇文章分享我是如何将数据库备份到 S3 的:https://jiapan.me/2020/auto-backup-database/

+

由于不想为这个存储付费,所以我在 Things 中创建了一个周期性的任务,每周六提醒我来清理前一段时间的过期数据,通常我只保留最近两天的,将其余的删除。

+

最开始我是登录到 S3 的网站上进行操作,后来嫌麻烦,就将 S3 挂载到了本地(使用的是 QSpace 这个软件),每周六定期在本地进行删除操作。

+

本着 DRY(Don’t repeat yourself)原则,能自动化的事就不要自己重复去做,所以我准备写个脚本定期处理。

+

S3 提供了很完善的 API 可以让程序方便的进行操作,各个语言也都提供了 S3 API 的 SDK 封装,我要做的就是周期性的调取文件列表,判断如果文件超过 2 天则进行删除。这样的动作使用 Serverless 最合适不过了,这一次我还是选择使用我最熟悉的 AWS Lambda ,使用的语言也是万能、灵活的 Python。

+

初始化项目

首先我们初始化一个 Serverless 项目:

+
1
SLS_GEO_LOCATION=en serverless create --template aws-python --path s3-clean
+

没有 serverless 的可以先参考官方手册 进行安装,这不是本文的重点。

+

注意下上边命令最前边的 SLS_GEO_LOCATION=en,这个一定要加,因为 serverless 做了一件有些流氓的事:判断你的所在地是中国的话,会走腾讯的服务,他们是没有 aws 模板的,报错如下:

+

1.png

+

加上 SLS_GEO_LOCATION=en 可以将我们的地区强制指定到国外,这样就会走官方的逻辑(腾讯这个行为太 low 了)。

+

进入上边 serverless 为我们创建出来的项目,可以看到生成好了两个文件,我们来编辑 handler.py 文件:

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
import os  
import boto3


def delete_expire():
# 初始化 s3 sdk
s3 = boto3.resource('s3',
region_name=os.environ['S3_REGION'],
aws_access_key_id=os.environ['S3_ACCESS_KEY_ID'],
aws_secret_access_key=os.environ['S3_SECRET_ACCESS_KEY'])

# 绑定 bucket bucket = s3.Bucket(os.environ['S3_BUCKET'])
print('Objects:')

# 获取bucket中所有文件
all_file = []
for item in bucket.objects.all():
print(' - ', item.key)
all_file.append(item)

# 文件超过3个时进行清理工作
if len(all_file) > 2:
# 文件按照上次修改时间排序
all_file.sort(key=lambda x: x.last_modified)

# 只保留最后两个文件,删除其余文件
for item in all_file[:-2]:
item.delete()
print("%s deleted" % item.key)
print("delete done")
return True
else:
print("less than 3 ignore")
return False


def delete_backup(event, context):
delete_expire()
return {
"statusCode": 200,
}
+

流程比较简单:

+
    +
  1. 初始化 sdk
  2. +
  3. 关联 bucket
  4. +
  5. 取全部文件列表
  6. +
  7. 对全部文件按照时间正序排(旧的在前)
  8. +
  9. 删除倒数第二个文件之前的所有文件
  10. +
+

上边代码中一些参数通过环境变量进行获取,稍后我们会在配置文件中介绍这几个参数。

+

然后我们编辑 serverless.yml 文件,这个是我们服务的配置文件:

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
service: bossku-s3-clean  

frameworkVersion: '3'

provider:
name: aws
region: ${env:S3_REGION}
runtime: python3.8
environment:
S3_REGION: ${env:S3_REGION}
S3_BUCKET: ${env:S3_BUCKET}
S3_ACCESS_KEY_ID: ${env:S3_ACCESS_KEY_ID}
S3_SECRET_ACCESS_KEY: ${env:S3_SECRET_ACCESS_KEY}

functions:
delete_backup:
handler: handler.delete_backup
events:
- schedule: cron(45 0 * * ? *)
- http:
path: delete_backup
method: get
+

provider.region 用来指定我们的服务启动在哪个地区,我这里配置了一个 S3_REGION 的占位,用来从我本地环境变量获取,目的是和我们的 S3 在同一个地区,这样理论上连通性会跟好一些。

+

provider.environment 就是给程序提供运行时环境变量的地方,也就对应我们程序中 os.environ['xxx'],每一个我都和本地一个同名的环境变量相关联。

+
    +
  • S3_REGION 表示存储文件时使用的 S3 区域,比如:ap-east-1
  • +
  • S3_BUCKET 用来指定程序要读写的 bucket
  • +
  • S3_ACCESS_KEY_ID S3 API 的 ACCESS KEY
  • +
  • S3_SECRET_ACCESS_KEY S3 API 的 SECRET KEY
  • +
+

再往下的 functions 是用来声明函数的区域,我将我们的 delete_backup 关联了两个事件:

+
    +
  1. 每天 0 点 45 分(北京时间早上 8 点 45 分)定时启动。
  2. +
  3. 让 Lambda 提供给我们一个 HTTP 的 GET 请求,用来手动触发便于调试。
  4. +
+

做完这些我们还有一个工作,将程序所依赖的 boto3 安装在项目目录下,这样就会在发布时会一起上传到 Lambda 中,Lambda 本身是不带这个包的,而且不支持 pip 安装。

+
1
pip install boto3==1.23.8 --target=.
+

接下来就可以部署到 Lambda 进行验证了,我们可以先将程序中的 item.delete() 进行注释,观察下日志看看流程是否正常。

+

部署

部署脚本如下:

+
1
2
3
4
5
6
7
8
export AWS_ACCESS_KEY_ID=YOUR_AWS_ACCESS_KEY_ID
export AWS_SECRET_ACCESS_KEY=YOUR_AWS_SECRET_ACCESS_KEY
export S3_REGION=YOUR_S3_REGION
export S3_BUCKET=YOUR_S3_BUCKET
export S3_ACCESS_KEY_ID=YOUR_S3_ACCESS_KEY_ID
export S3_SECRET_ACCESS_KEY=YOUR_S3_SECRET_ACCESS_KEY

serverless deploy
+

3.png

+

如图可以看到发布成功了,我们访问 Lambda 提供给我们的 endpoint 来手动触发下这个函数。

+

+

成功了,我们再到 Lambda 的操作界面看下日志,我通常是在函数的【监控】-【查看 CloudWatch 中的警报】-【日志组】中看日志,应该有我不知道的更方便的方式,后边学会了再做补充吧。

+

+

可以看到整个流程是 ok 的。

+

我们将 item.delete() 的注释取消掉再发布一次就可以了。

+

通过 Bark 通知我

为了在每次删除后都能及时的收到通知,我通过 Bark 给我的手机发个通知。我们只需将 delete_backup 函数改成这样就可以了:

+
1
2
3
4
5
6
7
8
def delete_backup(event, context):  
if delete_expire():
requests.get('https://api.day.app/{YOUR_KEY}/Bossku备份清理成功')
else:
requests.get('https://api.day.app/{YOUR_KEY}/Bossku备份清理失败')
return {
"statusCode": 200,
}
+

别忘了在本地目录安装 requests 包:

+
1
pip install requests==2.27.1 --target=.
+

再次发布,然后我手动触发两次,第一次文件超过 2 个所以可以执行成功,第二次文件不足两个执行失败,符合我们的预期。

+

6.png

+

这样我就不用再在每周六手动清理这些文件了。

+ +
+ + + + + + + +
+ + + + + + +
+ +
+ + + +
+ + + + + + + + +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2022/use-aws-lambda-push-blog-comment/1.png b/2022/use-aws-lambda-push-blog-comment/1.png new file mode 100644 index 0000000000..49ce927d86 Binary files /dev/null and b/2022/use-aws-lambda-push-blog-comment/1.png differ diff --git a/2022/use-aws-lambda-push-blog-comment/2.png b/2022/use-aws-lambda-push-blog-comment/2.png new file mode 100644 index 0000000000..c1274df093 Binary files /dev/null and b/2022/use-aws-lambda-push-blog-comment/2.png differ diff --git a/2022/use-aws-lambda-push-blog-comment/3.jpg b/2022/use-aws-lambda-push-blog-comment/3.jpg new file mode 100644 index 0000000000..695d47b0ac Binary files /dev/null and b/2022/use-aws-lambda-push-blog-comment/3.jpg differ diff --git a/2022/use-aws-lambda-push-blog-comment/4.png b/2022/use-aws-lambda-push-blog-comment/4.png new file mode 100644 index 0000000000..6e3039a4ad Binary files /dev/null and b/2022/use-aws-lambda-push-blog-comment/4.png differ diff --git a/2022/use-aws-lambda-push-blog-comment/5.png b/2022/use-aws-lambda-push-blog-comment/5.png new file mode 100644 index 0000000000..d747c0eef2 Binary files /dev/null and b/2022/use-aws-lambda-push-blog-comment/5.png differ diff --git a/2022/use-aws-lambda-push-blog-comment/6.png b/2022/use-aws-lambda-push-blog-comment/6.png new file mode 100644 index 0000000000..555a0f6e62 Binary files /dev/null and b/2022/use-aws-lambda-push-blog-comment/6.png differ diff --git a/2022/use-aws-lambda-push-blog-comment/7.png b/2022/use-aws-lambda-push-blog-comment/7.png new file mode 100644 index 0000000000..b67b8d8b56 Binary files /dev/null and b/2022/use-aws-lambda-push-blog-comment/7.png differ diff --git a/2022/use-aws-lambda-push-blog-comment/index.html b/2022/use-aws-lambda-push-blog-comment/index.html new file mode 100644 index 0000000000..ebf8b4fa87 --- /dev/null +++ b/2022/use-aws-lambda-push-blog-comment/index.html @@ -0,0 +1,523 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 利用 AWS Labmda 推送博客评论 | 贾攀的流水账 + + + + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

贾攀的流水账

+ +
+

Panmax's Blog

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+ + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ 利用 AWS Labmda 推送博客评论 +

+ + +
+ + + + +
+ + +

正向反馈

昨天写了篇博客 《 如何写作》 ,在这篇文章中我翻译了别人的一篇短文,同时加了点自己的叙述。

+

晚上用手机浏览这篇博客的时候发现收到一个新留言,赶紧打开电脑进行了回复。

+

+

让我兴奋的是,没想到有人会看我的博客,而且还能指正我理解不到位的地方。在被人关注并且能收获积极反馈的情况下会给我们正向激励,让我们更愿意做一些输出。

+

我博客的评论系统后端是用 Leancloud 做的存储,默认不支持通知,所以之前我并没有太关注过评论,甚至不知道哪些文章有评论,如果需要的话就到 Leancloud 后台去看看。

+

+

为了以后能更及时的接收与回复评论,我准备给博客评论加个监控,当有新评论时通过 Bark 提醒我,有朋友之前实现过这个功能,但我忘记怎么做的了,索性这次重新再造一个。

+

这次继续使用 AWS 的 Lambda 运行我们的服务,关于 Lambda 的使用姿势可以看下我前几天的一篇文章:《利用 AWS Lambda 定期清理 S3 文件》 ),这里直接介绍实现细节。

+

流程说明

流程图如下:

+

+

说明下如何判断有没有新评论:这里我们继续借助 Leancloud 的存储,为了不影响 Comment 表,我们新建一个 BarkComment 表来存储已经发过通知的评论,只需在 Class 名称处填入 BarkComment 即可,其他保持默认:

+


+

我们在通过 SDK 写入数据时会自动帮我们将需要的列创建出来,所以也不用做列的新增。

+

handler

再来看下核心代码:

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
import os  
import requests
import leancloud

def blog_comment(event, context):
leancloud.init(os.environ['LEANCLOUD_APPID'], os.environ['LEANCLOUD_APP_KEY'])

# 取出最近10条评论
Comment = leancloud.Object.extend('Comment')
query = Comment.query.descending("createdAt")
comments = query.limit(10).find()
print(len(comments))

# 判断是否有新评论
new_comment = 0
BarkComment = leancloud.Object.extend('BarkComment')
for comment in comments:
if not BarkComment.query.equal_to("commentId", comment.id).find():
new_comment += 1
bark_comment = BarkComment()
bark_comment.set("commentId", comment.id)
bark_comment.save()

# 如果有新评论发送通知
if new_comment > 0:
print("=============")
print(new_comment)
msg = "博客收到 %d 条新评论" % (new_comment,)
requests.get('https://api.day.app/YOUR_KEY/' + msg)
else:
print("no new comment")

return {
"statusCode": 200,
}
+

看过之前文章的已经知道,我们需要将 leancloudSDK 在项目目录下也下载一份:

+

pip install leancloud==2.9.10 --target=.

+

serverless.yml

provider.environment 新增本次需要的环境变量:

+
1
2
3
4
5
6
7
provider:
...
environment:
...
LEANCLOUD_APPID: ${env:LEANCLOUD_APPID}
LEANCLOUD_APP_KEY: ${env:LEANCLOUD_APP_KEY}
LEANCLOUD_API_SERVER: ${env:LEANCLOUD_API_SERVER}
+

functions 空间内添加:

+
1
2
3
4
5
6
7
8
9
10
functions:  
...

blog_comment:
handler: handler.blog_comment
events:
- schedule: cron(*/5 * * * ? *)
- http:
path: blog_comment
method: get
+

表示每 5 分钟执行一次,同时提供给我们一个 HTTP 终端来进行调试。

+

我们将这个服务进行发布,手动访问下分配给我们的 endpoint,通过下图可以看到我已经收到了通知:

+

+

因为之前 BarkComment 表中没有数据,所以系统认为这 10 条都是新评论。

+

然后我自己在博客里留了 3 条评论,等待每个 5 分自动执行的时候再看下效果:

+

+

在 14 点 25 分时,我们收到了「博客收到3条新评论」的通知,说明我们的程序成功判断出了新的增量数据,同时可以检查下 BarkComment 表也确实有了新数据。

+

其实我们可以做的更完善一些,比如通知我们具体是哪篇文章有新评论,评论的内容是什么等等。大家需要的话可以自己实现,我只在这里进行抛砖。

+ +
+ + + + + + + +
+ + + + + + +
+ +
+ + + +
+ + + + + + + + +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2022/what-i-access-to-information/20220627190542.png b/2022/what-i-access-to-information/20220627190542.png new file mode 100644 index 0000000000..6344ddd592 Binary files /dev/null and b/2022/what-i-access-to-information/20220627190542.png differ diff --git a/2022/what-i-access-to-information/20220628101618.png b/2022/what-i-access-to-information/20220628101618.png new file mode 100644 index 0000000000..ef56878f68 Binary files /dev/null and b/2022/what-i-access-to-information/20220628101618.png differ diff --git a/2022/what-i-access-to-information/20220628103008.png b/2022/what-i-access-to-information/20220628103008.png new file mode 100644 index 0000000000..6f92ce2166 Binary files /dev/null and b/2022/what-i-access-to-information/20220628103008.png differ diff --git a/2022/what-i-access-to-information/20220628103039.png b/2022/what-i-access-to-information/20220628103039.png new file mode 100644 index 0000000000..192eb8c933 Binary files /dev/null and b/2022/what-i-access-to-information/20220628103039.png differ diff --git a/2022/what-i-access-to-information/20220628133004.png b/2022/what-i-access-to-information/20220628133004.png new file mode 100644 index 0000000000..4561ea552e Binary files /dev/null and b/2022/what-i-access-to-information/20220628133004.png differ diff --git a/2022/what-i-access-to-information/index.html b/2022/what-i-access-to-information/index.html new file mode 100644 index 0000000000..1999a1c459 --- /dev/null +++ b/2022/what-i-access-to-information/index.html @@ -0,0 +1,537 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 我获取信息的 6 个渠道 | 贾攀的流水账 + + + + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

贾攀的流水账

+ +
+

Panmax's Blog

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+ + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ 我获取信息的 6 个渠道 +

+ + +
+ + + + +
+ + +

我日常获取信息主要有如下 6 个渠道:

+
    +
  • Twitter
  • +
  • NewsLetter
  • +
  • 掘金的 GitHub 热门榜
  • +
  • GitHub Following && For you
  • +
  • Telegram Channel
  • +
  • 书籍
  • +
+

Twitter

我在上大学期间做过的最有价值、对我人生影响最大的一件事,可能就是掌握了科学上网的手段,并且从大二开始没有再用过百度搜索,把搜索这个任务全部交给了Google ,毕业工作后在 Google 的加持下,我的工作产出和效率应该能比初入职场使用百度的同辈高出 30%,我认为做技术人员的第一步就是弃用百度、使用 Google。至今我仍然会为了提升自己的访问外网的体验,每年投入上千元在购买线路、软件这件事上。

+

但 Google 终究是个搜索引擎,是我们在遇到问题时使用的工具,接下来我要说的是另一个可以让我们被动获取信息的,等同于国内微博的 Twitter。在 Twitter 上言论相对自由,因为 Twitter 是个面向全世界的产品,所以在这里可以关注很多国内外的技术牛人(没错,也包括国内,由于国内的言论控制很多国内牛人也不在国内平台发言了),看他们的分享,而且信息的时效性很高。同时在上边活跃的国内圈子里的人,大部分都很 geek,愿意分享自己的发现的新玩意或者自己造的新轮子,我通过他们可以获取一手信息,玩到最新的玩具。

+

有人会说在 Twitter 上看不到自己需要的内容,那是因为你关注的人还不够多,或者还没关注到你想关注那个圈子里的 KOL,稍微耐心一些,再投喂给 Twitter 算法一些你的偏好,早晚能进入你的圈子,看到一个新世界。

+

今年是我使用 Twitter 的第十个年头,我经常在推上看到让我眼前一亮的内容,给我提供新点子、新工具、新观念。之前会偶尔随手点个 like,但这样在回顾时不便于索引和分享,最近尝试将内容收集到 Notion,方便自己也方便分享给更多的人看∶ https://panmax.notion.site/286a2dbe19ca4a8badcf2e06470964a6 ,这个列表我会随时、持续更新。

+

NewsLetter

今年 NewsLetter 在国内有流行起来的趋势,国内也出现了做 NewsLetter 业务的平台,如竹白,我也订阅了一些 Letters,有免费的也有付费的,下边分享几个我觉得质量不错的 Letter(排名不分先后),这些 Letter 我基本都是通过 Twitter 发现的:

+ +

我会在上班想摸🐟时翻看一下近期的 NewsLetter,我统一用 Google 邮箱接收这些信件,同时设置好了规则,让这些信件汇总在一个目录下,并且不会实时给我发推送(因为我并不需要立即阅读它们)。

+

+

掘金的 GitHub 热门榜

我用掘金提供的插件作为浏览器新标签页的默认首页,这个页面中间一栏有 GitHub 上的项目列表,我会不定期看一些我关注语言(如 Go、Rust、Python)又出了哪些新玩具,按热门排序,如果想看新鲜有趣的就看今日,如果想看长盛不衰的就看本周或者本月。

+

+

GitHub Following && For you

上边提到的掘金的 GitHub 项目列表,是按照 Star 增量和项目创建时间计算得出的,所有人看到的都是相同的静态数据,GitHub 官方也有自己的两个 Feed 流,分别叫 Following 和 For you。

+

Following 是看你关注的人的动态(如他关注了什么项目、他贡献了什么项目)

+

+

For you 是 GitHub 今年新推出的根据我们的喜好,使用算法推荐给我们的与我们相关、我们可能感兴趣的项目。

+

+

Telegram Channel

Telegram channel 是个小宝藏,类似于一个除了群主其他人禁止发言的群,群主产生优质内容后随时发到群里,列几个我自己常看的 channel:

+ +

+

书籍

上边介绍的那些方式获取的信息大多具有时效性,我们不仅要掌握时效信息,还要掌握能经历岁月洗礼的信息,这就要靠读书了,我读书不挑种类,只有一个前提,这本书在豆瓣上的评分要在 9 分以上,我不想把时间浪费在低质量的书上。额外情况是,如果我关注或者敬佩的人推荐了一本书,并且他对书的介绍吸引了我,我也会去读一下。

+

我还会去挑一些经典书目来读,因为这些书经过时间的淘洗,回应了人类社会最根本的问题,具有跨时代的意义。

+

我认为读书这件事没有太多捷径和技巧,拿到一本书后按部就班一页一页读就好,我从来没用过网上介绍的那些快速阅读方法,当然在读的过程中手里拿支笔写写画画是有必要的。对了,一定要读纸质书,原因见:纸质书赢了

+

我的书单:https://jiapan.me/book-list/

+

互联网时代从来不缺乏免费的内容,最珍贵的资源是我们的时间。不要花太多工夫读那些免费、廉价,但是质量低的内容,读它们不仅浪费时间,甚至会误导我们。

+ +
+ + + + + + + +
+ + + + + + +
+ +
+ + + +
+ + + + + + + + +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2022/what-i-think-other-already-think/index.html b/2022/what-i-think-other-already-think/index.html new file mode 100644 index 0000000000..77412ce653 --- /dev/null +++ b/2022/what-i-think-other-already-think/index.html @@ -0,0 +1,491 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 我想的事情其他人已经想到了 | 贾攀的流水账 + + + + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

贾攀的流水账

+ +
+

Panmax's Blog

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+ + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ 我想的事情其他人已经想到了 +

+ + +
+ + + + +
+ + +

因为6月份工作有些变动,本来计划的7月份要做的甲状腺复查打算趁着这段时间的空窗期趁早先做一次,避免7月份太忙不方便请假,所以挂了今天早上的号去医院。

+

医生给我开了一系列检查项目和药品后,我就去缴了费并打印了发票。其中有一项彩超,约的是下周一上午10点,今天可以先做抽血,抽血后今天的事情基本就结束了,然后我想到如果彩超约上午10点,可能会和早会冲突,当时医生给了我几个选项,上午10点、下午1、2、3点,但我习惯性地选择了第一个。所以我打算找医生去帮我换成下午1点,这样可以不请假就能把检查做了。

+

我在回去找医生的路上看了下检查单,上边已经标记了预约好的时间,当时就在想医生该怎么帮我改时间呢,单子会重新打吗?还是在系统中改了时间就行,单子上可以不改?反正替医生想了好几种方案,最后找到医生后,医生用了一个最简单的方案:直接开张新的,重新缴费,旧的那张去人工口办理退费。

+

好吧,这个问题看来医院已经考虑到了,那么下一个问题又开始在我脑子里旋转了,我已经把之前的费缴了,退费和缴费的内容是同一个项目、同样的价格,这样的话是否还需要我再缴一次费?而且我已经打印了发票,这个已经打印的发票如何处理。进一步我又想到,如果我把之前的项目退了,但是并不给新的缴费,那么后边在走商保报销的时候是不是就会有漏洞:可以把退了的钱也给报了。脑海中又替工作人员想了几种方案。

+

到了人工口,办理方式也是很简单粗暴,医生查询到我之前的缴费单已经打印过发票,要求我先把发票交回去,但是我的发票上是把今天所有项目打在了一起的,如果收回去后只给我把这次新缴费的打印发票,那我剩下那些就没办法报销了。带着这个问题我把发票递给了工作人员,工作人员看后说这个发票上的所有项目需要统一退费,再重新缴一次费,又是一个既简单有清晰的处理方式,这让我想到了软件设计模式中的 KISS(Keep It Simple, Stupid)原则。

+

我想的事情其他人已经想到了,而且有了最优解。

+ +
+ + + + + + + +
+ + + + + + +
+ +
+ + + +
+ + + + + + + + +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2022/why-wechat-not-support-scan-health-code/index.html b/2022/why-wechat-not-support-scan-health-code/index.html new file mode 100644 index 0000000000..c60d96df46 --- /dev/null +++ b/2022/why-wechat-not-support-scan-health-code/index.html @@ -0,0 +1,507 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 为什么健康宝不支持微信扫码? | 贾攀的流水账 + + + + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

贾攀的流水账

+ +
+

Panmax's Blog

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+ + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ 为什么健康宝不支持微信扫码? +

+ + +
+ + + + +
+ + +

前天我发了一条朋友圈,内容是:『我有一个问题想问下圈里做客户端开发的朋友:健康宝扫码是个特别频繁的使用场景,为什么微信不支持用自带的「扫一扫」自动跳转到健康宝,而是必须先打开健康宝,使用健康宝扫码?』这条朋友圈收到了不少评论,大家集思广益站在不同角度给了很多回答,为了不辜负大家的劳动成功,我在下文对这些回复做个分类总结,并结合自己的想法来说说我对各个原因的判断。

+

技术不支持

很多评论都认为不能微信扫码的原因是技术上不支持,比如有说 URL 限制的、有说健康码和微信二维码不兼容的还有说数据不互通的等等。

+

首先我站在技术角度发表下我的观点:技术上没有任何障碍,健康码本质上也是二维码,二维码的原理都是相同的,就是将一段字符串生成一个设备容易识别的图片。应用扫描图片时先解析成文本,根据文本内容执行不同的动作就可以了,并且二维码和文本互转的编解码标准是统一的。

+

另外我朋友圈中居住在北京以外的其他城市的小伙伴,如深圳、新疆、广西都表示他们城市的健康宝是支持直接用微信「扫一扫」的,这直接打破了技术不支持的“谣言”。我的问题也应该改为「为什么北京健康宝不支持微信扫码?」才更精确。

+

有评论说因为北京健康宝是北京的,不是国家的,所以微信不愿意花精力来做这个适配,但你看人家深圳不就支持吗,当然可以说因为腾讯总部就在深圳,近水楼台,但广西、新疆也支持了又该怎么说。

+

小程序列表渗透率

有朋友提到,不让用微信扫一扫是为了提升小程序列表的渗透率,这个脑洞开的相当大。

+

我们每次打开健康宝都要先下拉展开小程序列表,其他的小程序也会不可避免的被我们看到,同时可以培养我们下拉打开小程序列表的使用习惯,小程序的使用率会大大增加。

+

我认为这个原因的可能性也不大,首先在这种关头微信是不会冒着个险来发这么蝇头小利的“国难财”的,还有上边也提到了其他城市是可以用微信二维码扫描的,微信并没有对他们做限制。

+

健康宝产品经理考虑不周全

这个是有可能的,评论区有几个人也提到:「又不是不能用」、「反正体验不好你也要用」。

+

这条就不展开聊了,不用恶意来揣摩别人,我只是说有可能但不一定。

+

当然还有可能是开发效率低下,当前小程序等待迭代的需求排的太多了,这个低优的功能还没有排上开发日程。

+

打个岔:不知道北京健康宝的开发人员是不是在事业单位做了个有铁饭碗的程序员?

+

用户安全性考虑(健康宝产品经理考虑周全)

最后再来说两个我觉得比较靠谱的原因,第一个原因是为用户安全着想。

+

大家都知道微信「扫一扫」支持的场景很多,比如扫商品条码、花草、动物等等,我们最常用的场景是使用扫一扫付款,这几年电子支付几乎渗透到了每一个人。产品经理担心出现恶意换码,比如把健康码换成支付码或者其他 URL,这样有可能给人们的财产带来损失,尤其是对智能手机使用不太熟悉的老年人。即便是跳转到其他地方,也会给当时扫码的人们和企业主带来不便。为了避免这种情况的出现,健康宝干脆就只在自己的应用内识别自己的专码,来提升安全性,自然也不会推荐让用户通过微信「扫一扫」进入。

+

公民(国家)信息安全性

我认为比较靠谱的第二个原因就是信息安全问题,如果要让微信「扫一扫」支持跳转,肯定要对外暴露接口(或者 schema),这都或多或少地增加了风险。如果接口鉴权没有做好再加上被图谋不轨的人发现了这个漏洞,那么人们的信息就会有暴露的风险(责任全在美方)。

+

综上,我认为北京健康宝没支持微信扫码的原因有以下三个(排名分先后):

+
    +
  1. 公民信息安全
  2. +
  3. 用户财产安全
  4. +
  5. 在需求列表中
  6. +
+ +
+ + + + + + + +
+ + + + + + +
+ +
+ + + +
+ + + + + + + + +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2023/2023-Qingming-Festival/index.html b/2023/2023-Qingming-Festival/index.html new file mode 100644 index 0000000000..1e92353832 --- /dev/null +++ b/2023/2023-Qingming-Festival/index.html @@ -0,0 +1,497 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 2023年清明节无题 | 贾攀的流水账 + + + + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

贾攀的流水账

+ +
+

Panmax's Blog

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+ + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ 2023年清明节无题 +

+ + +
+ + + + +
+ + +

现在时间是2023年4月5日,清明节,凌晨2点,失眠。

+

在清明节前一天有位亲属去世了,需要回老家来奔丧。

+

上午10点半得知的消息,先从公司乘地铁回家,由于离家太远,12点多才到家,实属无奈。

+

简单收拾了些衣物开车上路,一路上都在下雨,时而大雨,时而小雨。中间在服务器休息+充电一小时,下午4点到的老家。

+

亲人是突然去世的,去世前没有经历过太多痛苦,也没有拖累家人长期的照料,也算是喜丧吧。

+

回来跟家里人闲聊过程中,得知小时候和我经常一起玩的哥哥,因为赌博现在已经倾家荡产了,把周围七大姑八大姨的钱借了个遍,甚至刷爆很多张信用卡来拆东墙补西墙,去年也找我借了几万块钱,说是生意周转使用,过一段时间就还,但迟迟没有还。现在还背了很多高利贷,身体也非常糟糕,本来已经有了轻生一走了之的念头,多亏家里人发现的及时,和他做了些心理工作,让他给那些借了钱的人挨个道歉,说明错误,并打下欠条,后边通过变卖家产去一点点偿还。

+

久赌无胜家。

+
+

最后用AI帮我补充的句子来结尾:

+
+

在这样的节日里,我们不仅要缅怀已故亲人,也要珍惜眼前人,因为生命短暂而珍贵。同时,也要引以为戒赌博的危害,要远离赌博,珍爱自己和家人的幸福生活。

+
+ +
+ + + + + + + +
+ + + + + + +
+ +
+ + + +
+ + + + + + + + +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2023/30-Survival-Tips-for-Adults/index.html b/2023/30-Survival-Tips-for-Adults/index.html new file mode 100644 index 0000000000..964b1867d8 --- /dev/null +++ b/2023/30-Survival-Tips-for-Adults/index.html @@ -0,0 +1,517 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 关于成年人的30个生存技巧 | 贾攀的流水账 + + + + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

贾攀的流水账

+ +
+

Panmax's Blog

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+ + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ 关于成年人的30个生存技巧 +

+ + +
+ + + + +
+ + +
    +
  1. 手机上最好的生产力应用程序叫做飞行模式
  2. +
  3. 让“不”成为你的默认选项。无论是新工作项目还是社交聚会,对非优先事项说“是”会破坏你的优先事项。如果这不是一个“肯定”,那么就是否定它。
  4. +
  5. 将“我对此一无所知”规范化为成功答案。
  6. +
  7. 如果书或电影很糟糕,你不必看完。
  8. +
  9. 停止后悔过去的决定。在当时拥有自己所掌握的知识下做出了最佳选择。与之和平相处吧。
  10. +
  11. 成功归结为一个简单的选择:1、确定自己想要什么;2、确定需要付出多少代价;3、选择是否愿意付出代价。
  12. +
  13. 写下您的 3:3:3 计划:专注于您最重要项目 3 小时时间、完成 3 个较短任务和进行 3 次维护活动。定义一个“高效率”的一天至关重要。否则即使输出极佳也永远无法得到平静。
  14. +
  15. 找到您的能量高峰状态(大多数人在早上)。在此期间时间块化 2-4 小时进行最重要的任务(参见 #7)。
  16. +
  17. 避免早上第一件事就查看手机。幸福来源于精心设计,而不是靠运气。
  18. +
  19. 为你的一天(就像电影一样)配上音乐。音乐是强大的情绪增强剂。几乎所有事情都可以通过音乐更加愉快地完成。
  20. +
  21. 每天早上进行内部清洁。立刻喝一杯满满的水。你的身体60%是水,要及时补充。
  22. +
  23. 遵循日本的80%法则:“吃到只有80%饱为止。”没有人有时间进入食物昏迷状态。
  24. +
  25. 规范午间小睡。这样可以让你在一天中获得两倍的精力。
  26. +
  27. 我们的祖先曾经狩猎长毛象,现在我们每天坐8个小时办公桌,然后平均再看3个小时电视。人类不适合这种生活方式。使用站立式办公桌、进行步行会议、每天运动身体等方法来改变习惯。
  28. +
  29. 心理学认为,你如何谈论别人就是你如何对待自己(所以要友善)。
  30. +
  31. 聚光效应(偏见):我们认为别人比实际更关注我们。残酷的事实是:当你意识到没有人在想着你时,你才真正拥有自由
  32. +
  33. 停止循环思考,提出有效问题。如果和我一样总会反复回忆某些事情,请问自己:这有用吗?1年后我还会关心它吗?
  34. +
  35. 磨练你的肢体语言(7-38-55法则)。人们会根据以下因素来喜欢/不喜欢你的交流:7%是用词,38%是语调和面部表情,55%是肢体语言。站直身子、挺胸抬头、眼神交流、微笑并握紧手臂。这样你就会变得更有魅力。
  36. +
  37. 一个人最喜欢听到的声音就是自己的名字。他们第二个最喜欢听到的声音是他们所爱之人和宠物的名字。每当你听到其中一个被提及,请记下来。之后,通过“姓名”询问他们如何了解这些信息,这样可以让你脱颖而出。
  38. +
  39. 花更多时间与给予你能量的人在一起,减少与夺走你能量的人相处时间。
  40. +
  41. 掌握“告别的礼物”。你不欠一个贬低你的朋友、伴侣或雇主忠诚。成功和幸福的人只是简单地说再见。
  42. +
  43. 每周做一些独自的事情(晚餐、电影等)。社会已经让我们认为独自做事很奇怪了。但如果你不习惯独处,就永远无法舒适地离开有毒关系。一个能够快乐独处的人是一个强大的人。
  44. +
  45. 写下你的目标。提醒自己想成为什么样子。拥有目标的14% 的人比没有目标者成功率高10倍,而拥有书面目标计划并实施它们3% 的人比只是拥有目标者成功率高3倍。
  46. +
  47. 避免告诉别人你的目标。这会释放廉价多巴胺,并欺骗你大脑以为已经实现了它们(降低动力)。悄悄行动吧。
  48. +
  49. 简化您的财务:取消未使用订阅服务;自动支付账单、储蓄和投资;按50/30/20规则预算(50%用于需求,30%用于愿望,20%用于储蓄)。
  50. +
  51. 购买能让你更健康、更富有或提供你自由时间的东西。这被称为实际唯物主义:产品可以在您生活质量上产生实质性影响。
  52. +
  53. 使用1%规则来控制冲动购买。如果该物品超过您年收入的1%,请等待3天。如果3天后仍然想要该物品,请购买它。通常情况下,您会意识到自己并不真正需要那个东西。
  54. +
  55. 如果购买了一件物品,则捐赠、丢弃或出售另一件物品。极简主义是一个双重学科:管理进入和离开的所有物品以保持平衡。
  56. +
  57. 给你的大脑留下一个隔夜任务。闭上眼睛时给你的大脑一个任务。“我怎样才能每月多赚1000元?”不要试图当场解决它;只需将其释放到潜意识中(它会在晚上处理)。
  58. +
  59. 让奇怪变得正常。“奇怪之处就是我们与众不同、被雇佣的原因所在。成为毫无歉意地独特的自己。事实上,变得奇怪甚至可能带给你终极幸福”
  60. +
+ +
+ + + + + + + +
+ + + + + + +
+ +
+ + + +
+ + + + + + + + +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2023/AWDL-Mac-disconnected/0.png b/2023/AWDL-Mac-disconnected/0.png new file mode 100644 index 0000000000..5ea3ceed7c Binary files /dev/null and b/2023/AWDL-Mac-disconnected/0.png differ diff --git a/2023/AWDL-Mac-disconnected/1.png b/2023/AWDL-Mac-disconnected/1.png new file mode 100644 index 0000000000..08941d0f04 Binary files /dev/null and b/2023/AWDL-Mac-disconnected/1.png differ diff --git a/2023/AWDL-Mac-disconnected/index.html b/2023/AWDL-Mac-disconnected/index.html new file mode 100644 index 0000000000..37eea0ba4e --- /dev/null +++ b/2023/AWDL-Mac-disconnected/index.html @@ -0,0 +1,520 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 解决由于 AWDL 导致 Mac 的断网问题 | 贾攀的流水账 + + + + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

贾攀的流水账

+ +
+

Panmax's Blog

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+ + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ 解决由于 AWDL 导致 Mac 的断网问题 +

+ + +
+ + + + +
+ + +

我的电脑在公司使用无线网络时经常性断网,为了有稳定的网络我在工位时经常接根网线,使用网线连接。之前公司运维给了个叫 WiFriedX 的工具来解决这个问题,最近发现问题又出现了,开会时断网非常耽误事,所以就又着手开始排查。

+

最后定位到是苹果搞的 AWDL 引起的,AWDL 全称:Apple Wireless Direct Link 苹果无线直连,用于 AirDrop、AirPlay 和其他服务的低延迟高速率 WIFI 点对点传输功能。苹果为它提供了独立的网络接口,可以通过 ifconfig awdl0 看到其状态。

+

+

苹果的操作内核为1个 WiFi Broadcom 硬件芯片提供了多个 WiFi 接口:

+
    +
  • en0:主要 WiFi 接口
  • +
  • ap1:用于 WiFi 网络共享的接入点接口
  • +
  • awdl0:苹果无线直接链接接口
  • +
+

通过拥有多个接口,我们的电脑就能够在 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
2
3
4
5
6
7
8
9
10
11
#!/usr/bin/env bash

set -euo pipefail

while true; do
if ifconfig awdl0 |grep -q "<UP"; then
(set -x; ifconfig awdl0 down)
fi

sleep 1
done
+

这段逻辑会每秒钟检测一次 awdl0 网卡状态,如果是开启就进行关闭。

+

运行这段代码可以达到永久关闭 awdl0 网卡的效果,但是如果是我们每次手动运行它会比较麻烦,每次重启电脑后还要记得再次运行。于是大神们继续封装,将这个代码在系统后代常驻运行,重启时也会自动启动。

+

永久关闭 AWDL

通过下边这个命令,可以把上边的脚本放在后台服务中一直执行,同时跟随系统启动:

+
1
curl -sL https://raw.githubusercontent.com/meterup/awdl_wifi_scripts/main/awdl-daemon.sh | bash
+

恢复 AWDL

关闭后会影响 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
2
alias awdldown='curl -sL https://raw.githubusercontent.com/meterup/awdl_wifi_scripts/main/awdl-daemon.sh | bash'
alias awdlup='curl -s https://raw.githubusercontent.com/meterup/awdl_wifi_scripts/main/cleanup-and-reenable-awdl.sh | bash &> /dev/null'
+ +
+ + + + + + + +
+ + + + + + +
+ +
+ + + +
+ + + + + + + + +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2023/Emotional-expression-in-cultural-differences-between-East-and-West/1.png b/2023/Emotional-expression-in-cultural-differences-between-East-and-West/1.png new file mode 100644 index 0000000000..ffbf89f795 Binary files /dev/null and b/2023/Emotional-expression-in-cultural-differences-between-East-and-West/1.png differ diff --git a/2023/Emotional-expression-in-cultural-differences-between-East-and-West/2.png b/2023/Emotional-expression-in-cultural-differences-between-East-and-West/2.png new file mode 100644 index 0000000000..f8cc0da2f5 Binary files /dev/null and b/2023/Emotional-expression-in-cultural-differences-between-East-and-West/2.png differ diff --git a/2023/Emotional-expression-in-cultural-differences-between-East-and-West/index.html b/2023/Emotional-expression-in-cultural-differences-between-East-and-West/index.html new file mode 100644 index 0000000000..6ad665f9c6 --- /dev/null +++ b/2023/Emotional-expression-in-cultural-differences-between-East-and-West/index.html @@ -0,0 +1,503 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 从《我们的一天》看东西方文化差异中的情感表达 | 贾攀的流水账 + + + + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

贾攀的流水账

+ +
+

Panmax's Blog

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+ + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ 从《我们的一天》看东西方文化差异中的情感表达 +

+ + +
+ + + + +
+ + +

几周前的一个周会上,我提出让大家每人分享一个自己推荐的书影剧,当时一起旁听会议的HR小姐姐推荐了一个美剧,叫「我们的一天(This Is Us)」,她说非常感人,另外一个男同事也随声附和,并说非常适合我这种有两个小孩的人去看,他自己看的时候哭的不要不要的。

+

周末的时候我看了4集这个剧,情节围绕着同一个家庭中同一天出生的兄妹三人来展开,其中一个黑人不是他们的亲兄弟。剧中将他们小时候和他们成人后的场景相结合,故事情节很好,有多条平行的故事线,结尾经常有悬疑可以解开,比如第一集中三个主角是分别拍摄的,但在最后一刻才揭晓他们三个原来是一家人。

+

但实话实说,目前来说这个剧还没有让我掉过泪,相比较而言「请回答1988」是能让我在地铁上哭出来的一部剧。我想这和我所在的文化环境与这两部剧所使用的感人手段不同有关。

+

亚洲人,尤其是中国人都比较内敛、隐忍,更喜欢默默的付出,有话不说出来,不太在公众场合宣泄自己的情绪,在「1988」这部剧中让我奔泪的也是这样的场景。

+

举两个例子,德善生日那一集爸爸最后说的几句话:

+
+

爸爸我也不是一生下来就是爸爸,

+

爸爸也是头一次当爸爸,

+

所以我的女儿稍微体谅一下。

+
+

+

另一个是德善奶奶去世,爸爸一天都嘻嘻哈哈招待来悼念的朋友,直到晚上远在海外的大哥回来,兄弟姐妹到齐了,屋里也只剩下了一家人,爸爸再也抑制不住自己的情绪兄弟几人抱头痛哭。

+

+

但「我们的一天」使用的手法就很直给,在公共场合表达爱、有了问题及时沟通,父母与孩子之间的关系平等交流。不过这也许并不是这部剧没有让我掉泪的原因,这部剧我也只看了4集,还不能这么早下定论,只是基于这几集记录下自己的想法。也许看过后边的部分后会有不同的结论。所谓生长不就是在不断推翻自己曾深信不疑的想法的过程吗?

+

写到最后,我想我没有被「我们的一天」所触动,更大的可能是「1988」更贴近于我的生活,里边的一些场景都是我有体会或者曾经经历过的。

+ +
+ + + + + + + +
+ + + + + + +
+ +
+ + + +
+ + + + + + + + +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2023/Focus-on-the-present-and-relaxation/index.html b/2023/Focus-on-the-present-and-relaxation/index.html new file mode 100644 index 0000000000..6a642e9ac6 --- /dev/null +++ b/2023/Focus-on-the-present-and-relaxation/index.html @@ -0,0 +1,502 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 关注当下与松驰感 | 贾攀的流水账 + + + + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

贾攀的流水账

+ +
+

Panmax's Blog

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+ + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ 关注当下与松驰感 +

+ + +
+ + + + +
+ + +

这篇博客是由我、飞书妙记、ChatGPT和NotionAI共同完成。

+
    +
  1. 我用飞书妙记口述零碎的内容,录了6分钟。
  2. +
  3. 飞书妙记将口述内容转为文字。
  4. +
  5. 我进行了简单的校对和删除无用口语词汇。
  6. +
  7. ChatGPT将这些零碎的内容重新组织成一篇连贯的文章,并进行了适当的补充。
  8. +
  9. 我和NotionAI进行了最终的编辑和润色。
  10. +
+

在工作中,我们总会遇到各种各样的人。有些人能够做出非常出色的工作,让人钦佩不已;而有些人则在同样的工作中表现平平,甚至不及预期。这让人不得不思考:到底是什么让一些人在工作中表现突出呢?

+

我发现,那些比较厉害的人常常具备两种品质,它们分别是松弛感和关注当下。

+

所谓关注当下,就是在该做什么事情的时候,就去做这件事情,而不是被其他琐碎的事情所干扰。

+

我有两个下属,其中一个能力较差,参加会议时总是显得很慌张,总是在赶项目进度,从不听会,实际工作效率很低。相比之下,另一位比较优秀的下属经常表现出专注的态度,即使手头上还有其他紧急事情要处理,也会认真听会积极参与讨论。

+

人们认为一心多用能够提高效率,但是实际上反而会让自己的工作做得更加粗糙,并且效率不高。

+

这给我带来了启示:成功的人必须懂得将注意力集中在当前自己正在做的事情上,这样才能创造最大的价值。尝试同时处理多个任务只会分散自己的注意力,降低自己的效率。

+

其次,我发现厉害的人尝尝具备“松弛感”,他们可以在掌握自己情况的基础上,放松一些,面对压力从容应对。

+

看过美食节目的人应该都知道“盗月社”,其中的一名成员叫朱狒狒,她外表看起来很普通,但是通过观察,我们会发现她总是面对任何挑战都能够从容应对。对于那些对打压自己感到无能为力的人来说,这种气质是非常重要的。她总能以一种松弛的方式去处理一些复杂的问题,并找到一个高效的解决方案。后来我还了解到,朱狒狒是北大毕业的。

+

尽管一个人成功的原因有很多,但是松弛感和关注当下这两个品质确实是让一些人在工作中表现突出的关键。作为普通人,我们也可以通过不断地学习和实践,从中汲取营养,让自己成为一个更加优秀的人。

+ +
+ + + + + + + +
+ + + + + + +
+ +
+ + + +
+ + + + + + + + +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2023/Food-Market/1.jpeg b/2023/Food-Market/1.jpeg new file mode 100644 index 0000000000..ef33a4337f Binary files /dev/null and b/2023/Food-Market/1.jpeg differ diff --git a/2023/Food-Market/10.jpeg b/2023/Food-Market/10.jpeg new file mode 100644 index 0000000000..34e638ba9e Binary files /dev/null and b/2023/Food-Market/10.jpeg differ diff --git a/2023/Food-Market/11.jpeg b/2023/Food-Market/11.jpeg new file mode 100644 index 0000000000..e79b530f28 Binary files /dev/null and b/2023/Food-Market/11.jpeg differ diff --git a/2023/Food-Market/12.jpeg b/2023/Food-Market/12.jpeg new file mode 100644 index 0000000000..b0d74fe672 Binary files /dev/null and b/2023/Food-Market/12.jpeg differ diff --git a/2023/Food-Market/13.jpeg b/2023/Food-Market/13.jpeg new file mode 100644 index 0000000000..aca5743a14 Binary files /dev/null and b/2023/Food-Market/13.jpeg differ diff --git a/2023/Food-Market/14.jpeg b/2023/Food-Market/14.jpeg new file mode 100644 index 0000000000..9154455a82 Binary files /dev/null and b/2023/Food-Market/14.jpeg differ diff --git a/2023/Food-Market/2.jpeg b/2023/Food-Market/2.jpeg new file mode 100644 index 0000000000..c7135c37ba Binary files /dev/null and b/2023/Food-Market/2.jpeg differ diff --git a/2023/Food-Market/3.jpeg b/2023/Food-Market/3.jpeg new file mode 100644 index 0000000000..af37649c1c Binary files /dev/null and b/2023/Food-Market/3.jpeg differ diff --git a/2023/Food-Market/4.jpeg b/2023/Food-Market/4.jpeg new file mode 100644 index 0000000000..bb62262232 Binary files /dev/null and b/2023/Food-Market/4.jpeg differ diff --git a/2023/Food-Market/5.jpeg b/2023/Food-Market/5.jpeg new file mode 100644 index 0000000000..78a48927ed Binary files /dev/null and b/2023/Food-Market/5.jpeg differ diff --git a/2023/Food-Market/6.jpeg b/2023/Food-Market/6.jpeg new file mode 100644 index 0000000000..6889c5a8f2 Binary files /dev/null and b/2023/Food-Market/6.jpeg differ diff --git a/2023/Food-Market/7.jpeg b/2023/Food-Market/7.jpeg new file mode 100644 index 0000000000..ff9402008b Binary files /dev/null and b/2023/Food-Market/7.jpeg differ diff --git a/2023/Food-Market/8.jpeg b/2023/Food-Market/8.jpeg new file mode 100644 index 0000000000..ef3ea10a62 Binary files /dev/null and b/2023/Food-Market/8.jpeg differ diff --git a/2023/Food-Market/9.jpeg b/2023/Food-Market/9.jpeg new file mode 100644 index 0000000000..0ce2c66198 Binary files /dev/null and b/2023/Food-Market/9.jpeg differ diff --git a/2023/Food-Market/index.html b/2023/Food-Market/index.html new file mode 100644 index 0000000000..565423fd80 --- /dev/null +++ b/2023/Food-Market/index.html @@ -0,0 +1,534 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 逛菜市场 | 贾攀的流水账 + + + + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

贾攀的流水账

+ +
+

Panmax's Blog

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+ + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ 逛菜市场 +

+ + +
+ + + + +
+ + +

昨天周五,因为家人都没在北京,所以晚上在公司吃了份轻食才走的。

+

众所周知,轻食吃完后没有任何满足感,而且为了控制体重我已经连续吃了一周轻食,加上白天因为家里的一些糟心事心情很差,晚上到家的时候又在楼下的街边烤串吃了个宵夜。

+

+

串儿有点咸,想到家里冰箱有可乐,撸串儿时没有买喝的,到家后喝了一罐可乐,又看了会美剧。因为是周五总觉得生活意犹未尽,躺下后又开始刷小红书,被算法支配到了11点半。

+

放下手机读了会《回忆爱玛侬》,读完了其中写的很压抑的《彷徨的命运》那一章后已经过0点了。

+

以上这些作死操作,再加上今天白天的午后,感觉很冷想喝点热乎的,于是喝了一杯瑞幸的热拿铁(早上已经喝过了一杯美式),导致久久无法入睡,翻来覆去、来回上了几次厕所之后,放弃了自然入睡的打算,吃了一片艾司唑仑睡去了。

+

第二天早上醒来已经将近九点,本来按照前两天的计划是周六一早开车回老家,但因为一些(长辈上的)家庭矛盾取消了这个计划,窝在被窝里刷手机。

+

拉开窗帘看了一眼,差点晃瞎眼镜。

+

+

这么好的天气不能虚度,刚好前几天听了一期《圆桌派》,聊的是关于「菜市场」的主题,而且中间提到了北京的新源里菜市场,我本人对吃非常感兴趣,也喜欢做菜和逛菜市场,所以打算今天也去菜市场逛逛。

+

查了下去新源里菜市场的路况很堵,又在小红书搜了下北京其他的有名菜市场,发现排名靠前的有个叫百姓菜篮子的菜市场,在百子湾,离我不远,开车只要20多分钟。

+

+

为了今天早饭也为了下周有的吃,起床后煮了锅鸡蛋,放上调料腌制好后,又配着吃了个面包,看了一集《我们的生活》后准备出门,在我起床后收拾的过程中就开始一阵一阵的下雨,所以出门时就考虑是作为休闲骑车过去还是开车过去。

+

+

考虑到我的车已经一周多没动过了,所以最后还是决定开车过去,就当溜溜车了。最终看来这个决定非常明智。

+

出门时天阴阴的,很凉爽,伴随着播放我最喜欢的音乐心情也很好。

+

+

+

开到一半的时候,天气突然大变,下起了瓢泼大雨,很庆幸自己选择了开车。

+

+

开到地点后雨势也减小很多,百姓菜篮子门脸不大,但进去后别有乾坤,是个极长极长的通道,中间、两边挤满了商家,与其说是个菜市场,不如说是个百货市场,里边卖什么的都有。

+

+

+

+

这个菜市场太棒了,但因为我的主要目的是逛,并不是买买买,新家这边装备也不齐全,即便买也只能买些好处理的,最后我买了一斤多蛏子、一斤多白蛤准备到家后白灼,又花1块钱买了根葱和一小块姜。

+

+

还买了几个巨大的桃子,夏天我最喜欢吃的两样水果是西瓜和桃子,路过一个零食小摊,混着买了些小零食,其中的香葱鸡片小饼干是我的童年记忆,虽然小时候吃顶过,现在还是喜欢吃。

+

+

最后出门前又买了个肉蛋堡,香迷糊了。

+

+

+

虽然买的不多,但还是庆幸自己开车来了,如果没有车我肯定拿不了这些东西,感谢我的小车车。

+

到家后把海鲜用白灼的手法处理了一下,真肥啊,在下吧台上吃着海鲜喝着小酒看着小雨很惬意。

+

大家周末愉快呀。

+ +
+ + + + + + + +
+ + + + + + +
+ +
+ + + +
+ + + + + + + + +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2023/IM-consistency/index.html b/2023/IM-consistency/index.html new file mode 100644 index 0000000000..ac44330052 --- /dev/null +++ b/2023/IM-consistency/index.html @@ -0,0 +1,519 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + IM 系统如何保证消息时序的一致性 | 贾攀的流水账 + + + + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

贾攀的流水账

+ +
+

Panmax's Blog

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+ + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ IM 系统如何保证消息时序的一致性 +

+ + +
+ + + + +
+ + +

TT 作为国内 TOP3 的社交应用,IM 是非常核心的功能,下边我来介绍一下 TT 的 IM 是如何保证消息时序的。

+

什么是消息时序?

消息的时序代表的是发送方的意见表述和接收方的语义逻辑理解。如果时序一致性不能保证,可能会导致聊天语义不连贯,容易出现曲解和误会。

+

比如,你给一个小姐姐发送了1、2、3、4、5几句话,小姐姐收到的却是4、5、2、3、1。这个小姐姐一定觉得你是个脑残,直接拉黑了。

+

对于单聊的场景,时序一致性需要保证接收方的接收顺序和发送方的发出顺序一致;

+

对于群聊的场景,时序一致性保证的是群里所有接收人看到的消息展现顺序都一样。

+

如何保证消息时序的一致性?

在讨论这个问题之前,我们先要知道为什么消息时序一致性不容易保证:因为对于后端服务来说,是不同机器、并发处理用户每个发消息请求的。也就是说用户发送过来的消息,被成功处理的先后顺序是不确定的,处理一条消息的内部逻辑非常复杂,举几个最常见的:消息过模型判断是否违规、判断双方用户状态、更新各种未读数等等。如果我们按照服务器处理一条消息成功后的时间将消息推送给对方,那么很有可能对方的接收顺序并不是之前的发送顺序。

+

这种情况下就需要给每一条消息提前分配好一个确定的发送时间点,也可以不是时间,只要是一个可比较大小的值就行,要满足后发送的消息一定比先发送的消息值要大。

+

我们称这个值为「时序基准」,多条消息之间可以根据一个共同的「时序基准」可以来进行比较。

+

接下来的问题就转变为了如何找到一个合适的「时序基准」。

+

获取「时序基准」的几种方式

客户端生成

客户端在发送消息时连同消息再携带一个本地的时间戳或者本地维护的一个序号给到 IM 服务端,IM 服务端再把这个时间戳或者序号和消息一起发送给消息接收方,消息接收方根据这个时间戳或者序号来进行消息的排序。

+

使用客户端时间或序号可能会有以下几个问题:

+
    +
  1. 客户端时钟存在较大不稳定因素,用户可以随时调整时钟导致序号回退等问题。
  2. +
  3. 客户端本地序号如果重装应用会导致序号清零,也会导致序号回退的问题。
  4. +
  5. 类似「群聊」和 「多点登录」这种多客户端场景,存在:物理世界中的同一时间点,不同客户端同时发消息给同一个接收方。
  6. +
+

第3点不太容易理解,用一个例子解释一下:比如同一个群里,多个用户同时发言,多客户端间由于存在时钟不同步的问题,并不能保证客户端带上来的时间是准确的,可能存在群里的用户 A 先发言,B 后发言,但由于用户 A 的手机时钟比用户 B 的慢了半分钟,如果以这个时间作为「时序基准」来进行排序,可能反而导致用户 A 的发言被认为是晚于用户 B 的。

+

IM服务器生成

客户端把消息提交给 IM 服务器后,IM 服务器依据自身服务器的时钟生成一个时间戳,再把消息推送给接收方时携带这个时间戳,接收方依据这个时间戳来进行消息的排序。

+

在实际环境中,IM 服务都是集群化部署,集群化部署也就是许多服务器同时提供服务。

+

虽然多台服务器通过 NTP 时间同步服务,能降低服务集群机器间的时钟差异到毫秒级别,但仍然还是存在一定的时钟误差,而且 IM 服务器规模相对比较大,时钟的统一性维护上也比较有挑战,整体时钟很难保持极低误差,因此一般也不能用 IM 服务器的本地时钟来作为消息的「时序基准」。

+

全局序号生成器

如果有一个全局递增的序号生成器,就能避免多服务器时钟不同步的问题了。IM 服务端就能通过这个序号生成器发出的序号,来作为消息排序的「时序基准」。

+

这种「全局序号生成器」可以通过多种方式来实现,常见的比如 Redis 的原子自增命令 incr,DB 自带的自增 id,或者类似 Twitter 的 snowflake 算法、「时间相关」的分布式序号生成服务等。

+

TT 在用的发号器

TT 没有搭建独立的全局序号生成服务,而是利用 PostgreSQL 强大的 function 能力来实现的。TT 本身也在结构化存储上大规模使用了PostgreSQL,基建相对来说是比较完善。

+

我们自己在 PostgreSQL 内实现了发号器函数,可以根据自己的ID、对方 ID、当前时间、shard 等条件生成集群间毫秒级唯一、保证递增但不保证连续的ID。

+

性能

我们 IM 使用的PostgreSQL集群分了8192个逻辑shard,每个shard每毫秒可生成1024个序号,理论上整个集群每秒最多了生成 (1024 * 1000 * 8192)=83亿个序号,性能上完完全全是够用的。

+

可用性

PostgreSQL 有自身的高可用架构,另外我们还用了 PostgreSQL 强大的逻辑 shard 能力,两个用户间的消息ID通过哈希规则,固定选择其中一个的shard 来生成,即使某个shard真的出了故障也只会影响8192 / 2 =4096分之1的用户。

+

两个用户间的一致性

考虑一个问题,如果不同的数据库实例的时间不一致,两个用户间的聊天顺序是否会有影响?答案是没有影响。

+

两个用户之间的消息ID始终通过一个固定的实例生成的。具体shard选取规则为:

+
1
(uid + other_uid) % shard_num
+

通过以上规则,可以保证无论是用户A发给用户B的,还是用户B发给用户A的消息,都可以路由到同一个shard上。

+

这相当于两个用户间的消息ID是基于同一个单机的发号器来生成的,不会由于不同机器时间不一致而造成消息顺序错乱的问题。

+

群消息

群聊消息的序号是以群的唯一 ID 计算哈希后,找到对应数据库 shard 来生成的。也就是说,多个用户在同一个群内的发言也是通过同一个发号器来生成序号,同一个群内的消息时序可以得到保证。

+

我们的精度也相对更高。据我所知,微博和微信的消息只能做到秒间有序,而我们可以做到毫秒间有序(然并卵)。

+ +
+ + + + + + + +
+ + + + + + +
+ +
+ + + +
+ + + + + + + + +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2023/IM-storage-design/0.png b/2023/IM-storage-design/0.png new file mode 100644 index 0000000000..c2d65aa22e Binary files /dev/null and b/2023/IM-storage-design/0.png differ diff --git a/2023/IM-storage-design/1.png b/2023/IM-storage-design/1.png new file mode 100644 index 0000000000..dac313c80e Binary files /dev/null and b/2023/IM-storage-design/1.png differ diff --git a/2023/IM-storage-design/2.png b/2023/IM-storage-design/2.png new file mode 100644 index 0000000000..900800f19b Binary files /dev/null and b/2023/IM-storage-design/2.png differ diff --git a/2023/IM-storage-design/3.png b/2023/IM-storage-design/3.png new file mode 100644 index 0000000000..b07f31a532 Binary files /dev/null and b/2023/IM-storage-design/3.png differ diff --git a/2023/IM-storage-design/index.html b/2023/IM-storage-design/index.html new file mode 100644 index 0000000000..331e842a28 --- /dev/null +++ b/2023/IM-storage-design/index.html @@ -0,0 +1,550 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + IM 系统存储设计 | 贾攀的流水账 + + + + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

贾攀的流水账

+ +
+

Panmax's Blog

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+ + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ IM 系统存储设计 +

+ + +
+ + + + +
+ + +

为了查看历史消息或者暂存离线消息,大部分 IM 系统都需要对消息进行服务端存储。下面以一对一的单聊为例介绍一下业界一般是如何设计 IM 消息存储方案的,然后再介绍下 TT 具体是如何做的,有什么区别。

+

索引和内容独立存储

单聊消息的参与方有两个:

+
    +
  • 发送方
  • +
  • 接收方
  • +
+

收发双方的历史消息是相互独立的。我用个例子解释一下:

+

女神给你发消息说:「今天七夕,给我发个7777红包,我要截图发朋友圈,一会还你」。你毫不犹豫给女神发了过去。女神在截图前将「今天七夕,给我发个7777红包……」这句话进行了删除。

+

相互独立的意思是说,一方删除消息不影响另一方的展示,女神那一侧删了,你这一侧是不受影响的,如果女神赖账不还,你可以把你这边的完整对话记录拿出来和她对峙。

+

基于以上逻辑,在设计数据库表结构时我们需要为收发双方维护各自的索引记录。

+

由于收发双方看到的消息内容实际是一致的,我们没有必要将内容存储两次,所以可以有一个表来独立存储消息内容。

+

「消息内容表」存储消息纬度的基本信息,如:

+
    +
  • 消息ID
  • +
  • 消息内容
  • +
  • 消息类型
  • +
  • 消息时间
  • +
+

收发双方的「消息索引表」通过唯一的消息 ID 来和消息内容进行关联,同时还要有一个枚举字段来记录这是条发送消息还是接收消息。

+

假设用户123给用户456发送一条消息,消息存储在关系型数据库中,上边涉及的两张表大致如下:

+

+

+

123给456发了一条「你好」的消息,这个动作会在消息内容表中存储一条消息,这条消息的 ID 为1024。

+

同时往索引表里存储两条数据:

+
    +
  1. 用户ID 为123,另一方用户 ID 为456,这是条发出消息,消息 ID 为1024
  2. +
  3. 用户ID 为456,另一方用户 ID 为123,这是条接收消息,消息 ID 为1024
  4. +
+

业界也常将消息的发出和接收这两个纬度抽象为发件箱和收件箱。

+

联系人列表

一般 IM 系统还需要一个最近联系人列表,来让互动双方快速查找需要聊天的对象,联系人列表一般还会携带两人最近一条聊天消息用于展示。

+

+

继续以 123 给 456 发消息为例,除了在内容表和索引表插入记录,还会更新各自的最近联系人表。上图中我们将 用户 ID=123 && 另一方用户 ID=456用户 ID=456 && 另一方用户 ID=123 的两行数据中的最后一条消息 ID 字段更新为 1024。为了便于客户端排序和展示,很多时候我们还会在最近聊系人表中冗余其他信息,如最后一条消息时间。

+

在大部分业务场景中,如果 123 是第一次给 456 发消息,会在发送消息的时候通过其他数据(如互关、好友等)校验双方好友状态,校验通过后给双方创建出联系人记录,这一点 TT 和常规做法略有区别,后文会做介绍。

+

联系人列表和消息索引表的区别如下:

    +
  • 联系人列表只更新存储收发双方的最新一条消息,不存储两人所有的历史消息
  • +
  • 联系人表的使用场景用于查询某一个人最近的所有联系人,是用户全局维度
  • +
  • 消息索引表的使用场景一般用于查询收发双方的历史聊天记录,是聊天会话维度
  • +
  • 收发一条消息时
      +
    • 联系人列表为更新操作
    • +
    • 消息索引表为插入操作
    • +
    +
  • +
+

TT 中的 IM 存储设计

消息表

TT 中将索引表和内容表进行了合并成了一张消息表,同时通讯录表承担了更多的工作。

+

+

要查询 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条数据,对于单方可见的消息,我们在消息的一个额外字段中进行描述即可。

+

架构本质上是一个需要权衡的过程,这种模式有优点的同时也有缺点。最大的缺点就是使用不够灵活,索引效率不够高效。另一个缺点是发送系统提示类的消息时,只能通过关联双方 ID 来展示在双方的聊天列表中,且同一条系统消息无法复用。

+

使用合并后这种方式,在底层消息分片存储上也相对更复杂一些。有索引表的情况下,我们拉取一个用户和另一个用户的记录,不管是收发消息,只需要根据 userID + otherUserID 进行查询就够了。刚刚也提到,这种情况下我们可以将索引数据按照 userID 进行分片,一个用户所有索引数据落在同一个分片上,消息内容表根据消息ID 进行分片。获取到消息索引数据后,根据数据中的消息 ID 进行点查询来获取消息内容,效率很高。

+

将索引表和内容表合并后,考虑到收发双法都会使用同一份数据,所以不建议使用用户 ID 进行分片,而是依然用消息 ID 分片,然后将发送方 ID 和接收方 ID 做一个联合索引。在查询双方聊天记录时,需要业务并行查询所有分片节点,然后在内存中进行排序。在我们的场景中,公司将 RocketDB 进行了二次封装,实现了一个自研的高性能关系数据库:TTDB,此类查询在 DB 中完成,不需要在业务代码内处理这个情况,查询效率也很高。TTDB 涉及很多 DB 方面的底层架构,超出了我的知识边界,就不过多进行介绍了。

+

会话表

上文中介绍的联系人表在 TT 中叫 Conversation:会话,两个用户能否发消息就判断两个用户之间有没有会话记录。

+

会话是在用户形成互关、配对等可以聊天的关系时,由内部服务提前创建好的。与常规方式不同,常规方式是在用户建第一次发消息时创建联系人。

+

当一个用户给另一个用户发消息时,我们只需要校验有没有会话,没有会话就认为这是一个无效请求,直接拒绝掉。

+

联系人表的必要性

最后思考一个问题,在有消息索引表或者 TT 中的消息表的情况下,为什么还需要联系人表(或会话表)?

+

联系人表或者会话表的必要性主要考虑以下几点:

+

1. 方便按最后聊天时间列出所有最近联系过的人

回想一下微信中的聊天列表页,列表中的顺序按照两个用户间最后一条消息时间进行倒排,同时展示最后一条消息内容。如果没有联系人表,我们需要通过消息索引表按照 userID 和 otherUserID 进行分组(group by),然后按时间倒排取(order by)第一条,性能会非常差。

+

2. 消息未读数维护

正常来说每条消息是有已读、未读字段的,如果要统计未读消息的数量,确实可以通过 SQL 进行未读消息的 count 来得到,但这样也是效率很差,通常的做法是在联系人表上冗余一个未读数字段。

+

3. 聊天 cell 的单独控制

举个例子,在微信中我可以将对方置为隐藏或删除,在没有联系人表的情况下很难实现。

+ +
+ + + + + + + +
+ + + + + + +
+ +
+ + + +
+ + + + + + + + +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2023/Motivation-to-work/1.jpeg b/2023/Motivation-to-work/1.jpeg new file mode 100644 index 0000000000..b9da2839b9 Binary files /dev/null and b/2023/Motivation-to-work/1.jpeg differ diff --git a/2023/Motivation-to-work/2.jpeg b/2023/Motivation-to-work/2.jpeg new file mode 100644 index 0000000000..3872c65168 Binary files /dev/null and b/2023/Motivation-to-work/2.jpeg differ diff --git a/2023/Motivation-to-work/3.jpeg b/2023/Motivation-to-work/3.jpeg new file mode 100644 index 0000000000..0bfe192511 Binary files /dev/null and b/2023/Motivation-to-work/3.jpeg differ diff --git a/2023/Motivation-to-work/index.html b/2023/Motivation-to-work/index.html new file mode 100644 index 0000000000..0351fc8508 --- /dev/null +++ b/2023/Motivation-to-work/index.html @@ -0,0 +1,501 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 早晨上班的动力 | 贾攀的流水账 + + + + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

贾攀的流水账

+ +
+

Panmax's Blog

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+ + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ 早晨上班的动力 +

+ + +
+ + + + +
+ + +

我上班有个习惯是比其他人至少早45分钟到公司,可以利用这段时间安静的写会代码或者学习一会。

+

不知道大家上班的动力都是什么,说起来你可能不信,促使我早早来公司的一个比较大的动力是「吃」。

+

就拿最近3年来说,之前在光华路办公的时候,那时候公司提供自助早餐,每天到公司后我都会为自己精心制作一份早餐,我会在上班路上想好今天的早餐如何搭配:吃几个煎蛋、面包片要烤到什么程度、今天的沙拉要用哪种酱等等。

+

+

+

+

后来公司搬到了望京,早上不再有自助早餐,又开始寻摸新的早餐吃食,于是发现了公司对面的汉堡王,办月卡后可以用非常低的价格买到早餐汉堡和一杯咖啡,而且汉堡种类很多,所以那段时间的动力变成了今天吃哪款汉堡。再后来大概是疫情期间服务很差的原因没有再续汉堡月卡了,又开始琢磨其他的东西吃,中间尝试过便利蜂的早餐、速食鸡胸肉等等。

+

关于汉堡王可以看这个:https://jiapan.me/2022/recent-breakfast-burger-king/

+

今年年初的时候开始换着花样在网上买各种早餐面包,不过较吸引我的并不单纯是面包,更多是来自到公司后用公司咖啡机的浓缩咖啡兑上冰农夫山泉后得到的冰美式的魅力,冰美式跟各类面包很搭。最近的搭配是山姆的杂粮奶酪包,吃到奶酪夹心的时候非常幸福,因为我到公司后整个楼层几乎没人,在大部分没有紧急的事情要处理的情况下,我会也不开电脑,也不看手机,在工位上细细品尝我的面包和咖啡,安静地吃顿早餐。

+

人,总该有点盼头吧。

+

最后再说说为什么我非要到公司后才吃早饭,而不能在家吃了再来公司?

+

这是个悲伤的故事,因为我有慢性甲状腺炎,每天早上要吃优甲乐。这个药的限制是吃药后半小时时间内不能吃东西,所以迫不得已只能到了公司才能吃早饭。

+ +
+ + + + + + + +
+ + + + + + +
+ +
+ + + +
+ + + + + + + + +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2023/Portfolio-Theory-and-Risk-Parity-Model/index.html b/2023/Portfolio-Theory-and-Risk-Parity-Model/index.html new file mode 100644 index 0000000000..ea0c28a284 --- /dev/null +++ b/2023/Portfolio-Theory-and-Risk-Parity-Model/index.html @@ -0,0 +1,550 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 投资组合理论 && 风险平价模型 | 贾攀的流水账 + + + + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

贾攀的流水账

+ +
+

Panmax's Blog

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+ + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ 投资组合理论 && 风险平价模型 +

+ + +
+ + + + +
+ + +

投资组合理论和风险平价模型是两种与投资组合管理相关的重要概念,是金融领域中用于优化投资组合的方法。

+

投资组合理论

投资组合理论是由美国经济学家哈里·马科维茨(Harry Markowitz)于20世纪50年代提出的理论框架,也被称为现代投资组合理论(Modern Portfolio Theory,MPT)。该理论旨在帮助投资者在风险和收益之间取得最佳平衡。投资组合理论的核心思想是通过将多种资产组合在一起,以最小化给定预期收益水平下的投资组合风险,或在给定风险水平下最大化预期收益。

+

辅助理解

想象一下,你有一个盒子,里面装着各种不同的玩具,比如娃娃、小车和积木。每个玩具就像是不同的投资。现在,假设你想保护你的玩具并确保它们的价值随着时间增长。

+

投资组合理论就像是一种决定你的盒子里应该有多少个不同玩具的方法。你要选择适当的玩具组合,这样你才能获得最好的结果。

+

但是,这里有个诀窍:不同的玩具有不同的风险和回报。有些玩具可能更有价值,但风险也更大,而其他的可能更安全,但增长速度较慢。所以你需要决定你愿意承担多少风险。

+

投资组合理论帮助你找到合适的平衡点。它建议你选择一种玩具组合,这样你就可以把风险分散开来。这意味着如果一个玩具表现不好,其他的玩具仍然可以让你获得回报。

+

简而言之,投资组合理论就是帮助你选择合适的玩具组合,以平衡风险,并让你的盒子里的玩具保持增值。

+

如何工作

假设你有1000美元,你有三种不同的投资选项:股票、债券和黄金。

+

现代投资组合理论认为,投资者可以通过合理地分配资金来平衡风险和回报。

+

首先,你需要了解每种投资的预期回报和风险。假设股票的预期回报是10%,债券是5%,黄金是3%。同时,股票的风险最高,债券次之,黄金的风险最低。

+

现代投资组合理论建议你根据你的风险承受能力和目标来分配资金。假设你对风险比较保守,你可以将60%的资金分配给债券,30%分配给股票,剩下的10%分配给黄金。

+

通过这样的分配,你在投资组合中平衡了风险和回报。债券的较高配比可以提供稳定的回报,股票的适度配置可以获得更高的回报,黄金的配置可以提供一定的保值功能。

+

现代投资组合理论的关键思想是通过将资金分配到不同的资产上,以实现风险的分散化。这样,即使某个资产表现不佳,其他资产仍可以为你的投资组合提供回报。

+

优点

    +
  1. 风险分散:投资组合理论强调通过将不同资产以适当的权重组合在一起,实现风险的分散化,从而降低整体投资组合的风险。
  2. +
  3. 预期回报最大化:投资组合理论帮助投资者在给定风险水平下,寻找最优的资产配置方式,以最大化预期回报。
  4. +
  5. 考虑相关性:投资组合理论考虑资产之间的相关性,通过选择不同相关性的资产组合,可以实现更有效的投资组合。
  6. +
+

局限性

    +
  1. 基于历史数据:投资组合理论通常基于历史数据来估计资产的预期回报和风险,但历史表现不一定能准确预测未来。
  2. +
  3. 忽略非系统风险:投资组合理论主要关注系统性风险,即与整个市场相关的风险,而忽略了非系统性风险,即与特定公司或行业相关的风险。
  4. +
  5. 需要大量数据和计算:实施投资组合理论需要大量的数据和计算,包括资产的历史表现、相关性矩阵等,这可能对个体投资者或资源有限的投资者来说是一个挑战。
  6. +
+

风险平价模型

风险平价模型(Risk Parity Model)是一种投资组合管理方法,旨在通过平等分配投资组合中不同资产的风险,实现更平衡的风险暴露。与传统的投资组合管理方法相比,风险平价模型更加关注风险分散和资产间的相关性

+

风险平价起源自一个目标收益率为10%、波动率为10%~12%的投资组合,是美国桥水创始人瑞·达利欧在1996年创立的一个投资原则,既全天候资产配置原则。

+

辅助理解

现在,想象一下你有一张画纸,上面有很多不同的颜色。每种颜色就像是投资中的不同资产,比如红色代表股票,蓝色代表债券,黄色代表房地产等等。

+

风险平价模型就是一种方法,让你在画纸上均匀涂上不同的颜色。这样,每种颜色(也就是每种资产)都有相同的风险,就像画纸上每个区域的颜色一样多。

+

为什么要这样做呢?因为不同的颜色(或资产)有不同的风险和回报。有些颜色可能非常亮,表示它们的风险更高,但潜在回报也更大。而有些颜色可能相对较暗,表示它们的风险较低,但潜在回报也较小。

+

风险平价模型帮助你确保你的画纸上每个颜色(或资产)的风险都是一样的。这样,如果一个颜色表现不好,其他颜色仍然可以给你带来回报。

+

简而言之,风险平价模型就是让你在画纸上均匀地涂上不同的颜色,以确保不同资产的风险是平衡的,并让你的投资更加稳定。

+

如何工作

假设你有1000美元,你想将其投资于两种不同的资产:股票和债券。

+

股票通常风险较高,但潜在回报也更高,而债券被认为更安全,但回报较低。

+

在风险平价模型中,你不仅仅是平均分配你的资金到股票和债券上(各500美元),而是根据每种资产的风险来分配你的投资。

+

假设股票的风险更高,你决定将70%的投资分配给债券,30%分配给股票。这种分配是根据每种资产的风险贡献应该相等的理念来确定的。

+

通过这样做,你在投资组合中平衡了风险。如果股票表现不佳,对债券的较高配置可以帮助抵消损失,并为你的整体投资提供更稳定性。另一方面,如果股票表现出色,较小的配置也不会对整体投资组合的表现产生太大影响。

+

风险平价模型旨在通过考虑不同资产的风险来实现资产间的平衡。它帮助你进行投资多样化,并更有效地管理风险。

+

优点

    +
  1. 风险平衡:风险平价模型通过平衡不同资产的风险贡献,实现投资组合的风险均衡。这可以帮助投资者降低对任何单个资产的依赖,从而提高整体投资组合的稳定性。
  2. +
  3. 简单易懂:风险平价模型相对较简单,容易理解和实施。它不需要大量的数据和计算,适用于个体投资者或资源有限的投资者。
  4. +
+

局限性

    +
  1. 忽略预期回报:风险平价模型关注风险的平衡,但忽略了资产的预期回报。这可能导致在追求风险均衡的同时牺牲了潜在的高回报机会。
  2. +
  3. 对某些资产不适用:风险平价模型在处理某些特殊资产类别(如复杂衍生品)时可能存在困难,因为这些资产的风险无法简单地衡量和比较。
  4. +
+

投资组合理论与风险平价模型的主要区别

目标和重点:

    +
  • 投资组合理论的目标是在给定风险水平下,最大化投资组合的预期回报。它关注如何通过资产配置来实现最佳的风险-回报权衡。
  • +
  • 风险平价模型的目标是平衡不同资产在整个投资组合中的风险贡献。它强调每个资产对总体风险的贡献应该是相等的。
  • +
+

风险分散方法

    +
  • 投资组合理论通过将不同风险和回报特征的资产组合在一起,以实现风险的分散化。它考虑资产之间的相关性,并通过优化资产权重来达到风险分散的目标。
  • +
  • 风险平价模型通过平衡不同资产的风险贡献来实现投资组合的风险分散。它将风险分配给各个资产,以确保它们在整个投资组合中对总体风险的贡献相等。
  • +
+

考虑因素:

    +
  • 投资组合理论考虑了预期回报、风险和资产之间的相关性。它通过优化资产配置来平衡这些因素,以实现最佳的风险-回报组合。
  • +
  • 风险平价模型更关注风险方面,特别是资产的风险贡献。它通过平衡不同资产的风险贡献来实现风险均衡,而对预期回报的考虑相对较少。
  • +
+

复杂性:

    +
  • 投资组合理论在实践中通常需要更多的数据和计算,包括资产的历史表现、相关性矩阵等。它可能需要更多的复杂模型和技术分析来确定最佳的资产配置。
  • +
  • 风险平价模型相对较简单,不需要大量数据和复杂计算。它可以作为一种直观且易于实施的方法,适用于个体投资者或资源有限的投资者。
  • +
+

关注点:

    +
  • 投资组合理论关注整个投资组合的特征和表现,它试图找到最优的资产配置,以实现预期回报和风险的最佳权衡。
  • +
  • 风险平价模型更关注投资组合内部的风险分散,它强调平衡不同资产的风险贡献,以降低整体投资组合的风险。
  • +
+ +
+ + + + + + + +
+ + + + + + +
+ +
+ + + +
+ + + + + + + + +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2023/Regression-model-vs-Classification-model/index.html b/2023/Regression-model-vs-Classification-model/index.html new file mode 100644 index 0000000000..4609a0f5a6 --- /dev/null +++ b/2023/Regression-model-vs-Classification-model/index.html @@ -0,0 +1,531 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 回归模型 vs 分类模型 | 贾攀的流水账 + + + + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

贾攀的流水账

+ +
+

Panmax's Blog

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+ + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ 回归模型 vs 分类模型 +

+ + +
+ + + + +
+ + +

在机器学习中,回归模型和分类模型是两种常见的预测模型,它们的主要区别在于其预测目标和输出类型。

+

预测目标

    +
  • 回归模型的预测目标是连续数值。
      +
    • 回归模型用于预测输出变量的数值,例如房价预测或股票价格预测。
    • +
    • 回归模型试图建立输入特征与输出值之间的数值关系。
    • +
    +
  • +
  • 分类模型的预测目标是离散类别。
      +
    • 分类模型用于将输入实例分配到预定义的类别中,例如垃圾邮件分类或图像识别。
    • +
    • 分类模型试图学习输入特征与类别之间的关系。
    • +
    +
  • +
+

输出类型

    +
  • 回归模型的输出是连续的。
      +
    • 回归模型生成一个实数或浮点数作为预测结果,可以是任意精度的数值。
    • +
    • 例如,预测某个人的年龄可以是一个实数,如25.6岁。
    • +
    +
  • +
  • 分类模型的输出是离散的。
      +
    • 分类模型预测样本属于预定义类别的概率或直接预测样本的类别标签。
    • +
    • 例如,对于垃圾邮件分类,模型的输出可以是”垃圾邮件”或”非垃圾邮件”。
    • +
    +
  • +
+

小孩子都能懂的回归模型解释

回归模型就像是一个预测机器,可以帮助我们猜测事物的未来。假设你喜欢吃冰淇淋,而冰淇淋的价格通常会随着天气变化而变化。现在,我们可以观察天气情况和冰淇淋的价格,然后用这些信息来猜测未来的价格。

+

比如,如果明天是个炎热的夏天,天气很热,那么冰淇淋的价格可能会比较高,因为很多人想要买冰淇淋来解暑。相反,如果明天是个寒冷的冬天,天气很冷,那么冰淇淋的价格可能会比较低,因为很少人会想要吃冰淇淋。

+

回归模型就是通过观察过去的天气和冰淇淋价格的关系,来预测将来的价格。它会考虑到很多因素,例如天气、季节和需求,然后给我们一个猜测的价格。虽然它不能百分之百准确地猜测价格,但它可以给我们一个大概的预测,帮助我们做决策。

+

小孩子都能懂的分类模型解释

分类模型就像是一个分类小助手,可以帮助我们将东西归类。想象一下,你有很多玩具,例如球、娃娃和积木。现在,你想要把它们分类整理,把球放在一起、把娃娃放在一起,积木也放在一起。

+

分类模型就是帮助我们做这个分类工作的机器。它会观察玩具的特点,比如形状、颜色和材质,然后根据这些特点把它们分成不同的类别。就像是在玩玩具时,你可以根据它们的外观和特点来决定它们应该放在哪个盒子里。

+

分类模型可以帮助我们在很多不同的情况下进行分类,比如识别动物、区分水果、辨别颜色等。它可以根据事物的特征将它们分成不同的组别,让我们更好地理解和组织世界。

+

应用场景

回归模型的应用场景:

    +
  1. 房价预测:根据房屋的特征(如面积、卧室数量、地理位置等),预测房屋的价格。
  2. +
  3. 销售量预测:根据过去的销售数据、广告投入和季节性因素,预测未来某个产品的销售量。
  4. +
  5. 股票价格预测:根据股票过去的价格数据、市场指标和新闻事件,预测股票的未来走势。
  6. +
  7. 气候模型:根据历史气象数据、大气压力和温度等因素,预测未来的天气情况。
  8. +
  9. 医学研究:根据患者的临床特征和生物标记物,预测患者的疾病风险或治疗效果。
  10. +
+

分类模型的应用场景:

    +
  1. 垃圾邮件分类:根据电子邮件的内容、发件人和其他特征,将电子邮件分为垃圾邮件和非垃圾邮件。
  2. +
  3. 图像识别:根据图像的特征和内容,将图像分类为不同的对象或场景,如猫、狗、汽车或风景。
  4. +
  5. 疾病诊断:根据患者的症状、体征和医学测试结果,将患者的疾病分类为不同的类别,如心脏病、癌症或糖尿病。
  6. +
  7. 情感分析:根据文本的情感特征,将文本分类为积极、消极或中性的情感。
  8. +
  9. 客户细分:根据客户的行为、偏好和购买历史,将客户分为不同的细分群体,以便进行个性化营销。
  10. +
+ +
+ + + + + + + +
+ + + + + + +
+ +
+ + + +
+ + + + + + + + +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2023/Stuffy-gourd/index.html b/2023/Stuffy-gourd/index.html new file mode 100644 index 0000000000..dddfc447a7 --- /dev/null +++ b/2023/Stuffy-gourd/index.html @@ -0,0 +1,491 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 闷嘴葫芦 | 贾攀的流水账 + + + + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

贾攀的流水账

+ +
+

Panmax's Blog

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+ + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ 闷嘴葫芦 +

+ + +
+ + + + +
+ + +

我们公司早些时候就调整了年终奖的发放制度,拆成4个季度随工资发放,好处显而易见,有离职想法的同学不需要再漫长的等待年终奖了,而且本季度内离职只要表现不太差,离开后在该季度奖金发放时也基本能拿到自己应得的部分。

+

因为奖金和绩效挂钩,在每次发奖金前都需要和组员完成结果同步,我们是每个月的第5个工作日发工资,所以下周一是我们8月的发薪日也是Q2季度的奖金发放日,这就意味着需要在本周内完成绩效结果的同步和沟通。

+

每次沟通由实线和虚线两个Lead一起沟通,之前我是带虚线团队,虽然也需要参与沟通,但只需要辅助输出就行,不需要讲太多,每次听实线逻辑清晰的表达,都庆幸多亏自己不用长篇阔论去说。

+

这次沟通我需要作为实线TL和组内另一个虚线TL跟下边同学沟通,但我发现我还是没有太多可说的,我缺乏绩效沟通时的基本技巧,加上平时的思考比较少,(而且不擅长画饼),很容易冷场,在对方问一些刁钻或者我没有考虑过的问题的时候脑子里很容易就出现一片空白或者像一团浆糊,这让我想到读过的「一句顶一万句」这部小说中经常用的闷嘴葫芦这个词。

+

还好和我配合的这位虚线TL足够有经验,基本都是他来对线,我继续做辅助,很幸运每次我遇到困难时都会有贵人相助。

+

另外因为上个季度离职的同学比较多,只需要和4位同学沟通就行,我这个资深 I 人在每沟通完一个人后都很疲惫,需要自己待一会来充电,想到下个季度要沟通8个同学就有些头大🥲

+ +
+ + + + + + + +
+ + + + + + +
+ +
+ + + +
+ + + + + + + + +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2023/The-4-most-important-business-financial-indicators/index.html b/2023/The-4-most-important-business-financial-indicators/index.html new file mode 100644 index 0000000000..9e753a4f61 --- /dev/null +++ b/2023/The-4-most-important-business-financial-indicators/index.html @@ -0,0 +1,603 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 4个最重要的企业财务指标 | 贾攀的流水账 + + + + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

贾攀的流水账

+ +
+

Panmax's Blog

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+ + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ 4个最重要的企业财务指标 +

+ + +
+ + + + +
+ + +

当谈到财务指标时,净资产收益率、毛利率、净利率和市盈率是经常被提及的几个指标,这些也是巴菲特最看重的4个指标。

+

巴菲特是历史上最伟大的价值投资者,他的交易逻辑的核心是:寻找优质企业并长期持有这些企业的股票。如何判断一个企业是否优质?这时就可以依据上边提到的4个指标来进行判断了。

+

净资产收益率(Return on Equity,ROE)

这个指标可以帮助投资者评估企业的盈利能力和管理效率

+

净资产收益率是用来衡量企业利润与其净资产之间关系的指标。

+

它反映了企业利用所有者权益实现的盈利能力。

+

净资产收益率的计算公式为:净资产收益率 = 净利润 / 平均净资产。

+
    +
  • 净利润

    +

    指的是企业在一定时期内扣除所有成本和费用后所剩下的利润。

    +
      +
    • 它是企业经营活动的最终利润。
    • +
    • 净利润可以通过企业的损益表(或利润表)中的数据来计算得出。
    • +
    +
  • +
  • 平均净资产

    +

    是指企业在一定期间内的

    +

    资产净值的平均值

    +

    +
      +
    • 资产净值指的是企业的资产减去负债,也可以理解为企业的所有者权益或净资产
    • +
    • 平均净资产则是将期初净资产和期末净资产相加后除以2,表示在该期间内的平均资产净值。
    • +
    +
  • +
+

举例

假设有一家公司,在某一年度的净利润为500万元,期初净资产为2000万元,期末净资产为2500万元。

+

首先,计算平均净资产: 平均净资产 = (期初净资产 + 期末净资产)/ 2 = (2000万元 + 2500万元)/ 2 = 2250万元

+

接下来,计算净资产收益率: 净资产收益率 = 净利润 / 平均净资产 = 500万元 / 2250万元 ≈ 0.2222 或 22.22%

+

在这个例子中,公司的净利润为500万元,期初净资产为2000万元,期末净资产为2500万元。

+

通过计算,得到平均净资产为2250万元。

+

最后,通过将净利润除以平均净资产,得到净资产收益率为22.22%。

+

这个例子中的数据说明了净资产收益率的计算方法。净资产收益率衡量了企业在一定期间内每单位净资产所创造的净利润水平。在这种情况下,净资产收益率为22.22%,表示该公司在该年度内每单位净资产创造了22.22%的净利润。

+

为什么净资产收益率可以评估企业的盈利能力和管理效率?

    +
  1. 盈利能力评估:净资产收益率反映了企业在一定期间内每单位净资产所创造的净利润水平。较高的净资产收益率意味着企业能够有效地利用其资产创造盈利,表明企业在经营活动中取得了较高的利润回报。相反,较低的净资产收益率可能意味着企业的盈利能力较弱,资产利用效率不高。
  2. +
  3. 管理效率评估:净资产收益率反映了企业管理层对资产的运营和配置能力。较高的净资产收益率表明企业管理层能够有效地管理和运营资产,实现更高的利润水平。这可能反映了企业在生产、销售、成本控制等方面的优秀管理能力。相反,较低的净资产收益率可能暗示企业的管理效率较低,资产配置和运营方面存在问题。
  4. +
+
+

毛利率(Gross Profit Margin)

这个指标可以帮助投资者了解企业的盈利能力和生产成本控制情况

+

毛利率是用来衡量企业销售产品或提供服务后的毛利润与销售收入之间的关系的指标。

+

它反映了企业在销售过程中所保留的利润比例。

+

毛利率的计算公式为:毛利率 = 毛利润 / 销售收入。

+
    +
  • 毛利润

    +

    是指企业在销售产品或提供服务后剩余的销售收入减去与销售直接相关的成本。

    +
      +
    • 它表示企业在核心业务活动中所保留的利润。
    • +
    • 毛利润 = 销售收入 - 与销售直接相关的成本
    • +
    +
  • +
  • 销售收入

    +

    是指企业在一定时期内通过销售产品或提供服务所获得的总收入。

    +
      +
    • 它代表了企业主要经营活动的收入来源,可以在企业的损益表中找到。
    • +
    +
  • +
+

如果一个行业的毛利率低于20%,那么几乎可以断定这个行业存在着过度竞争。

+

净利率(Net Profit Margin)

这个指标可以帮助投资者评估企业的盈利能力和经营效率

+

净利率是用来衡量企业净利润与销售收入之间关系的指标。

+

它反映了企业在销售过程中实现的净利润比例。

+

净利率的计算公式为:净利率 = 净利润 / 销售收入。

+

毛利润和净利润的区别举例?

假设有一家制造公司,它生产并销售手机。在某一年度,公司的销售收入为1000万元。与销售直接相关的成本包括原材料、直接劳动和制造费用,总计为600万元。此外,公司还有其他间接费用和费用,如销售费用、管理费用和利息费用等,总计为200万元。

+

根据上述数据,我们可以计算毛利润和净利润:

+

毛利润 = 销售收入 - 与销售直接相关的成本 = 1000万元 - 600万元 = 400万元

+

净利润 = 毛利润 - 其他间接费用和费用 = 400万元 - 200万元 = 200万元

+

在这个例子中,公司的销售收入为1000万元,与销售直接相关的成本为600万元,其他间接费用和费用为200万元。

+

毛利润表示在销售过程中,公司通过销售所保留的利润。在这种情况下,公司的毛利润为400万元,即销售收入减去与销售直接相关的成本。

+

净利润则是在扣除所有成本和费用后所得到的最终利润。在这个例子中,公司的净利润为200万元,即毛利润减去其他间接费用和费用。

+

毛利润关注销售过程中所保留的利润,而净利润则考虑了所有与经营活动相关的费用和收入。

+

毛利率与净利率的区别?

毛利率和净利率是两个常用的财务指标,用于衡量企业的盈利能力。它们之间的区别在于考虑的成本因素不同。

+
    +
  1. 毛利率是企业销售产品或提供服务后的毛利润与销售收入之间的比例关系。毛利润是指销售收入减去直接与销售相关的成本,例如生产成本、原材料成本和直接人工成本等。毛利率的计算公式为:毛利率 = (销售收入 - 销售成本)/ 销售收入。毛利率衡量了企业从核心业务活动中获得的利润比例,它反映了企业在销售过程中所保留的利润比例。
  2. +
  3. 净利率是企业净利润与销售收入之间的比例关系。净利润是指销售收入减去所有成本和费用,包括销售成本、管理费用、利息费用和税费等。净利润是企业最终实现的利润。净利率的计算公式为:净利率 = 净利润 / 销售收入。净利率衡量了企业在销售过程中实现的净利润比例,它反映了企业在营运活动中的盈利能力。
  4. +
+

总结起来,毛利率关注的是销售收入和与销售直接相关的成本之间的关系,它衡量了企业从核心业务中获得的利润比例。而净利率则考虑了所有成本和费用,包括销售成本以外的费用,衡量了企业在所有经营活动中实现的净利润比例。净利率相对于毛利率更全面地反映了企业的盈利能力和经营效率。

+
+

市盈率(Price-to-Earnings Ratio,P/E Ratio)

市盈率是衡量股票相对于每股盈利的价格的指标。

+

它是投资者评估一家公司的股票是否被低估或高估的重要指标。

+

市盈率的计算公式为:市盈率 = 股票市场价格 / 每股税后收益。

+
    +
  • 股票市场价格指的是股票在市场上的交易价格,也就是投资者购买或出售股票所需支付或获得的价格。

    +
  • +
  • 每股税后收益

    +

    是指企业每股普通股的税后净利润,也可以理解为每一股票所对应的盈利。

    +
      +
    • 计算公式为:企业的净利润 / 总发行的普通股数量
    • +
    +
  • +
+

较高的市盈率可能意味着市场对该股票有较高的期望和溢价,而较低的市盈率可能意味着市场对该股票的期望较低。

+

举例

假设有一家公司,它在某一年度的每股收益为10元,而股票的市场价格为100元。

+

首先,计算市盈率: 市盈率 = 市场价格 / 每股收益 = 100元 / 10元 = 10倍

+

在这个例子中,每股收益为10元,市场价格为100元。

+

通过计算,得到市盈率为10倍。

+
+

优秀企业四个指标的参考值

    +
  • ROE > 20%
  • +
  • 毛利率 > 40%
  • +
  • 净利率 > 5%
  • +
  • 市盈率 20-40 之间(按照A股标准计算得出)
  • +
+
+

净资产收益率高和净利率高的公司是否一定是好的投资对象?

ROE高和净利率高通常被视为公司财务状况较好的指标,但并不意味着这些公司一定是好的投资对象。以下是一些需要考虑的因素:

+
    +
  1. 行业和周期性:不同行业的盈利能力和周期性有所不同。即使一家公司的ROE和净利率很高,如果它所处的行业面临结构性问题或周期性低迷,那么这些指标的表现可能会受到影响。
  2. +
  3. 可持续性:高ROE和净利率可能是暂时的,而非持续的。投资者需要评估这些指标是否具有持续性,例如公司的竞争优势、市场地位和可持续的盈利模式。
  4. +
  5. 债务水平:高ROE和净利率的公司可能通过借入资金来实现这些指标,但高负债率可能增加公司的风险。因此,投资者需要关注公司的债务水平和偿债能力。
  6. +
  7. 估值:高ROE和净利率的公司可能被市场高度看好,导致其股票价格被高估。投资者需要综合考虑股票的估值水平,以确定是否存在投资机会。
  8. +
  9. 其他因素:除了财务指标,投资者还应考虑公司的管理团队、战略规划、产品竞争力、创新能力以及行业趋势等因素,这些因素对于评估公司的长期投资价值也是至关重要的。
  10. +
+

因此,高ROE和净利率只是投资决策的起点,而不是唯一的决策依据。投资者应该进行全面的研究和分析,以综合考虑多种因素,并结合自己的投资目标和风险承受能力做出决策。

+
+

ROE 和 ROI 的区别

    +
  1. ROE是用来衡量企业利用所有者权益(净资产)创造利润的能力。它的计算公式是净利润除以平均净资产。ROE衡量了股东权益的回报率,反映了企业在投入资本的同时,通过运营活动创造的盈利能力。ROE通常用于评估企业的盈利能力和资产利用效率。
  2. +
  3. ROI(Return on Investment投资回报率)是用来衡量特定投资项目或资产的回报率。它的计算公式是净利润除以投资成本,并通常以百分比表示。ROI可以用于评估特定投资项目的经济效益,衡量投资的回报程度。ROI可以用于比较不同投资项目之间的收益率,帮助投资者做出投资决策。
  4. +
+

虽然ROE和ROI都涉及利润和投资,但ROE主要关注企业的盈利能力和资产利用效率,而ROI主要关注特定投资项目或资产的回报率,它们评估的对象和应用范围不同。

+

在评估企业绩效时,ROE和ROI通常会结合使用,以提供更全面的分析。ROE可以衡量企业整体的盈利能力和管理效率,而ROI可以帮助评估具体投资项目的回报情况。

+ +
+ + + + + + + +
+ + + + + + +
+ +
+ + + +
+ + + + + + + + +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2023/The-Open-Mind-in-Foreign-University-Education/1.png b/2023/The-Open-Mind-in-Foreign-University-Education/1.png new file mode 100644 index 0000000000..a4e2b97ac4 Binary files /dev/null and b/2023/The-Open-Mind-in-Foreign-University-Education/1.png differ diff --git a/2023/The-Open-Mind-in-Foreign-University-Education/2.png b/2023/The-Open-Mind-in-Foreign-University-Education/2.png new file mode 100644 index 0000000000..7e6a4245f8 Binary files /dev/null and b/2023/The-Open-Mind-in-Foreign-University-Education/2.png differ diff --git a/2023/The-Open-Mind-in-Foreign-University-Education/3.png b/2023/The-Open-Mind-in-Foreign-University-Education/3.png new file mode 100644 index 0000000000..722e65e86c Binary files /dev/null and b/2023/The-Open-Mind-in-Foreign-University-Education/3.png differ diff --git a/2023/The-Open-Mind-in-Foreign-University-Education/4.png b/2023/The-Open-Mind-in-Foreign-University-Education/4.png new file mode 100644 index 0000000000..442e0d08a8 Binary files /dev/null and b/2023/The-Open-Mind-in-Foreign-University-Education/4.png differ diff --git a/2023/The-Open-Mind-in-Foreign-University-Education/5.png b/2023/The-Open-Mind-in-Foreign-University-Education/5.png new file mode 100644 index 0000000000..b9f095c3ca Binary files /dev/null and b/2023/The-Open-Mind-in-Foreign-University-Education/5.png differ diff --git a/2023/The-Open-Mind-in-Foreign-University-Education/6.png b/2023/The-Open-Mind-in-Foreign-University-Education/6.png new file mode 100644 index 0000000000..928c2aceb0 Binary files /dev/null and b/2023/The-Open-Mind-in-Foreign-University-Education/6.png differ diff --git a/2023/The-Open-Mind-in-Foreign-University-Education/index.html b/2023/The-Open-Mind-in-Foreign-University-Education/index.html new file mode 100644 index 0000000000..10e6c4dcb2 --- /dev/null +++ b/2023/The-Open-Mind-in-Foreign-University-Education/index.html @@ -0,0 +1,502 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 国外大学教育中的开放思想 | 贾攀的流水账 + + + + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

贾攀的流水账

+ +
+

Panmax's Blog

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+ + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ 国外大学教育中的开放思想 +

+ + +
+ + + + +
+ + +

今天在观看哈佛幸福课时,老师在过程中讲述了适当休息、放下当前的工作对于创造力的重要性。

+

之后还用性爱做了个比喻,非常喜欢国外这种开放的教学方式,在进行这个比喻时非常自然。

+

反观国内,学生们只能私下里偷摸讨论这种事情,不管在任何场合都无法拿到台面上来说。

+

+

+

+

+

+

+

以上观点也应了蒋勋老师在讲解红楼梦时提到的一句话:「人生的领悟不是在知识里,人生的领悟其实是在生命的经验当中。」

+ +
+ + + + + + + +
+ + + + + + +
+ +
+ + + +
+ + + + + + + + +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2023/Unable-to-obtain-is-growth/index.html b/2023/Unable-to-obtain-is-growth/index.html new file mode 100644 index 0000000000..d8554808c4 --- /dev/null +++ b/2023/Unable-to-obtain-is-growth/index.html @@ -0,0 +1,507 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 要不到手是成长 | 贾攀的流水账 + + + + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

贾攀的流水账

+ +
+

Panmax's Blog

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+ + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ 要不到手是成长 +

+ + +
+ + + + +
+ + +

陈奕迅的《红玫瑰》中有一句:得不到的永远在骚动。

+

红楼梦中的薛蟠就是一个很好的例子:

+

最早因为得不到香菱而打死冯渊,得到后几天就腻了;

+

第二次因为得不到柳湘莲而在酒席上大闹,被湘莲暴揍一顿;

+

第三次在娶了夏金桂不久又骚动着想要得到宝蟾。

+
+

说句题外话,在写上边的时候发现香菱和湘莲的读音有几分像,有没有可能是作者有意为之,湘莲就是来为香菱报仇的?

+
+

在这几次骚动中,唯一一次求而不得就是调戏柳湘莲那一次,而那一次也是薛蟠得到最多成长的一次。薛蟠因为怕丢人,就跟随老管家出远门学做生意去了,按作者的意思是过程还不错,只是回来的路上遇到了土匪,不过最后又被柳湘莲久了,二人还拜了把子。

+

再说一个因得不到而骚动的例子,贾赦想要取老太太身边的丫鬟鸳鸯,但用了各种招数都没到手,最后被老太太臭骂一顿才罢休,于是他从外边买了个小媳妇,也是就稀罕了两天后边就让她独守空房了。

+

这么来看得到了又怎样呢?还不如在努力后发现得不到时把心态放平和,未来还能作为一个美好的回忆。

+

UpdatedAt2023年08月02日:再补充一个关于宝玉的故事。宝玉曾一度以为大观园里的女孩都会围着他转,听他的话讨好他。有一次,他看到龄官用金簪在地上写着”蔷”字,他被这个女孩迷住了,非常欣赏她。后来,在梨香院再次遇到龄官时,宝玉让龄官给他唱一出戏,龄官不肯唱,过了一会儿,贾蔷来了,跟龄官交流了一会儿,宝玉知道了龄官喜欢的是贾蔷。此时,宝玉才明白并不是所有的女孩都喜欢他,他也不能得到所有女孩,这次经历给宝玉上了一堂非常生动的爱情课。

+

我博客的样式改了很多版,唯一没改过的是首页的那三句话:

+
+

人会长大三次。

+

-> 第一次是在发现自己不是世界中心的时候。

+

-> 第二次是在发现即使再怎么努力,终究还是有些事令人无能为力的时候。

+

-> 第三次是在,明知道有些事可能会无能为力,但还是会尽力争取的时候。

+
+

原话我已经忘记是从哪里看到的了,但也可以视为一种得不到手的成长。

+

我觉得大多数时候求而不得的人要比想要什么就能得到什么的人更幸福,这样的人更懂得珍惜当下,心智也更成熟,更能接受自己的失败,从失败中成长、从失败中学习。

+

求而不得可以让我们更加珍惜已经得到的部分,不再认为得到是理所当然的。我以前在求而不得时会有个很悲观很恶毒的想法:看到自己无法得到的东西另一个人却垂手可得就会觉得很不公平。现在会经常用黛玉的那句「事若求全何所乐」来让自己释怀,况且自己得到的已经够多了。

+ +
+ + + + + + + +
+ + + + + + +
+ +
+ + + +
+ + + + + + + + +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2023/Use-Cloudflare-Zero-Trust-protect-your-web-applications/0.png b/2023/Use-Cloudflare-Zero-Trust-protect-your-web-applications/0.png new file mode 100644 index 0000000000..05abe80d0f Binary files /dev/null and b/2023/Use-Cloudflare-Zero-Trust-protect-your-web-applications/0.png differ diff --git a/2023/Use-Cloudflare-Zero-Trust-protect-your-web-applications/1.png b/2023/Use-Cloudflare-Zero-Trust-protect-your-web-applications/1.png new file mode 100644 index 0000000000..4b9253d09f Binary files /dev/null and b/2023/Use-Cloudflare-Zero-Trust-protect-your-web-applications/1.png differ diff --git a/2023/Use-Cloudflare-Zero-Trust-protect-your-web-applications/2.png b/2023/Use-Cloudflare-Zero-Trust-protect-your-web-applications/2.png new file mode 100644 index 0000000000..24eac2fff6 Binary files /dev/null and b/2023/Use-Cloudflare-Zero-Trust-protect-your-web-applications/2.png differ diff --git a/2023/Use-Cloudflare-Zero-Trust-protect-your-web-applications/3.png b/2023/Use-Cloudflare-Zero-Trust-protect-your-web-applications/3.png new file mode 100644 index 0000000000..cf216407a3 Binary files /dev/null and b/2023/Use-Cloudflare-Zero-Trust-protect-your-web-applications/3.png differ diff --git a/2023/Use-Cloudflare-Zero-Trust-protect-your-web-applications/4.png b/2023/Use-Cloudflare-Zero-Trust-protect-your-web-applications/4.png new file mode 100644 index 0000000000..a39a7ca347 Binary files /dev/null and b/2023/Use-Cloudflare-Zero-Trust-protect-your-web-applications/4.png differ diff --git a/2023/Use-Cloudflare-Zero-Trust-protect-your-web-applications/5.png b/2023/Use-Cloudflare-Zero-Trust-protect-your-web-applications/5.png new file mode 100644 index 0000000000..f8c5b27f2f Binary files /dev/null and b/2023/Use-Cloudflare-Zero-Trust-protect-your-web-applications/5.png differ diff --git a/2023/Use-Cloudflare-Zero-Trust-protect-your-web-applications/6.png b/2023/Use-Cloudflare-Zero-Trust-protect-your-web-applications/6.png new file mode 100644 index 0000000000..70f3aab124 Binary files /dev/null and b/2023/Use-Cloudflare-Zero-Trust-protect-your-web-applications/6.png differ diff --git a/2023/Use-Cloudflare-Zero-Trust-protect-your-web-applications/7.png b/2023/Use-Cloudflare-Zero-Trust-protect-your-web-applications/7.png new file mode 100644 index 0000000000..6fdb95971e Binary files /dev/null and b/2023/Use-Cloudflare-Zero-Trust-protect-your-web-applications/7.png differ diff --git a/2023/Use-Cloudflare-Zero-Trust-protect-your-web-applications/8.png b/2023/Use-Cloudflare-Zero-Trust-protect-your-web-applications/8.png new file mode 100644 index 0000000000..49e6b2864d Binary files /dev/null and b/2023/Use-Cloudflare-Zero-Trust-protect-your-web-applications/8.png differ diff --git a/2023/Use-Cloudflare-Zero-Trust-protect-your-web-applications/9.png b/2023/Use-Cloudflare-Zero-Trust-protect-your-web-applications/9.png new file mode 100644 index 0000000000..8d17cc572a Binary files /dev/null and b/2023/Use-Cloudflare-Zero-Trust-protect-your-web-applications/9.png differ diff --git a/2023/Use-Cloudflare-Zero-Trust-protect-your-web-applications/index.html b/2023/Use-Cloudflare-Zero-Trust-protect-your-web-applications/index.html new file mode 100644 index 0000000000..73b8284789 --- /dev/null +++ b/2023/Use-Cloudflare-Zero-Trust-protect-your-web-applications/index.html @@ -0,0 +1,525 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 使用 Cloudflare Zero Trust 保护你的 Web 应用 | 贾攀的流水账 + + + + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

贾攀的流水账

+ +
+

Panmax's Blog

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+ + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ 使用 Cloudflare Zero Trust 保护你的 Web 应用 +

+ + +
+ + + + +
+ + +

我经常在 VPS 上搭建一些小应用,很多应用是为了方便自己,并没有打算公开使用。

+

比如最近我打了一个 ChatGPT 的服务,自己用起来非常方便。但是有个问题是这个服务默认不支持用户登录认证,在启动时配置了 openai 的 key 后,就可以直接使用了。

+

+

我之前的做法是使用 Nginx 的 Auth 功能来实现,配置起来比较麻烦。它使用静态的用户名和密码,使用起来也不够优雅。

+

+

我这里的需求是,不需要获取具体的用户信息,只要确认这个人是经过我的同意的,就可以访问我的Web 页面。

+

我一直认为 Cloudflare 会提供这样的通用功能,但之前没有找到。今天我看了一篇文章:https://dmesg.app/zero-trust-access-web.html,突然意识到原来 Cloudflare Zero Trust 就是我一直在找的功能。

+

+

经过几步,我已经成功地为我的站点添加了邮箱验证码授权功能。

+

第一步添加应用

因为我要保护的服务是已经在自己服务器上部署好的,所以这里选择 Self-hosted。

+

+

第二步配置应用

想要在站点上使用Cloudflare Zero Trust,前提是域名已经接入 Cloudflare DNS。

+

如下图,我配置了一个 chatgpt 服务,要保护的域名是 chatgpt.jiapan.me,认证后过期时间为1个月:

+

+

第三步配置策略

如下图所示,我配置了一个允许策略,过期时间与上一步配置的应用 session 保持一致。

+

认证规则使用邮箱,要求邮箱后缀为 @jiapan.me

+

+

剩下的就保持默认,一直下一步就行了。

+

测试

现在当我再打开 https://chatgpt.jiapan.me 时,会被重定向到 Cloudflare 的认证页面。需要输入一个邮箱地址:

+

+

如果输入的不是以 @jiapan.me 结尾的邮箱,也不会报错,会正常进入到输入验证码页面,但实际上收不到验证码邮件。这一步 Cloudflare 做得很好,不会让不法分子破解出具体能用什么样的邮箱可以收到验证码。

+

+

输入以 @jiapan.me 结尾的邮箱后,就可以正常收到邮件了。

+

+

当我们将验证码输入到 Code 框中后,就可以正常访问我们的服务了。

+

当然,也不是必须有自己独立的邮箱,Cloudflare Zero Trust也支持完整的邮箱地址匹配。比如,通过下面的方式,我补充了一个可以通过 jiapan@163.com 接收Code的规则:

+

+

现在,我不仅可以保证自己的服务不被未经授权的人访问,而且不需要自己去维护和管理用户认证信息。Cloudflare Zero Trust 还支持多种认证方式,比如 OAuth2,LDAP,JWT 等等,可以根据自己的需求选择合适的认证方式。(这一段是 ChatGPT 写的)

+ +
+ + + + + + + +
+ + + + + + +
+ +
+ + + +
+ + + + + + + + +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2023/after-you-sing/index.html b/2023/after-you-sing/index.html new file mode 100644 index 0000000000..2d5993d38d --- /dev/null +++ b/2023/after-you-sing/index.html @@ -0,0 +1,518 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 你方唱罢我登场 | 贾攀的流水账 + + + + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

贾攀的流水账

+ +
+

Panmax's Blog

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+ + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ 你方唱罢我登场 +

+ + +
+ + + + +
+ + +
+

「乱哄哄,你方唱罢我登场,反认他乡是故乡。甚荒唐,到头来都是为他人作嫁衣裳。」

+
+

这是《红楼梦》中的甄士隐听到跛足道人的「好了歌」后提的注解中的最后一句。表达的是:朝代兴亡就像演戏一样,你唱完了下台,轮到别人来唱。

+

大到国家,小到公司都是如此。下面说说最近一年多里我在公司中所经历的「你方唱罢我登场」的三件事。

+

一:

与业务线并行,公司成立了一条职能线,由一位公司元老级别的 DBA 主管作为这条线的负责人,这位负责人找了另一位技术上比较有威望的同事,也就是我前领导辅助他一起推进职能线的建设。

+

前期风风火火,对未来规划的风生水起,各种畅想,计划了很多听起来非常牛逼的大工程和人员培养计划,推行了半年没有什么起色,实际上这半年来大部分工作也是他的副手也就是我的前领导来做的规划和进行的具体推进。

+

后来这位负责人因为休陪产假,一段时间内没有推进工作,公司高管对他不满,所以就把他赶下了台,让他的副手上任了。这位负责人在担任这个职位前是 DBA 团队的主管,担任这个工作后公司为 DBA 组补充了新的主管。他从这个位置下来后,只能在 DBA 组内做一名普通的员工了,不到一个月时间他就提了离职。

+

他的戏演到头了,为他之前的副手,也就是我的前领导做了「嫁衣裳」,接下来该副手登场了。

+

二:

我的前领导上任后,也是新官上任三把火,通过安排大量会议而让这条线有存在感,还要考核大家的代码量,安排每人每周进行分享之类的工作。当然,这些也并不是他的本意,具体情况我就不讲了。

+

我能看出这也只是强弩之末,光是通过这些手段是做不起来的,但因为交情上的缘故,我还是会配合他做好他安排给我的工作。

+

随着下边抱怨的声音越来越大,逐渐降低了会议和分享的频率,再往后就慢慢取消了。这样苟延残喘了一年,也没有任何起色,公司之前给他的饼也没有兑现,刚好外部有不错的机会就提了离职。

+

他本来是一位技术方面非常让人信服的技术管理者,但因为想一直往上爬,为了证明自己,最后只能悻悻离场,之前那么高调的人最终却非常低调的离开了公司,很惋惜。但他明知不可为而为之的勇气非常领我佩服,他身上的那种人格魅力是值得我学习的地方。

+

三:

第三个故事就和前两个没有关系了。我当前所在公司的创始人将公司卖给集团几年后就离开公司二次创业去了,我们公司作为集团的一个事业部独立运营。现在事业部负责人是去年十月底任命的,前两天也听说了他要离开的消息。具体是因为产出不及预期还是他的思路和集团高层有分歧就不得而知了。

+

下一个继任者是集团的创始团队之一,不知能把这场戏唱多久。

+
+

三个故事讲完了,最后再读一遍完整的《好了歌注》吧。

+

好了歌注

甄士隐

+

陋室空堂,当年笏满床;

+

衰草枯杨,曾为歌舞场;

+

蛛丝儿结满雕梁,绿纱今又在蓬窗上。

+

说甚么脂正浓、粉正香,如何两鬓又成霜?

+

昨日黄土陇头埋白骨,今宵红绡帐底卧鸳鸯。

+

金满箱,银满箱,转眼乞丐人皆谤。

+

正叹他人命不长,那知自己归来丧?

+

训有方,保不定日后作强梁。

+

择膏梁,谁承望流落在烟花巷!

+

因嫌纱帽小,致使锁枷扛;

+

昨怜破袄寒,今嫌紫蟒长。

+

乱烘烘你方唱罢我登场,反认他乡是故乡。

+

甚荒唐,到头来都是为他人作嫁衣裳。

+

人到底要走向哪里去,什么是生命的本体。我们追逐的东西是不是生命里面真正想要的、觉得最重要的?我们误认了世俗里面虚拟出来的假象,把它们当成了故乡,努力地飞奔而去。其实那只是「他乡」而已,并不是生命本质的东西。

+ +
+ + + + + + + +
+ + + + + + +
+ +
+ + + +
+ + + + + + + + +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2023/another-night-without-sleep/index.html b/2023/another-night-without-sleep/index.html new file mode 100644 index 0000000000..2c73ba1b1a --- /dev/null +++ b/2023/another-night-without-sleep/index.html @@ -0,0 +1,504 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 又一晚没睡 | 贾攀的流水账 + + + + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

贾攀的流水账

+ +
+

Panmax's Blog

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+ + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ 又一晚没睡 +

+ + +
+ + + + +
+ + +

现在是早上5:11,昨晚11点半躺下后没有任何睡意,眼睁睁一直躺到现在

+

中间尝试读书、冥想、听相声都没有缓解

+

刚刚把小红书、Twitter、脉脉这些会给我制造焦虑或者杀时间的 APP 卸载了

+

我第一次失眠是在高中时,在这之前我是每天都要午睡的体质

+

高中时非常喜欢班里一个女生,她也喜欢我

+

第一次失眠的原因是我们考试考砸了,我向她保证我们一起好好学习

+

然后那个晚上整晚都在迫切的希望自己早点睡着,早上早点起来开始学习

+

结果第一个不眠之夜就这么诞生了

+

到现在十五年了,不要说午睡,晚上很容易整晚无法入睡

+

运气好的话有时可以靠一片处方安眠药胡乱睡几个小时

+

高中时就开始了为了治疗失眠的求医之路

+

我也忘了那时候都吃些什么药了,反正是一把一把吃,也不见效

+

从失眠第一天开始,就像突然失去了睡眠的这项基本技能

+

躺在床上很虚无,忘记了该如何入睡

+

现在我会定期去医院的神经内科,开精神类处方安眠药

+

为了方便我都是挂周末取药的临时号,好几次医生都劝我挂个普通号或者专家号好好看看

+

但当我说我这个症状已经十几年了之后,医生也就不再说什么

+

据说失眠的人会出现在别人的梦里

+

既然我失眠了,希望梦到我的那个人可以一夜好眠

+ +
+ + + + + + + +
+ + + + + + +
+ +
+ + + +
+ + + + + + + + +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2023/appearance/index.html b/2023/appearance/index.html new file mode 100644 index 0000000000..b931b87f7f --- /dev/null +++ b/2023/appearance/index.html @@ -0,0 +1,498 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 外貌 | 贾攀的流水账 + + + + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

贾攀的流水账

+ +
+

Panmax's Blog

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+ + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ 外貌 +

+ + +
+ + + + +
+ + +

人的外貌是个无形的加分项,不管是在校园中、职场中还是还是日常社交中,外貌都占了很重要的位置,颜值即正义,这也是为什么现在医美越来越火的原因。

+

爱美之心人皆有之,人是视觉型动物,看到的其他人后都会先根据外貌给对方做个评判。

+

在学校里老师更喜欢辅导长得好看的学生,这一点我可以通过自己见过的两个例子来佐证。第一个是我上初二时,班里换了一个英语老师,她之前是教高中的,看了我们班的男生后说你们都没有长开,我一点给你们上课的欲望都没有。另一个是前段时间听谐星聊天会,有一期一个上麦的女老师也提到类似观点,她更喜欢叫长得好看的男学生。

+

写到这里发现一个问题,如果同样的观点是出自男老师对女学生的,那么一定会在社会上被指责,可女老师偏爱好看的男学生却不会。

+

在职场上,领导也更喜欢给长得好看的人倾斜资源。我不是圣贤,我也承认自己在这方面有“偏心”。同样一件事,给长得好看的同事就愿意多讲几句,光怕对方没听明白。对于长得一般的就不会这么上心。在工作跟进和员工关怀上,对长得好看的同事我也有意无意地偏向很多一些。

+

人们还会根据对方的外貌来给同一个行为打上不同的标签。比如一个女生在公共场合大声喧哗、和男生勾肩搭背,对于长得好看的就是活泼开朗、可爱大方、不拘小节。对于长得丑的就是没教养、不讲究、太随便、没有分寸感。

+

有时候在地铁上闻到一股屁味,我也会环顾一下四周的人,猜测是哪个人放的,被我猜测的人大概率长得也不怎么好看。🤦🏻‍♂️真的是很不应该的偏见。

+

宝玉的爸爸贾政,本来对宝玉很厌恶,恨铁不成钢。在《红楼梦》第23回,大观园完成省亲的任务后,贾政遵嘱元春娘娘的旨意,让宝玉同姐姐妹妹们一起住进大观园。他把宝玉、贾环叫进房来训话,看到宝玉长的这么好看心情一下子也好了,和贾环形成了很大的对比,原文是:「贾政一举目,见宝玉站在跟前,神彩飘逸,秀色夺人;看看贾环,人物委琐,举止荒疏」。

+

此时的贾政不由得又想起了已经去世的大儿子贾珠,想到自己和王夫人年事已高,很欣慰自己有宝玉这么个好儿子,心一下子软了很多:「把素日嫌恶处分宝玉之心不觉减了八九」。

+

有个好看的外表固然值得庆幸,没有也不需要自暴自弃。贾环的「人物委琐,举止荒疏」多半来自他觉得自己的是庶出导致的不自信,这一点上探春就比他自信的多。

+
+

我皮囊不够好看,灵魂也不算有趣,我生于尘埃,溺于人海,关于我的一切都平淡的不像话。即便是这样,我也是宇宙的孩子,和植物、星辰没什么两样。

+
+ +
+ + + + + + + +
+ + + + + + +
+ +
+ + + +
+ + + + + + + + +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2023/automated-audit/0.png b/2023/automated-audit/0.png new file mode 100644 index 0000000000..6a8712c06b Binary files /dev/null and b/2023/automated-audit/0.png differ diff --git a/2023/automated-audit/1.png b/2023/automated-audit/1.png new file mode 100644 index 0000000000..620fde5dfe Binary files /dev/null and b/2023/automated-audit/1.png differ diff --git a/2023/automated-audit/2.png b/2023/automated-audit/2.png new file mode 100644 index 0000000000..f80f47e8a9 Binary files /dev/null and b/2023/automated-audit/2.png differ diff --git a/2023/automated-audit/3.png b/2023/automated-audit/3.png new file mode 100644 index 0000000000..72167930a2 Binary files /dev/null and b/2023/automated-audit/3.png differ diff --git a/2023/automated-audit/4.png b/2023/automated-audit/4.png new file mode 100644 index 0000000000..4a16cd4195 Binary files /dev/null and b/2023/automated-audit/4.png differ diff --git a/2023/automated-audit/5.png b/2023/automated-audit/5.png new file mode 100644 index 0000000000..6362857860 Binary files /dev/null and b/2023/automated-audit/5.png differ diff --git a/2023/automated-audit/6.png b/2023/automated-audit/6.png new file mode 100644 index 0000000000..a49546bfc2 Binary files /dev/null and b/2023/automated-audit/6.png differ diff --git a/2023/automated-audit/index.html b/2023/automated-audit/index.html new file mode 100644 index 0000000000..614ec80b3b --- /dev/null +++ b/2023/automated-audit/index.html @@ -0,0 +1,534 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 将重复工作自动化 | 贾攀的流水账 + + + + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

贾攀的流水账

+ +
+

Panmax's Blog

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+ + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ 将重复工作自动化 +

+ + +
+ + + + +
+ + +

作为程序员,我们最擅长的事情就是用程序解决问题,恨不得天天拿着锤子找钉子。

+

我们公司的服务上线流程是先由服务负责人审批,然后再由团队的 Leader 审批。如果自己就是服务负责人,则只需经过团队的 Leader 审批即可。因此,我们大部分服务负责人设置的是最经常改动上线的那个人。

+

所以无论如何设置,最终都需要我来审批。每天平均要审批30多个上线单,不论是在医院看病、开车、吃饭、开会,随时都可能有审批。而且基本上都会伴随着一个「钉」,在公司里还好一些,一请假可就要了命了。上一次请假,下午四点之前我没有什么特别重要的事情,四点后准开车出去办点事,但是好巧不巧,四点之后开始不断有上线审批,换着人轮番上线。我一手握着方向盘,另一只手拿着手机审批,幸好我的车是自动挡,否则手动挡的话我真就忙不过来了。那个时候我真的有点火大,决定一定要写一个工具来自动帮我审批。

+

第二天上班后,我就让旁边的同事提了个上线单。在审批这个上线单的整个流程中,我进行了抓包,以查看每个步骤的请求内容。最后,我梳理了整个流程发现:将5个接口请求配合起来,就可以实现自动化审批,具体细节这里不展开。

+
+

这里不得不再吹一次 Python,从写第一行代码到完成,不到4个小时就实现了完整的功能。

+
+

要实现这个自动审批功能,还需要解决两个问题:

+
    +
  1. 接口鉴权
  2. +
  3. 消息通知
  4. +
+

接口鉴权

我们公司的开发平台支持两种认证方式。在内网环境下,可以通过域账户登录;在非内网环境下,可以使用钉钉登录。无论使用哪种方式,最终都是设置后端所需的 Cookies。两种方式本质上并没有太大区别。

+

我需要一个稳定的机器来循环执行审批脚本,所以我将这个脚本放在了我的服务器上。为了快速验证第一版程序中的 Cookies 是否有效,我将 Cookies 写死在了代码内,并测试了它们的过期时间。经过验证,Cookies 的过期时间为12小时。因此,每天早上更新一次 Cookies 即可。

+

更新 Cookies 需要手动操作。因为登录界面和登录接口有许多校验和加密逻辑,无法通过简单的模拟来完成。每天早上手动登录一次,然后提取 Cookies 即可。这比以前已经方便了许多。

+

更新 Cookies

接下来需要解决每天如何方便更新Cookies的问题。一开始想到的解决方案是自己编写一个API,每天调用该API来更新Cookies。评估后觉得该方案有些太重,而且没有界面的话就不能随时随地更新,只能通过Postman或者Curl命令,不太友好。最后,我采用了一个非常方便的方式。这个方法有界面,足够安全,可用性有保障。

+

这个方法会在之后单独用一篇文章来介绍,写完后在这里补充链接(我以后在程序内读取需要更新数据类的需求,大概率都会使用这个方法,敬请期待)。

+

消息通知

既然已经实现了自动审批逻辑,就一定要做好监控和通知,不能盲目审批,否则后果不堪设想。

+

最初我使用的是 Bark,每次审批后会向我的手机发送一条 Push,但这样不容易查看历史消息,也不方便聚合消息。另外,我认为这些通知并不一定只有我自己可以看到,可以让更多的人看到,比如全组的同学,这样好处是大家的信息更加同步。例如,之前有一个人上线服务时,除了他自己和 Leader,其他人是不知道的。因此,第二版的通知实现是与钉钉机器人对接。为了不干扰正常的组内聊天,我专门建了一个机器人通知群,把涉及到的组内同事拉进来。

+

+

我还在通知信息中加上了上线人在上线单中填写的描述,能非常方便的看出哪个服务、上了什么功能。

+

顺便做些其他通知

既然有了这个通知群,那可以再利用它做些其他通知。比如

+

订餐通知:

+

+

每天随机出一道算法题:

+

+

上下班打卡+毒鸡汤:

+

这里说明一下,我们打卡基本不要求时间,只是为了方便统计考勤

+

+

+

每周五实验延期提醒和周报提醒:

+

+

+

再举两个自动化的例子:

    +
  • 我写了个脚本,当发现我的博客收到评论后,会自动给我手机发一条 push
  • +
  • 我的博客是纯静态页,没有管理后台,之前只能用我的一台配好环境的电脑来发布,现在改成了任何电脑都可以发布(这个后边找时间专门写一篇文章来介绍)
  • +
+

最后

为什么我这么看重审批,进而想要将其自动化。

+

首先,我觉得它是一项重复性的工作,确实没有必要每一次都进行人工操作。每次操作对我来说都是一次打扰。

+

其次,更重要的是,我不想因为上线审批不及时而成为团队效率的瓶颈。我一直提倡高效、不加班的工作方式,但是如果一个审批需要十多分钟,就有些大家的浪费时间了。

+ +
+ + + + + + + +
+ + + + + + +
+ +
+ + + +
+ + + + + + + + +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2023/chatgpt-parameters/index.html b/2023/chatgpt-parameters/index.html new file mode 100644 index 0000000000..6c3978258e --- /dev/null +++ b/2023/chatgpt-parameters/index.html @@ -0,0 +1,505 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ChatGPT 参数介绍 | 贾攀的流水账 + + + + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

贾攀的流水账

+ +
+

Panmax's Blog

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+ + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ ChatGPT 参数介绍 +

+ + +
+ + + + +
+ + +

好久好久没有写博客了,说因为忙肯定是借口,主要还是没什么动力。

+

最近我也追了追 AI 的风潮。不过相对来说,我开始使用 AI 的时间还算比较早,从前年开始就用 Github Coplit 辅助写代码,去年 12 月就用上了 ChatGPT 的网页版。ChatGPT 真正出圈的时间是在今年 1 月底。我还用 ChatGPT 的 API 写了几个小工具,甚至在公司的项目中也有使用。

+

在使用 GPT 的 API 期间,遇到了几个参数读不懂官方文档在说什么的情况,网上能查到的内容也不多。因此,我结合几个查到的资料和自己的使用体验,对这三个参数做下说明。

+

Temperature

温度参数控制着生成文本的随机性

+
    +
  • 当温度值为0时,表示引擎是预定义的,这意味着无论输入文本如何,它都会创建相同的输出。
  • +
  • 当温度值为1时,会使引擎变得非常有创造力,但同时也承担了更大的风险。
  • +
+

Frequency penalty

频率惩罚参数控制模型重复预测的趋势,减少已生成单词的概率。惩罚取决于一个词在预测中已出现的次数,降低了一个词被多次选择的概率。该惩罚不考虑词频,只考虑词是否出现在文本中。

+

Presence penalty

存在惩罚参数鼓励模型做出新颖的预测。如果某个词已经出现在预测文本中,则存在惩罚会降低该词的概率。与频率惩罚不同,存在惩罚不依赖于单词在过去预测中出现的频率。

+

总结:

本文总结了 ChatGPT 的三个主要参数:Temperature(温度)、Frequency penalty(频率惩罚)和Presence penalty(存在惩罚)。

+
    +
  • Temperature 控制模型生成的多样性。
  • +
  • 频率惩罚存在惩罚分别用于防止单词和主题的重复。
      +
    • 不同文章对这两个惩罚的解释略有不同。
    • +
    • 可以将 Frequency Penalty 视为避免单词重复的方法,将 Presence Penalty 视为避免主题重复的方法。
    • +
    +
  • +
+ +
+ + + + + + + +
+ + + + + + +
+ +
+ + + +
+ + + + + + + + +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2023/close-apple-watch-notify/index.html b/2023/close-apple-watch-notify/index.html new file mode 100644 index 0000000000..fea752042a --- /dev/null +++ b/2023/close-apple-watch-notify/index.html @@ -0,0 +1,495 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 我关掉了Apple Watch的通知功能 | 贾攀的流水账 + + + + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

贾攀的流水账

+ +
+

Panmax's Blog

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+ + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ 我关掉了Apple Watch的通知功能 +

+ + +
+ + + + +
+ + +

长期以来,我使用Apple Watch的主要用途有两个。

+

首先是在收到消息时,及时利用振动反馈通知我。其次是用它来监测身体指标,例如心率、血氧和其他运动类指标。

+

我希望能够及时收到消息,主要是担心漏掉重要的信息,回复不及时会给对方和自己带来损失。但是,这也给我带来了极大的困扰。

+

不管是在钉钉还是微信中,每收到一条消息都会震动一次。这已经让我形成了条件反射,收到消息就想赶紧看,如果不看就会感到着急。再遇上个夺命连环call的主就更要命了,本以为有很多人找我,结果打开一看,发现是同一个人连续发了一堆短句。

+

这样导致的严重问题是在我和他人面对面沟通或正在一个会上发言时,如果这时候来了消息,会瞬间打乱我的思路。我能明显感觉到自己紧张了起来,就连面部表情也发生了变化。

+

因为前段时间把上线单审批做了自动化,这种相对来说更重要的事情不需要我再处理消息了。再加上一直以来被通知的打扰,我决定关闭手表上的通知功能。

+

到今天已经尝试了两周,明显感觉自己的慌张感少了很多。

+

以前,我总是通过手表振动来触发查看消息。在没有了这个触发事件后,我在很长一段时间内甚至会忘记查看消息,这使我更加专注于工作。而且,我发现即使没有及时阅读和回复消息,我也没有错失任何重要事项。

+

在此之前,我已经尽可能地减少了手机上的推送通知。只有几个重要的应用可以向我发送推送通知,我甚至关闭了不必要的红点提醒。

+

此次关闭Apple Watch的通知功能后,我松弛了很多。

+ +
+ + + + + + + +
+ + + + + + +
+ +
+ + + +
+ + + + + + + + +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2023/continue-write-30-days/index.html b/2023/continue-write-30-days/index.html new file mode 100644 index 0000000000..05b3adff6d --- /dev/null +++ b/2023/continue-write-30-days/index.html @@ -0,0 +1,494 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 连续写了30天流水账 | 贾攀的流水账 + + + + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

贾攀的流水账

+ +
+

Panmax's Blog

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+ + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ 连续写了30天流水账 +

+ + +
+ + + + +
+ + +

8月份一眨眼就过去了,来到了Q3的最后一个月,打工人没有9月。

+

回头一看连自己都不敢相信:流水账在不知不觉中已经连续更新了一个月。我不敢称自己写的东西叫文章,它们缺乏逻辑、没有华丽的辞藻、没有育人的道理,都是些自己东拼西凑的碎碎念。

+

我在8月初的时候冒出一个想法,要不要尝试每天更新一篇博客,那个时候觉得这是不可能完成的任务,没有立flag,纯粹是内心的驱使想要挑战一下。每天想一个主题去写写,可能记录日常想法,可能是技术问题,也可能是所见所闻。没有给自己的内容设限,也许突然被什么事情触发了就会记下来写一写。

+

我在这期间写了篇叫「闷嘴葫芦」的流水账,主要是讲我在绩效沟通时无话可讲的尴尬场面。我这一次连续写30天也是想通过写作提升自己思维表达方面的能力,目前从自己的感受来看貌似还没有什么效果。

+

另一个触发我开始写的原因是,在5到7月期间,我因为生活和工作的两面夹击,博客停更了很长时间,七月底突然在钉钉上收到一位不认识同事的问候,询问我怎么好久没有更新了,是不是这段时间很忙,还说了一些鼓励我的话。看了一下她的信息,是一位远在成都的同事。那一瞬间我大受感动,没想到我这犄角旮旯的地方还会被发现,而且是被同一个公司的同事发现。被关注可以大大提升一个人的成就感,虽然这有点不成熟,但至少对那个低谷阶段的我来说确实像冥冥之中的安排,一只无形的手把我从谷底拉出。

+

开始连续写流水账后,我从之前的只摄取知识向输出内容转变,开始留意生活,想到了什么好的主题就赶紧记下来,因为有了主题,就会有意无意的收集可以作为内容的素材。在交流中、听播客节目时、阅读时、跳绳时甚至在写东西的过程中都会有灵感蹦出来。

+

我现在有1个固定+2个零碎的写作时间,固定时间是工作日的中午,我会在每周的一、三、五中午跳绳,跳完绳差不多12点50左右,然后拿出25分钟左右时间写一点东西,然后做5-10分钟冥想,一点半下楼吃个饭就开始下午的工作。周二和周四中午会有一个多小时的大块时间来写。零碎时间是每天晚上到家后,和地铁上通勤时。地铁上我会随心情或读书或写作,如果是写作我就用手机上的 Notion 来写。

+

作为连续写水文30天的奖励,今天就喝一杯瑞幸刚出的酱香型拿铁奖励一下自己吧。

+

希望自己能坚持下去这个习惯,100天见。

+ +
+ + + + + + + +
+ + + + + + +
+ +
+ + + +
+ + + + + + + + +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2023/crocs/1.jpeg b/2023/crocs/1.jpeg new file mode 100644 index 0000000000..c2518f3256 Binary files /dev/null and b/2023/crocs/1.jpeg differ diff --git a/2023/crocs/2.jpeg b/2023/crocs/2.jpeg new file mode 100644 index 0000000000..f74892b69c Binary files /dev/null and b/2023/crocs/2.jpeg differ diff --git a/2023/crocs/3.png b/2023/crocs/3.png new file mode 100644 index 0000000000..e876e3bcb0 Binary files /dev/null and b/2023/crocs/3.png differ diff --git a/2023/crocs/index.html b/2023/crocs/index.html new file mode 100644 index 0000000000..8cdbf853aa --- /dev/null +++ b/2023/crocs/index.html @@ -0,0 +1,499 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 洞洞鞋 | 贾攀的流水账 + + + + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

贾攀的流水账

+ +
+

Panmax's Blog

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+ + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ 洞洞鞋 +

+ + +
+ + + + +
+ + +

最近进入了雨季,穿普通的鞋子上下班在路上不小心灌上水会很不舒服,一家之主打算给我从网上买一双叫 Crocs 牌子的洞洞鞋,但是上周六,突然预告北京会有大暴雨,现在从网上买已经不赶趟了,正好家附近挨着燕莎奥特莱斯,一家之主告诉我那里,那里有 Crocs 的门店。

+

到了之后我看到价格后直接劝退,一双洞洞鞋竟然要399,这不是坑人吗。但是一家之主执意要让我买,告诉我这鞋特别舒适,而且能穿好多年。我对于穿衣装打扮没有什么追求,但是禁不住劝最后还是买了,买了一双白色的。

+

+

这双鞋穿起来确实非常舒适,但是最吸引我的并不是它的舒适性和外观,而是这鞋居然可以搞 DIY,鞋子上的洞洞是一个个插槽,可以自由组装自己喜欢的饰品,这真的是又让我眼前一亮。

+

洞洞鞋的样子千篇一律,但鞋上的每个洞洞都是标准尺寸,饰品厂只需要按照洞洞尺寸生产标准的饰品,就可以打造出一个生态。每个人都可以像玩乐高一样拼装出自己得意的作品,这卖点一下子就出来了。

+

每个人都有追求个性的愿望,买再好的鞋子也可能会撞鞋,而且也无法突出我的个性。而买洞洞鞋,我可以根据自己的心情来做搭配,比如今天我往上边搭配一个爱心,明天装饰一个咖啡杯,后天装饰一个皮卡丘,这都可以代表我这一天的心情。

+

看看我的搭配,猜猜我今天心情如何?

+

+

有没有可能,只要某种东西做到既方便携带,又可以通过组合的方式来折腾,佩戴上后还能展现一个人的性格和个性,人们就愿意来购买,进而会形成一个成规模的市场?我能想到的另一个例子是手串儿。

+

+ +
+ + + + + + + +
+ + + + + + +
+ +
+ + + +
+ + + + + + + + +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2023/do-not-angry-at-work/1.png b/2023/do-not-angry-at-work/1.png new file mode 100644 index 0000000000..4f3e5f4fcd Binary files /dev/null and b/2023/do-not-angry-at-work/1.png differ diff --git a/2023/do-not-angry-at-work/index.html b/2023/do-not-angry-at-work/index.html new file mode 100644 index 0000000000..fa0be52183 --- /dev/null +++ b/2023/do-not-angry-at-work/index.html @@ -0,0 +1,507 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 不要在工作中生气 | 贾攀的流水账 + + + + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

贾攀的流水账

+ +
+

Panmax's Blog

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+ + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ 不要在工作中生气 +

+ + +
+ + + + +
+ + +

我很容易在工作中生气,大部分情况下是因为对方打扰了我计划的节奏。

+

比如,当我还有很多工作没有完成的时候,被产品拉着开方案讨论会,或者被其他部门拉着讨论需求。这种感觉就像:我是砍柴的,他是放羊的,我和他聊了一天,他的羊吃饱了,我的柴还没着落;与此类似的是中午12点和下午7点后打扰我个人生活的工贼。

+

另一个容易生气的点是一堆群找我看问题,很多时候就是:一杯茶、一包烟、一个BUG看一天(尽管我不抽烟)。手头的需求做不完,还要去处理各种线上问题,甚至还要配合公司的安全部门一起打击黑灰产,把时间都浪费在了偶然复杂度的事情上了。

+

还有一种情况是被迫做一些跟业务成果、个人成长无关的事情,举个例子,为了降低运维成本,我们公司今年要做机房搬迁。我需要花大量时间和SRE讨论细节,他们会给我们提很多需求,列很多TODO,这些事情只有苦劳,没有功劳,都是些杂活。

+
+

不知道公司兴师动众要用一年时间完成的机房迁移,是真的能省很多成本,还是为了某个高层的个人绩效才要搞的。

+
+

我生气、愤怒的本质是感受到了失控感,自己无法控制自己的时间,觉得自己宝贵的时间被别人浪费了。

+

不管是当面抢白还是打字怼对方,只要我给对方表现出过不耐烦、发脾气、发泄情绪,事后一定会后悔,会有歉意。以至于会做出一些补偿性回馈,在其他事情上补偿,但补偿的人可能并不是当事者。比如在跟下一个人沟通时就会非常有耐心,甚至百依百顺,破格答应他提的一些条件。或者在下班路上对路人友好了很多。

+

工作中完全犯不上生气,大家都是来这里给资本家打工赚钱的,都在争取自己的利益,没有谁要故意为难谁。产品经理临时找我插入需求或者调整方案,也一定是她的领导这么要求他的,她也有自己的苦衷,不会刻意来找我的茬,我完全没有必要把气撒在他的身上。

+

东东枪曾分享过一个观点:

+
+

我们何德何能?凭什么要求自己的工作环境、共事的伙伴都是完美的呢?

+
+

谁也不是我肚子里的蛔虫,不知道我的所思所想很正常,难免在我不想被打扰的时候打扰我。

+

在工作上生气还会给同事留下非常糟糕的印象,我自己也不喜欢跟脾气不好的同事配合

+

管理好压力,管理好情绪,管理好预期。

+

气大伤身是有科学依据的:

+

+

时间总是不够用的,事情总是做不完的。今天做不完就明天做,明天做不完就分给别人做。

+ +
+ + + + + + + +
+ + + + + + +
+ +
+ + + +
+ + + + + + + + +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2023/doing-things-correctly-and-doing-the-right-thing/index.html b/2023/doing-things-correctly-and-doing-the-right-thing/index.html new file mode 100644 index 0000000000..56b2cb37c4 --- /dev/null +++ b/2023/doing-things-correctly-and-doing-the-right-thing/index.html @@ -0,0 +1,503 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 正确地做事与做正确的事 | 贾攀的流水账 + + + + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

贾攀的流水账

+ +
+

Panmax's Blog

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+ + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ 正确地做事与做正确的事 +

+ + +
+ + + + +
+ + +

如何理解做事和做正确的事情?

+

举个例子,前两周我们做了一个业务需求,为了促进两个平台用户之间的交流,用户可以借助AI为对方生成卡通头像。

+

在开发过程中,我们考虑到可能存在隐私风险,因此产品经理向公司法务部门咨询。果不其然,法务部门告知存在风险,暂时无法上线。如果仅止于此,我们只能说法务部门在「正确地做事」。

+

接下来,法务部门和产品经理一起商讨方案,增加用户确认提醒,让用户明确授权对方可以使用自己的照片来制作卡通头像。这样一来,可以避免法律风险,这就是「做正确的事」。

+

作为业务方,我们承担着业务压力和责任。在与其他团队协作时,应该站在专业角度,告诉我如何能够做好,而不是直接告诉我不能做。例如,如果存在法律风险,你应该提供避免此类风险的方法或者如何变得合规。如果你怕这样说了有风险,这也是「正确地做事」,但一定不是「做正确的事」,因为这对公司发展没有任何好处。

+

很多大公司的员工都很痛苦,因为大部分人都在正确地做事。每个团队站在自己的角度考虑问题,而且他们的理由你还无法反驳,总要面对一堆不背业务责任的横向部门给提出的建议。

+

按照做事方式,公司里的员工可以分成两组,一组是只关心正确地完成自己任务的员工。他们的想法是,我只要在公司生存下来就好了,其他的我不关心。从人性上来讲,我可以理解他们这样的想法:只要我不犯错就行了。

+

另一组员工则注重做正确的事,只要这个事对公司有帮助就去做,不在乎自己能获得什么利益或者面临什么风险,因为大家的目标是一致的。

+

对管理者与领导者的理解,常常有不少人将其混为一谈,觉得管理者就是领导者, 领导者也就是管理者。

+

事实上,这是一种误解。管理学大师彼得·德鲁克对领导和管理做过经典区分:

+
+

「管理」是正确地做事,「领导」则是做正确的事。

+
+

管理一个团队只需要让团队不犯错就可以了(正确地做事),如果要领导一个团队就得有目标,遇山开路,遇水搭桥(做正确的事)。

+

管理者「正确地做事」强调的是效率,领导者「做正确的事」强调的是效能。

+

效率注重做一件工作的最佳方法。

+

而效能则重视时间的最优利用,包括是否应该做某项工作。

+

「做正确的事」是更高层次的「正确地做事」。

+ +
+ + + + + + + +
+ + + + + + +
+ +
+ + + +
+ + + + + + + + +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2023/effectiveness-not-complexity/index.html b/2023/effectiveness-not-complexity/index.html new file mode 100644 index 0000000000..881ad8350e --- /dev/null +++ b/2023/effectiveness-not-complexity/index.html @@ -0,0 +1,505 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 有效不一定复杂 | 贾攀的流水账 + + + + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

贾攀的流水账

+ +
+

Panmax's Blog

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+ + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ 有效不一定复杂 +

+ + +
+ + + + +
+ + +

上个季度我在业务中做了个看似很简单的功能,却得到了非常好的收益。这个功能简单来说就是帮用户给对方打个招呼,提升两个人聊天的概率,进而提升日活等指标。

+

我们是个社交平台,但是发现很多用户在形成匹配后并没有说过话,浪费了很多的机会。之前已经在产品形态上给了用户非常便捷的方式去选择一个文案发送,但渗透率还是很低,这次我们尝试直接帮用户去发开场打招呼这条消息。

+

我们把一种特殊类型的系统消息实现成用户自己发送消息的样式,只在男性侧展示,男性会认为是女性主动给她了条消息。文案上我们只使用常见且无意义的打招呼文案,不容易被对方察觉,比如:hello、你好、hi、嗨,等等。

+

我们通过一些特征匹配到受众的男性(并不是所有用户都适用),这些特征也是我们不断摸索出来的。再为这个男性找一个最适合他的女性,通过上边说的那条消息引导男性活跃、主动起来,我们帮用户迈出第一步,当第一层窗户纸捅破后用户大概率可以继续聊起来。

+

当然这中间我们还打磨了大量细节,比如负反馈、冷却期等,这里不再详述。上线经过几轮实验迭代后得到了非常好的收益,日活、次留、七留等指标都有超大幅度提升。

+

这个功能并不复杂,实现起来也比较简单,却取得了巨大的成功,主要还是巧妙利用了人性,找到了非常好的触发点。

+
    +
  • 一个不怎么聊天的男性看到有女性给他打招呼,会很有诱惑力,毕竟有句老话叫:女追男隔层纱。
  • +
  • 我们的使用的文案都是非常常见的消息,基本不会被察觉,男性就会顺着这条消息给女性也做个礼貌性回复,并且继续往下聊。
  • +
  • 一开始是用女性拉动男性,男性再回消息拉动女性,这样转起来就会形成一个正向螺旋,整个大船就会启动起来。
  • +
+
+

这个功能最终被下线了,原因是其他业务线产品发现这个套路后,过度使用这种方法来拉动短期增长,以达成 Q2指标,求快过程中没有经过多次实验迭代直接全量,产生了大量客诉,给生态造成了影响,最后使用相关特性的功能全部停止。虽然最终下线了,但整个过程让我产生很多思考,而且我也很荣幸引领了一股产品决策的潮流。

+

最有效的方法往往没那么复杂,生活也一样,可以很简单,你只需要不假思索地做正确的事:

+
    +
  • 健康:节食,早睡
  • +
  • 家庭:付出,陪伴
  • +
  • 成长:阅读,写作
  • +
  • 财富:定投,指数
  • +
+ +
+ + + + + + + +
+ + + + + + +
+ +
+ + + +
+ + + + + + + + +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2023/establish-a-set-of-rules/1.jpeg b/2023/establish-a-set-of-rules/1.jpeg new file mode 100644 index 0000000000..8768455ad4 Binary files /dev/null and b/2023/establish-a-set-of-rules/1.jpeg differ diff --git a/2023/establish-a-set-of-rules/2.jpeg b/2023/establish-a-set-of-rules/2.jpeg new file mode 100644 index 0000000000..a00187cfd9 Binary files /dev/null and b/2023/establish-a-set-of-rules/2.jpeg differ diff --git a/2023/establish-a-set-of-rules/3.jpeg b/2023/establish-a-set-of-rules/3.jpeg new file mode 100644 index 0000000000..9170c023d7 Binary files /dev/null and b/2023/establish-a-set-of-rules/3.jpeg differ diff --git a/2023/establish-a-set-of-rules/4.jpeg b/2023/establish-a-set-of-rules/4.jpeg new file mode 100644 index 0000000000..aca6b01b62 Binary files /dev/null and b/2023/establish-a-set-of-rules/4.jpeg differ diff --git a/2023/establish-a-set-of-rules/5.jpeg b/2023/establish-a-set-of-rules/5.jpeg new file mode 100644 index 0000000000..88ee2d4514 Binary files /dev/null and b/2023/establish-a-set-of-rules/5.jpeg differ diff --git a/2023/establish-a-set-of-rules/index.html b/2023/establish-a-set-of-rules/index.html new file mode 100644 index 0000000000..dd7e60f699 --- /dev/null +++ b/2023/establish-a-set-of-rules/index.html @@ -0,0 +1,521 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 约法三章 | 贾攀的流水账 + + + + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

贾攀的流水账

+ +
+

Panmax's Blog

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+ + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ 约法三章 +

+ + +
+ + + + +
+ + +

现在是2023年08月12日早上4点43分,我的眼睛瞪得像铜铃。周六计划了好几件事情看起来都要泡汤了,因为涉及做较大额度钱方面的决策,在前一晚严重缺乏睡眠的状态下无法进行合理决策。

+

前几天听了一档播客节目事,播主安利了一个酒吧,被种了草,查了一下刚好在家和公司中间。昨天周五,下班后微信里摇了个同事就来了。

+

+

价格不算便宜,但想着来都来了,况且酒的味道还不错,伴随着小吃就又多喝了一杯。

+

小吃1,拼盘:

+

+

小吃2,taco:

+

+

第一杯的名字叫「朕的糖葫芦」,是一款山楂口味的啤酒:

+

+

第二杯叫「双城记」,苦度很高,对于喜欢喝美式的我来说很对口:

+

+

点第二杯的时候为了下酒又加了份毛豆,辣辣的很下酒。结果天空不作美下起了大雨,等了好久都没有转小雨的迹象,这种天气也打不上车,就冒雨走到地铁站乘地铁回家,到家已经11点半。

+

洗漱完过了0点,舍不得一天就这么过去、加上明天又到了周末,导致刷手机刷到了1点,结果躺下后就没有了困意,翻来覆去到三点多,又起来看了半小时书,之后吃了片安眠药躺到4点半还是无法入眠,就又起来开始写这篇文章。

+

实际上我自己是有酒精过敏的,每次喝完酒都会全身发红。而且我也明知酒精是一级致癌物,但每过一段时间还会想喝点,抱着小喝怡情的侥幸心理。昨晚喝的酒后劲还非常大,我在乘地铁回来的路上,如果再多坐一站可能就吐了。

+

每次喝多了都会难受,每次自己都会告诫自己以后不要再喝酒。同样晚睡也是,每次超过晚上11点半后就很难入睡,每次失眠都会告诫晚上早些上床做睡前准备,睡前远离手机。虽然一段时间内会有效,但自己是好了伤疤忘了疼。

+

每个人都有适合自己的作息方式,按照睡眠类型来分有两种:

+
    +
  • 晨型人又称云雀型,生物钟更快一些,能在早上自然醒来,白天不容易疲惫,晚上也倾向于早早休息;
  • +
  • 夜型人又称猫头鹰型,他们大多是夜猫子,爱晚睡晚起;
  • +
+

我无疑是云雀星,白天也没有午睡习惯。但我很羡慕周五晚上去嗨,然后利用周末补觉的那些人,我自己尝试了多次后发现真的不适合自己,毕竟这些东西都是基因里已经决定了的。

+

我在昨天属实算「纵欲」了,因为最近一段时间睡眠质量不错,就放松了警惕,白天喝了两杯咖啡,上午一杯、下午一杯,晚上还喝了度数较高的啤酒,为了配酒还吃了高热量实物,深夜又刷了很久手机。

+

有必要给自己约法三章了,虽然之前也已经约法过,但希望这次是最后一次…

+
    +
  1. 每天最多喝一杯咖啡,如果前一晚没睡好可以考虑加浓。下午2点后禁止喝咖啡!
  2. +
  3. 任何场合都要远离酒精制品,包括团建、家庭聚会。
  4. +
  5. 晚上10点半后不再看手机(工作内容,比如业务报警除外)。
  6. +
+

再补充一个让我有点难过的事情,凌晨4点多我决定不再尝试入睡,起来写这篇文章前,看了眼手机才注意到微信里有一条昨天晚上9点40多的语音消息,念念和我说她要一个人睡了,还给我发了照片。我当时在喝酒,没有看到也没有回复她,她当时一定很期待我的回复吧。

+ +
+ + + + + + + +
+ + + + + + +
+ +
+ + + +
+ + + + + + + + +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2023/every-boy-has-his-own-goddess/1.jpeg b/2023/every-boy-has-his-own-goddess/1.jpeg new file mode 100644 index 0000000000..a4867aad1c Binary files /dev/null and b/2023/every-boy-has-his-own-goddess/1.jpeg differ diff --git a/2023/every-boy-has-his-own-goddess/index.html b/2023/every-boy-has-his-own-goddess/index.html new file mode 100644 index 0000000000..2f5c978e10 --- /dev/null +++ b/2023/every-boy-has-his-own-goddess/index.html @@ -0,0 +1,502 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 每个男生心中都有自己的女神 | 贾攀的流水账 + + + + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

贾攀的流水账

+ +
+

Panmax's Blog

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+ + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ 每个男生心中都有自己的女神 +

+ + +
+ + + + +
+ + +

我在想一个问题,是不是每个男生心中都有自己的女神?比如刘亦菲、林志玲、新结恒衣,我的女神有点特殊。

+

初中时和我一个班的有个L姓女生,因为长得好看性格又好非常受欢迎,那个时候她有非常多的追求者。她和班里一个当时身高已经超过一米九的韩国籍男生交往过(我当时上的是一个国际学校,有很多韩国交换生),和体育委员交往过,还和其他班的男生交往过,这几个仅是我知道的,不知道的可能更多。

+

我和她是同一个县的,每周五会一起坐大巴车回老家,我也鼓起过勇气约她来我家一起写暑假作业,那时候真的只是写作业而已。我知道自己几斤几两,也听到过她在私下里对我的评价,知道自己不可能,而且看到她身边整天有那么多人围绕,很是羡慕,甚至有些自卑,所以不敢有任何逾矩的想法。

+

好巧不巧,我们两个高中又去了一个学校,但这次没有在一个班里。她凭借着自己的优势又成了学校里的小红人,我们班也有好几个仰慕者。其中有一个W姓的男生看她戴了红框眼镜,在还不知道她名字的情况下就用了小红这个昵称来称呼她,当这个W姓的男生知道我和她是老乡后羡慕不已,整天问我很多问题,比如:你说小红有男朋友了吗?小红喜欢什么样的男生?我作为他的可靠线人乐此不疲的和他一起探讨。

+

高中时她偶尔遇到糟心事的时候会和我这个不可能的备胎倾诉,可能因为我那时候没什么经历,也不能给她出什么好建议,给她出主意的人很多,能静静听她讲的没几个,她就把我当成了一个特别好的倾诉对象。

+

高考时她通过艺考去了湖南的一所大学,我留在了河北,她的大学生活非常丰富,我就通过她的朋友圈又看了她四年,我也会有一搭没一搭的在微信问候一下她。那时候流行微博,我还在微博上偷偷关注了她。她和我说她想用Instagram,我就指导她一步步进行科学上网的配置,后来也顺理成章关注了她的Instagram账号,她Ins上的很多照片是没有发在朋友圈和微博的,我就觉得自己发现了她的秘密基地,有些窃喜。

+

一转眼又4年过去了,大学毕业前我在石家庄实习,本来是打算留在石家庄了,可看同学们都来了北京有些心痒痒。毕业第二天给公司领导提了离职,同一天收到了北京的一个面试通知,我在公司楼道里和对方聊了几句,对方问了我一些问题就给我发了offer,如果是现在这么卷的环境我肯定连一个offer也拿不到。

+

等我到北京开始上班后,看到L回石家庄了,准备在石家庄创业之类的,而且看起来是单身状态。我有些后悔来北京,幻想如果没有来我是不是也许会有什么机会?但既然已经来了就好好在北京发展吧,我们继续有一搭没一搭偶尔互相发个消息。

+

又过了半年她可能在石家庄不太顺利,也来了北京,在北京找了份工作,没多久在北京认识了新的男朋友,又没多久和男朋友吵架对方把他赶出去,她当时不敢和家里说,也没钱在外边住,就找我借了几千块钱,我毫不犹豫借给了她,这笔钱过了好久才还回来。

+

我刚来北京不久有段创业经历,是做一个类似探探的产品,我邀请她来我们APP注册发照片。每天通过后台数据看到她被很多人点赞我内心里替她高兴。

+

再后来我结婚了,作为同学、老乡的身份会继续每隔几个月问问她怎么样,不知道是不是巧合,有好几次我问她的时候都碰巧她遇上困难,和我聊聊她的遭遇。

+

实际上我们从高中毕业后就再也没见过面,之后的所有交流都是在微信上,她有时也会突然来找,甚至还和我说她梦到了我。

+

+

现在她的生活依然丰富多彩,全国各处旅游打卡吃美食,而且是个滑雪手、摩托车手。工作也是换了一份又一份,很早之前我问时是在做婚礼策划,过一段时间再问时准备开个精酿小酒馆,她就像一个神,让人捉摸不定,我是泯然众生中的一个守望者。

+

2021年她在朋友圈晒了结婚证,巧的是那个男生也姓贾。去年他们举办了婚礼,她穿婚纱真好看。

+ +
+ + + + + + + +
+ + + + + + +
+ +
+ + + +
+ + + + + + + + +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2023/first-meal-after-moving-into-a-new-house/0.jpeg b/2023/first-meal-after-moving-into-a-new-house/0.jpeg new file mode 100644 index 0000000000..f76ce29c8a Binary files /dev/null and b/2023/first-meal-after-moving-into-a-new-house/0.jpeg differ diff --git a/2023/first-meal-after-moving-into-a-new-house/1.jpeg b/2023/first-meal-after-moving-into-a-new-house/1.jpeg new file mode 100644 index 0000000000..6d52a12972 Binary files /dev/null and b/2023/first-meal-after-moving-into-a-new-house/1.jpeg differ diff --git a/2023/first-meal-after-moving-into-a-new-house/index.html b/2023/first-meal-after-moving-into-a-new-house/index.html new file mode 100644 index 0000000000..834f5a98a5 --- /dev/null +++ b/2023/first-meal-after-moving-into-a-new-house/index.html @@ -0,0 +1,495 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 乔迁第一顿饭 | 贾攀的流水账 + + + + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

贾攀的流水账

+ +
+

Panmax's Blog

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+ + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ 乔迁第一顿饭 +

+ + +
+ + + + +
+ + +

今天一家人在新家吃了一顿团员饭,作为我们拥有新家后的第一次正式庆祝。不过还并没有完全搬过来,小登还太小、小念还需要在现在的幼儿园上完大班,所以在之后的很长一段时间内还是只有我一个人在这边住😂

+

+

上午和路秘书一起送小念去上陶艺课,她和我一起来的原因是想给那个安排我们进来的老师送两盒月饼,她知道这个活我这种笨嘴笨舌的人肯定完不成,而且我一点都不擅长这些。一开始我也觉得她完不成,认为老师不会轻易收家长东西的。没想到在路秘书的再四推让下,那个老师最后还是接了我们的东西,还主动和我们说下学期可以再给我们推荐一些其他课程。我非常非常佩服路秘书这种有社交牛逼症的人。

+

距下课还有一个半小时,我和路秘书压了40分钟马路,走到了一个距离上课地点最近的一个瑞幸,中间经过铁路高架桥看到一列高铁经过,路秘书跟我讲了一个当年追她的男生后来进了铁路局工作的一段故事。我们到瑞幸后我点了一杯之前没喝过的咖啡,在那里歇了20分钟,之后一人骑了一个共享单车回到了上课地点。因为平时上下班路程上的需要,我开了哈罗和滴滴两个共享单车平台的月卡,所以今天我用每个平台扫了一个,骑车就没有花钱。

+

中午回家后路秘书亲自操刀给我剪了个头发,以后又可以在剪头发的开销上省下一笔钱了。过程中我爸作为有8年理发经验的人进行了友情指导。之后去稻香村买了些熟食,我还给自己买了三块在疫情居家办公期间发现的一个好吃的糕点——山楂锅盔,强烈爱吃山楂口味的小伙伴尝一尝。买完熟食回家收拾了一些东西就来新家了,吃饭过程中还喝了两盅酒,现在还晕乎乎的。

+

小念今天带回了她的第一件陶艺作品,一只啄木马笔筒,里边插了扭扭棒做的花:

+

+ +
+ + + + + + + +
+ + + + + + +
+ +
+ + + +
+ + + + + + + + +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2023/foobar/1.png b/2023/foobar/1.png new file mode 100644 index 0000000000..9d478af3bd Binary files /dev/null and b/2023/foobar/1.png differ diff --git a/2023/foobar/index.html b/2023/foobar/index.html new file mode 100644 index 0000000000..cb2fb2b081 --- /dev/null +++ b/2023/foobar/index.html @@ -0,0 +1,509 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + foobar | 贾攀的流水账 + + + + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

贾攀的流水账

+ +
+

Panmax's Blog

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+ + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ foobar +

+ + +
+ + + + +
+ + +

如果你是一位程序员,一定在编程教材或网上文档的示例代码中见到过使用 foo、bar 这两个词为变量命名。如:

+
1
2
3
String foo = "Hello, ";
String bar = "World";
System.out.println(foo + bar);
+

或者:

+
1
2
3
4
5
6
7
8
9
10
#include <stdio.h>

int main()
{
char foo[] = "Hello,";
char bar[] = "World!";
printf("%s %s\\n", foo, bar);

return 0;
}
+

foobar 的由来

foo、bar 的来源究竟是什么呢?我尝试查了一些资料来解答这个问题。

+

对于 foobar 的来源,主要有两种解释:

+

1. FUBAR 缩写词

这一派认为,foo和bar源自美国陆军二战缩写 FUBAR,“Fouled Up Beyond All Recognition”(操蛋到无法修复)。

+

+

2. 电子学术语

foo 表示电子学中反转的信号,bar 表示一个低电平有效的数字信号。

+

为什么使用 foo和 bar

1. 约定成俗

老一辈的程序员们很喜欢在示例代码中使用这两个词作为变量名,发展到后来甚至已经成为 C 和 UNIX 文化的一部分。

+

在 linux/lib/test_debug_virtual.c 中,使用 foo 作为结构名称,使用 bar 作为内部字段名称。:

+
1
2
3
4
5
struct foo {
unsigned int bar;
};

static struct foo *foo;
+

在 linux/tools/testing/selftests/bpf/test_cgroup_attach.c 中将临时文件夹命名为 foo 和 bar:

+
1
2
3
4
5
#define FOO		"/foo"
#define BAR "/foo/bar/"
#define PING_CMD "ping -q -c1 -w1 127.0.0.1 > /dev/null"

char bpf_log_buf[BPF_LOG_BUF_SIZE];
+

2. 易于查找

这个解释虽然有些牵强,但也说的通。

+

foo 和 bar 很容易在代码块中发现,这使得在用眼睛浏览和扫描代码时可以轻松找到和替换。

+

结论

foo 和bar 在代码中无任何实际含义,在教学或写文档过程中为了快速说明一个特性、操作符的使用方法,同时作者又不想大费周章的想一个恰当的变量名,就统统使用 foo、bar 来表示一些无意义的变量,久而久之这个习惯就流传了下来。

+

这两个词在这种用法中没有任何意义,仅仅表示一个变量占位符,就像代数中使用的字母 x 和 y 一样。

+

最后

如果你在示例代码中看到 foo、bar,需要明白这个变量的名称是不重要且随意的,将重点放在后边的代码或者整体逻辑上即可。foo 和 bar 作为两个最常用的临时变量,它们实际上并没有任何词语含义,通常为了方便起见,用来代替更准确的名称。

+

foo 和 bar 比其他临时变量更受欢迎,因为它们的受欢迎,而且它们不可理解的性质使它们很容易被精确定位。

+

也因为 foobar 这个术语非常流行,后来有一个 Windows 上的音频播放器将自己命为 foobar2000。

+ +
+ + + + + + + +
+ + + + + + +
+ +
+ + + +
+ + + + + + + + +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2023/for-money/1.jpeg b/2023/for-money/1.jpeg new file mode 100644 index 0000000000..cb865a6edb Binary files /dev/null and b/2023/for-money/1.jpeg differ diff --git a/2023/for-money/2.jpeg b/2023/for-money/2.jpeg new file mode 100644 index 0000000000..3d269c907e Binary files /dev/null and b/2023/for-money/2.jpeg differ diff --git a/2023/for-money/3.jpeg b/2023/for-money/3.jpeg new file mode 100644 index 0000000000..ab4da43283 Binary files /dev/null and b/2023/for-money/3.jpeg differ diff --git a/2023/for-money/index.html b/2023/for-money/index.html new file mode 100644 index 0000000000..5662d67eb3 --- /dev/null +++ b/2023/for-money/index.html @@ -0,0 +1,512 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 为了碎银几两 | 贾攀的流水账 + + + + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

贾攀的流水账

+ +
+

Panmax's Blog

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+ + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ 为了碎银几两 +

+ + +
+ + + + +
+ + +

最近几天北京下暴雨,公司启动了远程办公模式,我之前好像在一篇文章中写到,相对来说我更喜欢到公司上班,因为去公司工作更有条理和规划,(基本上)能事先规划好每个时间段要做的事情,也能保持生物钟的稳定状态。

+

周末的时候,我整理了新家的吧台,然后在吧台旁看了一会书。

+

+

周一早上把电脑搬了过来,把吧台作为了一个可以观景的办公环境。

+

+

在家办公效率总觉得很低,主要有以下几个原因:

+
    +
  1. 没有双显示器的辅助。我之前本来有一个外界显示器,是在19年买的,分辨率不怎么高,前段时间出咸鱼了。
  2. +
  3. 总是想摸摸这里看看那里分散注意力。
  4. +
  5. 还要考虑吃些什么,必要时还要做饭。
  6. +
+

但在家办公又觉得事件过得很快,一天还没做什么事时间就过去了。因为登登的出生,加上念念幼儿园也放了暑假,家里人这段时间回老家了。家人也不在北京,工作上有没有非常紧急且具体的事情要做,晚上就很空虚。

+

躺在床上脉脉、小红书、Twitter 这三个轮番刷,刷到将近零点,放下手机后一阵巨大的失落感袭来。

+

我突然意识到,我的女儿现在已经五岁半了,明年就要上小学。想起了在我刚工作的时候有个前辈,那时候他的孩子大概也是这么大,他和我说这个时候的小孩是最好玩的,过了这个阶段再大一些就没这么好玩了。

+

我错过了念念最好玩的一段时间,前段时间她掉了第一颗乳牙,她把这颗乳牙放进一个小瓶子里,然后跟我发微信视频炫耀,我看到她的喜悦,可心里却酸酸的。

+

+

在生登登出生之前的很长一段时间,每个周末我都会抽出时间来陪念念玩一会,周日下午四点半还会带着她去上美术课,我们两个的小秘密是每次带她出门我都会买一瓶可乐和她分享,或者去 DQ 吃个冰淇淋。那段时间应该是我陪她最多的时候了,念念这么大了,我还没有带她去过远方旅行,所以打算今年国庆前后带她去一趟上海迪士尼,圆一次她的公主梦。

+
+

我们每个人都是小丑,一生当中就在玩这五个球:家庭、工作、健康、朋友和灵魂。五个球当中只有工作这个球是橡胶做的,砸下去还会弹起来,其他四个球是玻璃做的,砸碎了再也不会复原。

+
+

背负着北京的房贷,和家人暂时分居,我经常思考是否应该继续这样的生活,不知道这种生活还要坚持多久。我总是用「熬过这段时间,孩子们大一些就好了」这样的想法来宽慰自己。

+

我也有过回老家的想法,但面临着工资和生活习惯上的落差。我还琢磨过各种副业,想通过副业增加些额外收入,也避免自己被裁员后没有经济来源,但都由于各种原因(大部分是看不到赚钱的希望或者时间投入太多)最终都放弃了。

+

为了碎银几两,为了三餐有汤。希望现在的困难只是暂时的,就像北京当前的暴雨,终会拨云见日见彩虹。

+ +
+ + + + + + + +
+ + + + + + +
+ +
+ + + +
+ + + + + + + + +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2023/go-struct-with-json-tag/index.html b/2023/go-struct-with-json-tag/index.html new file mode 100644 index 0000000000..07f0693174 --- /dev/null +++ b/2023/go-struct-with-json-tag/index.html @@ -0,0 +1,513 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Go Struct 不指定 JSON tag 时的默认规则 | 贾攀的流水账 + + + + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

贾攀的流水账

+ +
+

Panmax's Blog

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+ + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ Go Struct 不指定 JSON tag 时的默认规则 +

+ + +
+ + + + +
+ + +

Golang 在序列化和反序列化一个 Struct 时,如果指定了 JSON tag 会严格按照指定的 tag 内容来执行,在没有指定 tag 或 tag 大小写不精准时,会有一些默认规则。

+

序列化

序列化的情况比较简单:

+
    +
  • 指定了 tag 的可导出字段,按照 tag 的命名进行序列化
  • +
  • 没有指定 tag 的但可以导出的字段(首字母大写)会完全按照变量命名来进行序列化
  • +
+
1
2
3
4
5
6
7
8
9
10
11
12
type A struct {
Case int
casE int
Cas_E int
CaSE int `json:"ok"`
}

func main() {
a := A{1, 2, 3, 4}
s, _ := json.Marshal(&a)
mt.Println(string(s))
}
+

上边这段代码输出:

+
1
{"Case":1,"Cas_E":3,"ok":4}
+
    +
  • casE 这个字段没有输出,原因是因为他是个不可导出的私有字段,即使设置了 tag 也不可序列化。
  • +
  • CaSE 序列话后的 key 为 ok 是因为我们给它指定了 tag
  • +
  • 其余字段都是按照我们原本的拼写格式进行的输出
  • +
+

反序列化

序列化的情况稍微有点复杂,其整体的优先级为:

+
    +
  • 先按 tag 匹配,后按字段名匹配
  • +
  • 有 tag 的仅匹配 tag,没有tag 的可参与字段名匹配
  • +
  • 先精确匹配,后模糊匹配
  • +
  • 多个模糊匹配的按照声明在前的匹配
  • +
+

我们看几个例子:

+

情况1,带 tag 的两个字段都无法匹配上(精准匹配+模糊匹配),不带 tag 的两个字段都可以模糊匹配上,优先赋值给前边声明的字段:

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
type B struct {
Case int `json:"a"`
CaSE int `json:"b"`
CasE int
CaSe int
}

func main() {
s := []byte(`{"CAsE":2}`)
var b B
json.Unmarshal(s, &b)
fmt.Printf("%#v\\n", b)
}
// 输出:main.B{Case:0, CaSE:0, CasE:2, CaSe:0}
+

情况2,带 tag 的其中一个字段可以模糊匹配上:

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
type B struct {
Case int `json:"case"`
CaSE int `json:"b"`
CasE int
CaSe int
}

func main() {
s := []byte(`{"CAsE":2}`)
var b B
json.Unmarshal(s, &b)
fmt.Printf("%#v\\n", b)
}
// 输出:main.B{Case:2, CaSE:0, CasE:0, CaSe:0}
+

情况3,带 tag 的两个字段都可以匹配上,第一个模糊匹配,第二个精准匹配:

+ +
+ + + + + + + +
+ + + + + + +
+ +
+ + + +
+ + + + + + + + +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2023/good-things-2023/index.html b/2023/good-things-2023/index.html new file mode 100644 index 0000000000..f4ebe82e81 --- /dev/null +++ b/2023/good-things-2023/index.html @@ -0,0 +1,507 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 好物推荐 | 贾攀的流水账 + + + + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

贾攀的流水账

+ +
+

Panmax's Blog

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+ + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ 好物推荐 +

+ + +
+ + + + +
+ + +

以下是我最近几年用过的觉得还不错、值得推荐的好物,有些之前也推荐过,好东西值得多次推荐。

+

这些商品网上的介绍非常多,我在这里不做详细介绍。结合自己的使用感受,尝试用自己的一句话描述。

+

注:排名分先后。

+

AirPods Pro2

戴上它,播放你喜欢的音乐,不管周围多么嘈杂,仿佛整个世界都是你的。

+

在地铁上,戴着它不播放音乐或者播放轻音乐,捧起一本书,享受一段安静的阅读时光。

+

Apple Watch

我最贴身的助理。

+

我的是S6,已经用了3年了,感觉还能再战3年。如果觉得旧了,30元左右在淘宝买个新表带,换上后跟新的一样。

+

HHKB 键盘

最适合程序员使用的键盘,没有之一。

+

如果不知道给你的程序员朋友送什么礼物,送这个键盘准没错。

+

湿厕纸

用之前很抗拒,用习惯后爽死了。现在如果不用它就觉得粑粑没擦干净。

+

戴森吹风机

动力十足,气流吹到头上后又很轻柔。向我这种头发不太长的,1分钟以内结束战斗。

+

戴森吸尘器

用它吸一吸你的床,就知道你每天睡的床有多脏了。用它打扫卫生,看着集尘桶内的灰尘越来越多,很有成就感也很解压。

+

罗技 MX Master3 鼠标

贴合手型,就像握住了D罩杯。滚轮像指尖陀螺,没有思路时可以用它来解压。侧边按键写代码,浏览网页时前后推很实用。

+

这个鼠标是跟一个朋友交换的,她买这个鼠标后觉得太大,刚好我买了个小的,就和她换了。刚用的时候没觉得太好用,等习惯后就发现离不开了。

+

植观洗发水

我用的去屑清爽那一款,我之前一直尝试各重洗发水,自从用了这款后就再没有换过。

+

各位男程序员同胞,给自己换个好洗发水把。

+

菁华3合1洗衣凝珠

这个味道我太爱了,穿着用它洗过的衣服去上班,无意间闻到它的芬香后,整个人都快乐了许多。

+

烘干机

被烘过的衣服穿起来太舒服了,松松软软,和在夏日的暖阳下晾干的一样,一股清新阳光的味道。

+

Usmile 牙刷

之前我一直用的是飞利浦电动牙刷,用坏两个后,在被安利下购买了 Usmile 的牙刷,功能性和质量超出我的预期,一点不比飞利浦差,价格也十分平易近人。

+

必迈运动鞋

跑步鞋界的国货之光。

+

洗碗机

太适合我这种爱做饭不爱刷碗的人了。

+ +
+ + + + + + + +
+ + + + + + +
+ +
+ + + +
+ + + + + + + + +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2023/hard-to-say-goodbye/index.html b/2023/hard-to-say-goodbye/index.html new file mode 100644 index 0000000000..6e515b41ce --- /dev/null +++ b/2023/hard-to-say-goodbye/index.html @@ -0,0 +1,492 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 离别好难说出口 | 贾攀的流水账 + + + + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

贾攀的流水账

+ +
+

Panmax's Blog

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+ + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ 离别好难说出口 +

+ + +
+ + + + +
+ + +

我是个喜聚不喜散的人,而且我觉得这和喜不喜欢social没有关系。聚了、认识了、熟悉了就不想再散。

+

黛玉也说过:“人有聚就有散,聚时欢喜,到散时岂不清冷?既清冷,则生伤感,所以不如倒是不聚的好。比如那花开时令人爱慕,谢时则增惆怅,所以倒是不开的好。”

+

在经过5、6两个月努力招聘后,我的小组加上我在内有9个人,本来应该是10个但被砍了一个HC。看起来一切都在步入正轨,但上周老板通知考虑到我们大部门的成本问题和另一个创新部门的业务扩张,我们需要为那边提供十几个后端人力,我们团队也要背一个名额,让我从团队中挑选一个合适的人。

+

当时也没有定好具体过去的时间,我们只是大致讨论了人选,因为还没实际执行,那时候我对这件事还没太大的想法。今天下班前突然通知本周内就要调过去,需要尽快沟通。我本来打算后天再和那个同学沟通,今天先问了问他手中的项目进展,提醒了一下他尽量不要延期。最后他问我是不是给他排了其他工作,此刻我当然可以说没有,然后等后天再和他说这件事,但我觉得我不应该为了让他把需求做完或者只图自己一时心里安稳就骗他,应该实事求是坦诚相告,于是把他带到会议室沟通了前边的内容,唯一没有提到的是为了缩减这边的成本,只说了各种好处、那边的成长空间之类的。

+

沟通完后我能看出他失落的表情,说实话我自己也非常失落,尽管这不是在聊裁员,但我还是很失落,感觉是自己没有保护好队友。我们一起磨合了一个多月,帮他度过过了onboarding期,他马上就要大展宏图做一些重要工作的时候被调走了。

+

和他沟通过程中也能感觉到自己没有那么坚定,如果真的让我和一个同学聊优化的事情也许会更难过吧。

+

回到开头黛玉那句话,如果我知道有这次的散,我还会招进那个人吗?我可能还是会的,虽然相处时间不是很长,但对他来说还是有成长,对我们团队来说他也确实有相应的贡献,而且整个过程中也是比较愉悦的。

+ +
+ + + + + + + +
+ + + + + + +
+ +
+ + + +
+ + + + + + + + +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2023/health-work-environment/index.html b/2023/health-work-environment/index.html new file mode 100644 index 0000000000..49582bd573 --- /dev/null +++ b/2023/health-work-environment/index.html @@ -0,0 +1,509 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 提供健康的工作环境 | 贾攀的流水账 + + + + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

贾攀的流水账

+ +
+

Panmax's Blog

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+ + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ 提供健康的工作环境 +

+ + +
+ + + + +
+ + +

近三个月我的团队除了入职一位去年招到的校招生外,还通过社招渠道加入了4位新同学,他们中有两位上家公司就职于字节跳动、一位上家就职于快手、一位上家就职于小米。

+

从大厂跳到中小厂一般几个原因:

+
    +
  1. 上升空间受限,不想再在大厂做螺丝钉。具体来说有以下几点:
      +
    • 工作内容单一、枯燥。
    • +
    • 员工长期从事重复性工作,产生职业倦怠。
    • +
    • 个人发展受限。在当前公司难以获得切实的学习与发展机会。
    • +
    +
  2. +
  3. 入职即巅峰,工作一两年后对薪资不满。亦或是过度追求股权激励,基本薪资偏低,与工作量不匹配。
  4. +
  5. 工作压力大,加班时间长。卷不动了。
  6. +
+

关于第1点,跳槽的前提条件一般都是薪资待遇不能比之前差,既然他们选择了跳槽,说明第1点现阶段是基本满足了,再往后还要看自己的能力和公司的发展。

+

关于第2点,我目前的团队业务包含了公司的核心场景和推荐工程领域,有非常多的事情可以做,有足够的挑战和 scope 来提供给大家。

+

关于第3点,之前我做不了主,现在有了一定可以做主的空间,我会给团队提供一个我认为是比较的健康工作环境,具体有以下几点:

+
    +
  1. 周一早上不开晨会,我们每日例行的会议只有一个晨会,大概是在上午10点半左右人到全以后开,作为当日开启工作的kickoff。晨会上不用汇报工作,只需要说昨天做了什么,今天准备做什么,遇到了什么问题即可。我在周一取消掉的原因是过个周末容易记不住上周五做了什么,而且包括我在内有周一上班恐惧症,所以取消掉周一早会好让大家更好调整状态,过个没有负担的周末。
  2. +
  3. 周一到周四下午过6点半后不开会。下午6点半后大家的精力和体力基本已经耗尽,如果这一天会议已经比较多,此时应该静下心来把自己的羊放一放。一天的工作即将结束,此时适当调整自己的情绪,梳理一下当天的得失。更进一步,如果有工贼拉我在下午6点半后讨论问题,我也会毫不留情的拒绝,如果是拉我的下属被我知道了,我也会和对方重新协商时间(老板和紧急状况除外,毕竟还要恰饭,不过这种情况少之又少,可能一个季度也就一两次)。
  4. +
  5. 周五下午尽量不开会。我希望团队中每个同学在工作之外都有自己向往的生活,有更多美好的事情可以做,所以规定周五无必要不加班,所以大部分同学都会在7点前走,很少有7点半后吃了饭再走的情况。周五下午不开会还有个原因是,如果有同学打算周末去稍远一点的地方玩,一般会选择周五下午出发,不安排会议可以让同学们放心请假。但实际上我自己周五下午被安排了3个会,不过还好会议都安排在了5点前。
  6. +
  7. 并行需求不超过两个。我会尽量让每个人手中doing状态的工作保持在两个以内,人的注意力和专注力是有限的,切换上下文的成本极高,频繁切换的后果就是工作质量变差。说实话,我现在时间片就已经被切的支离破碎,很难再像之前那样进入心流的状态,但我不想让每个人都有这样的不好体验,所以会由我去对接那些外部乱七八糟的事情,需求接不过来时我会自己多接几个,或者往后压一压。
  8. +
  9. 不抢工作成果。我不会在绩效自评或者在和其他人交流时把下属的工作成果归功于自己,甚至有时候在有老板夸赞某个项目做的不错时,我还会主动说明这是 xxx 做的。不过作为偏管理的职务,我肯定要对整个团队负责,所以我是会在绩效评定时将整个团队取得的成果进行展现的,这放在哪里都是合理且必要的。
  10. +
+

尽管我会在可行范围内提供比较好的工作环境,且在绩效考核时我更看重的是结果,不会用类似不够「不卷」的原因来打差绩效,但有一点要说明的事实就是,通常在工作上投入更多时间确实更有可能取得好的结果。我提供了健康工作的可选性,至于更看重什么、最终如何选择还要看个人,比如现阶段你更追求金钱个个人成功,那把大部分精力放在工作上没有问题,好的绩效确实可以带来客观的现金回报。我也希望每个人可以现在跟长远的角度来思考这个问题。

+

我的某些行为有点像宝玉在守护着大观园中的姐姐妹妹,不知道这种环境是会豢养出偷虾须镯的坠儿,还是会培育出香菱那样的诗仙。虽然不知道最终会结出什么果,但我会坚持自己认为正确的事。

+ +
+ + + + + + + +
+ + + + + + +
+ +
+ + + +
+ + + + + + + + +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2023/how-to-make-life-easier/index.html b/2023/how-to-make-life-easier/index.html new file mode 100644 index 0000000000..aa0a7f1e40 --- /dev/null +++ b/2023/how-to-make-life-easier/index.html @@ -0,0 +1,512 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 如何让生活更轻松 | 贾攀的流水账 + + + + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

贾攀的流水账

+ +
+

Panmax's Blog

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+ + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ 如何让生活更轻松 +

+ + +
+ + + + +
+ + +

设立小期待

给自己设立一些值得期待的事情,比如:

+
    +
  • 期待上下班路上可以读自己喜欢的书
  • +
  • 期待早上到公司后喝一杯醇香的美式
  • +
  • 期待周末可以和孩子们共度美好时光
  • +
  • 期待周末去吃好吃的东西
  • +
  • 期待每个月第五个工作日工资到账
  • +
  • 期待每个月20日公积金到账
  • +
+

有了期待,每一天就会有盼头。引用《基督山伯爵》的一句话:“人类的一切智慧是包含在这四个字里面的:’等待’和’希望’!”

+

奖励自己

自己完成一件工作后一定要奖励一下自己,可以是奖励自己一个包包、一块手表。或者简单一些的,一个项目上线后,奖励自己一杯咖啡、奶茶都可以。

+

在你完成自己觉得很有成就感的事情后,你的老板、同事不一定能给你即时的正向反馈,我们可以自己给自己即时正反馈。

+

建立及时正反馈很有必要的,只有这样才能积小胜成大胜。

+

奖励自己的时机并不限于完成了一个工作之后,当自己失落、状态不好时也可以奖励自己。比如今天早上下着雨,我来公司的路上淋湿了,加上昨天晚上有些没睡好,心情有些糟糕,所以到了公司楼下去瑞幸点了一杯自己爱喝的咖啡。

+

对自己好一点,先学会爱自己,才能好好爱别人。

+

留出属于自己的时间

尽管我们一天当中大部分时间都在公司度过,但这并不意味着在公司的一定要把全部精力投在公司的工作上,要有自己可以掌控的时间。

+

我这里并不是说要让大家在公司接私活,主要指的利用有限的时间高效工作,留出一些时间来提升自己。

+

拿我自己为例,我每天有效的工作时间只有4小时,超过这个时间工作效率会很低下,所以我会给自己安排一些工作工作以外属于自己的事情,比如读书、写 leetcode、学习一些专项课程,最近还加入的写流水账的时间。这些事情和工作都是相辅相成的。

+

学会说不

这一点在职场中太重要了,同事、领导交给你的任务不要全盘接收。接的工作太多,哪一个也完成不好,最后还搞的自己压力巨大、身心俱疲。

+

现在我会参与每周的需求会,根据人力情况决定下周接几个需求,在已经得知人力不足的情况下,我会好不犹豫把需求拒掉,同时也不会给每个同学排的太紧,结合上一条,我会刻意给他们留出一些自己的时间。

+

要把自己遇到的困难告诉领导,俗话说会哭的孩子有奶吃。

+

关于说不这件事,我想再用个其他人没有用过的角度补充一点:不要觉得自己比别人聪明,屁股决定脑袋,别人不说是因为他的屁股没有在那个位置上。用宝钗的处事哲学就是「事不关己莫开口,一问摇头三不知」。尤其是在人多的场合开会的时候,不要做话唠,不要试图在他人面前证明自己。与其给一个模糊不清的信息,倒不如大方承认自己不知道。

+

Permission to be human

中文翻译为:允许自己为人,听起来感觉有些别扭。

+

放下过度控制的欲望,接受不能改变的事。过去的事已成过往,许可、接受已经发生的事。

+

允许自己有焦虑、烦恼、悲伤或不快乐。失望、烦乱、悲伤是人性的一部分。接纳这些,并把它们当成自然之事,允许自己偶尔的失落和伤感。然后问问自己,能做些什么来让自己感觉好过一点。

+

想休息的时候就休息休息,想堕落的时候也可以偶尔堕落一下,不要把自己逼得太紧,承认自己某些方面就是不行,比如现在我去理发店,他们再给我推荐办卡时,我会很坦诚的承认自己没有钱,不再找各种借口。

+ +
+ + + + + + + +
+ + + + + + +
+ +
+ + + +
+ + + + + + + + +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2023/i-want-a-motorcycle/1.png b/2023/i-want-a-motorcycle/1.png new file mode 100644 index 0000000000..3a07b4492b Binary files /dev/null and b/2023/i-want-a-motorcycle/1.png differ diff --git a/2023/i-want-a-motorcycle/index.html b/2023/i-want-a-motorcycle/index.html new file mode 100644 index 0000000000..7bcd728e3d --- /dev/null +++ b/2023/i-want-a-motorcycle/index.html @@ -0,0 +1,496 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 可能即将拥有人生中第一台摩托车 | 贾攀的流水账 + + + + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

贾攀的流水账

+ +
+

Panmax's Blog

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+ + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ 可能即将拥有人生中第一台摩托车 +

+ + +
+ + + + +
+ + +
+

因为一篇文章而种草的一辆摩托车

+
+

四月份的时候读《读库2205》那一期的时候,有一篇文章介绍了本田超级幼兽的发展史,结果我就被种草了,当时查了一下刚好有一款新的幼兽要在中国上市,而且是全球最低的定价13000元,样式很复古,一看就是小巧精悍类型的,我特别喜欢。

+

+

(图片来自小红书,目前这个车还需要订购,预计2个月才能到货)

+

有摩托车的前提是先有个摩托车驾照,今晚(2023年6月12日)我要去趟山东德州,以特种兵的方式训练,24小时内拿到驾照。

+

祝我好运~

+

小幼兽我来了,男人至死是少年。

+ +
+ + + + + + + +
+ + + + + + +
+ +
+ + + +
+ + + + + + + + +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2023/integrating-city/1.jpeg b/2023/integrating-city/1.jpeg new file mode 100644 index 0000000000..4db79698ca Binary files /dev/null and b/2023/integrating-city/1.jpeg differ diff --git a/2023/integrating-city/2.jpeg b/2023/integrating-city/2.jpeg new file mode 100644 index 0000000000..c420eda951 Binary files /dev/null and b/2023/integrating-city/2.jpeg differ diff --git a/2023/integrating-city/index.html b/2023/integrating-city/index.html new file mode 100644 index 0000000000..9d515e5405 --- /dev/null +++ b/2023/integrating-city/index.html @@ -0,0 +1,497 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 融入一个城市 | 贾攀的流水账 + + + + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

贾攀的流水账

+ +
+

Panmax's Blog

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+ + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ 融入一个城市 +

+ + +
+ + + + +
+ + +

作为一个北漂或在大城市打工的人,您是否有过融入这个城市的瞬间?如果有,是在什么样的时刻产生的?

+

我感到自己真正融入北京,成为这座城市的一份子,是在拥有了自己的车之后。就像小时候搬家,到了一个陌生的胡同,真正融入环境的最好证明就是:和已经生活在胡同里新认识的小伙伴们一起奔跑玩耍。成年后在路上开车也有类似小时候在胡同里玩耍的感觉,身旁经过的车辆都成了我的玩伴。

+

夜晚驾车穿过一条条马路,看着两边的路灯从身后闪过,车内播放着自己喜欢的歌曲,边开车边欣赏这个城市,有一种很兴奋且奇妙的感觉。最有感觉的一次是2020年中旬,,那时候疫情还很严重,公司搬到了新的办公楼,有自己的停车场。虽然公司在二环,但因为还存在封锁和居家办公,路上的车不是很多,我尝试了一段时间开车上下班。一天晚上下班回家路上下起了大雨,我打开了雨刷,把车内音响音量开到最大,在暴雨中前行,除了雨声和音乐,其他什么声音都听不到。看着窗外的大雨,听着动感十足的乐曲,那一瞬间仿佛与这个城市产生了共鸣,我被车被小心翼翼的保护着,仿佛这个城市也在对我说:“好的,我允许你加入我们了,现在开始我们是同志了。”

+

因为有这辆车,在疫情严重到我居家办公,一家之主还在上班的时候,我每天早晚接送她上下班,路上车很少,天气晴的很好,早晚出门转一圈心情也很好。

+

因为有这辆车,周末的时候我们一家可以说走就走,去爬山、去野生动物园、去吃好吃的,不用再风吹日晒骑车、挤地铁或者花时间打车。

+

车成为我连结北京的纽带,让我更好融入这个城市。无论是穿梭于城市的大街小巷,还是在周末的远足中,我都能够更好的体验到这座城市的魅力,享受到这里的美好时光。

+

+

我的车并不高端,是一辆很普通的新能源比亚迪,我为拥有它而骄傲。到现在3年多了,跑了两万多公里,没有出过任何大问题,非常感谢它带着我在这个城市实现梦想,在一次次出行时为我保驾护航。

+

+ +
+ + + + + + + +
+ + + + + + +
+ +
+ + + +
+ + + + + + + + +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2023/interesting-probability/1.jpeg b/2023/interesting-probability/1.jpeg new file mode 100644 index 0000000000..a97aa273f0 Binary files /dev/null and b/2023/interesting-probability/1.jpeg differ diff --git a/2023/interesting-probability/2.png b/2023/interesting-probability/2.png new file mode 100644 index 0000000000..99d6e80b97 Binary files /dev/null and b/2023/interesting-probability/2.png differ diff --git a/2023/interesting-probability/index.html b/2023/interesting-probability/index.html new file mode 100644 index 0000000000..35991ea047 --- /dev/null +++ b/2023/interesting-probability/index.html @@ -0,0 +1,502 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 有趣的概率 | 贾攀的流水账 + + + + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

贾攀的流水账

+ +
+

Panmax's Blog

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+ + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ 有趣的概率 +

+ + +
+ + + + +
+ + +

今天发生了一件哭笑不得的事情,一个我不认识的人申请加我微信,一开始我没有通过,问她有什么事,她又留言让我通过一下。

+

我的微信号只能通过一个6位数字来添加,也就是我的 QQ 号,因此,能够添加我的方式一般只有两种:一种是我们在同一个群里,另一种是我主动把我的号码告诉对方。通过手机号是无法添加我的。

+

这个陌生人一上来他就问我是不是快递员。我看到她添加我的方式为:通过 QQ 号搜索,我第一反应是 QQ 里的某个长年不用的群有人乱发小广告什么的导致的,或者我的 QQ 号在什么地方泄露了。

+

+

我告诉她我不是,接下来她发给我一张淘宝收货页面的截图,这个取件码可不就是我的 QQ 号嘛。

+

+

我跟她解释了那个6位数字是她的取件码,不是联系快递员的方式。看她的收货地址和朋友圈猜测她应该是偏远地区的家庭妇女,平时淘宝用的也不多,第一次收货不知道取件码怎么回事,以为是添加这个微信来取件。

+

这件事让我想到之前看到过的一个定理叫做:「无限猴子定理」。

+
+

让一只猴子在打字机上随机地按键,当按键时间达到无穷时,几乎必然能够打出任何给定的文字,比如莎士比亚的全套著作。

+
+

任何随机现象,事件只要概率为正,不论概率值多小,都有可能发生。(这是不是也有点像墨菲定律?)

+

一部小说都有概率通过随机的方式被组合出来,区区6位数字命中的概率就更高了。生活就是因为充满了这么多随机事件才变得更丰富多彩。

+

生活中最难的就是如何辨别什么是偶然,什么是必然。我们期待把生活全部变成必然,但其实你会发现人的一生很短暂,我们一生的经历很难都是必然的。

+ +
+ + + + + + + +
+ + + + + + +
+ +
+ + + +
+ + + + + + + + +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2023/learn-manage-again-again-again/index.html b/2023/learn-manage-again-again-again/index.html new file mode 100644 index 0000000000..80726344bd --- /dev/null +++ b/2023/learn-manage-again-again-again/index.html @@ -0,0 +1,518 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 又又又要学着管理了 | 贾攀的流水账 + + + + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

贾攀的流水账

+ +
+

Panmax's Blog

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+ + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ 又又又要学着管理了 +

+ + +
+ + + + +
+ + +

我有一个其他人都不知道,但并没有什么卵用的技能:「克领导」。

+

基本上每次跳槽、换部门后直属领导都会在1-2年内离职,然后我就会被迫莫名往上走一个阶梯,我有时候真的怀疑是自己把领导气走的,凡事有再一再二,没有再三再四,这种事发生在我身上已经不下四次了。

+
+

如果你不喜欢自己现在的领导,请联系我。

+
+

近几年我换工作或者部门的很大一部分原由是不想做管理,虽然我没有做过那个很火的人格测试,但我确信自己是个 I 人,从小参加家庭聚会什么的几乎都是一言不发那种,别人问一句我答一句,非常不擅长与陌生人沟通,遇到事不想麻烦别人。

+

但不知为什么,在工作中总是做着做着就到了管理岗,每次在做了一段时间管理后我总给自己找个理由:我还年轻,还有技术上的成长空间,不能把太多时间花费在管理上,还不到那个时机。

+

关于做着做着就到了管理岗这个事,我想和中国的「学而优则仕」这个传统有关,但我真的不是什么优等生,至于为什么经常是我被选中,再容我想想,后边有机会我会再做个复盘。

+

我在去年5月份转岗到了公司的推荐工程部门,但因为各种原因自己并没有在推荐这个领域上有特别大的精进,但今年6月初的时候,公司核心产品部门的后端TL和推荐工程后端TL(也就是我的直属TL)都提了离职,果不其然,我又被推了上来。涉及的这两个团队恰好都在我的+2层的TL下,他在一番考虑后决定把两个团队合并,都挂到我下边,这对我来说是个很大的挑战。

+

我在一个月前已经提了陪产假,准备在6月份休个安心的假期,这突如其来的变故让我措手不及,其一是我对核心产品的项目几乎没有了解,其二是我对推荐领域也不在行,这使得我非常焦虑。

+

焦虑也不只是因为我的新职位,而是不光是他们两个离职,随着人心惶惶那一批离职潮一下子走了 4 个人。

+

因为缺乏相关经验,加上突然多人离职的人手不足,我就拼命的把活往自己身上揽,让自己忙碌起来,因为担心一不忙了整个团队就会崩塌,另一个让自己忙的目的是故意把自己塞满,想表现出来的样子就是「老板你看,我都这么努力还是没扛下来,这就不怪我了」。以至于经过一多月的狂奔,现在养成了一个心态是一无事可做就会慌张,感觉自己是不是错失了什么。

+

实际的现状是经过HR和其他部门协助面试,我们迅速补够了人手,工作节奏可以正常流转开了,我也不应该再在具体的工作上投入太大的精力,而是站在相对的高度做一些顶层设计,但我还是转不过这种心态。

+

我这是典型的:「用战术上的勤奋掩盖战略上的懒惰」。因为不想思考,就自己在行动上瞎忙,明明可以交给其他人的不重要工作,非要花费自己大量时间去做。

+

前段时间学习了哈佛的积极心理学,其中有一点应用在自己身上就是我太渴望被多数人认同,而不是被理解,以至于自己用极其忙碌的方式来证明自己。

+

在后边的工作中我需要更多的松驰感,而且不能再用自己年龄还小还没到管理的年纪来逃避了,具体来说:

+
    +
  1. 多想,做好规划
  2. +
  3. 不再想着做团队内超级兵
  4. +
  5. 让其他人了解我而不用认同我
  6. +
  7. 稳定情绪
  8. +
+
+

以下部分是在5月中旬,团队内有2个人先后提了离职(那时候我还没有意识到问题的严重性,最终一共有4人离职),在一次周会上我做的发言:

+

首先感谢zm和dw两位同学之前为团队作出的贡献。zm(18年12月5日入职)在探探工作了4年半,从一个互联网从业者的角度来看,算是很长了。在推荐工程团队做出了不可磨灭的贡献,回老家也算是荣归故里了。dw虽然刚刚工作了两年(21年5月21日入职),但在推荐和核心业务方向都做出了很大的贡献。特别是上个季度,在核心缺少人手的情况下,cover 了大部分的需求,快速掌控了核心的几个业务子域。现在刚好有个肉翻的机会,去国外看看也挺好。

+

不知道大家是否看过《权利的游戏》,里面小拇指说过一句话:“混乱不是深渊,混乱是阶梯”。短期动荡状态是让自己成长和脱颖而出的一个很好的机会。一个人的成功不在于是否努力多做两件事,而在于能否跃迁到更高的量级。前边说的成长不止技术上的成长,更是心智上的,因为你比别人见过更多的起起落落。

+

如果真的问我团队里突然有两个人离职,现在把指挥棒交到了我手里,我慌不慌,我会很诚实地回答我会有点慌。但我不会觉得这是个坏事,我会把它当成一个挑战。两位同学的离职确实给我们的团队带来了一定的影响,但我们要正视现实,勇敢接受挑战。有句老话叫:“铁打的营盘流水的兵”。我在去年转到推荐组的时候,给我的最大感受是推荐组是整个探探最优秀的团队,只要我们的营盘还在,流水的兵是个很正常的事情。

+

在推荐领域,大家都是我的老师,在核心业务方向,我们剩下来的这些同学都是新人。我并不认为只有能力最强的人才可以做 TL,有问题我可以和这么多优秀的同学一起商量着来,我的主要工作是为大家做好后勤工作,可以帮大家扫清前进的障碍。一个好的 TL 应该是一个没什么存在感的角色,我相信在这么多优秀同学的一起努力下我们可以顺利度过这段时间的小动荡。不管大家是否承认,这段时间我们都有一些懈怠,大家振作起来,踏下心来把手头的工作做好,是对公司的负责,也是对自己的负责。

+

我原本的计划是从上周开始休陪产假,但是知道泽明的事后就往后推了一个月。我是我们组里唯一有娃的,甚至是有两个娃的,大家可能不太能理解我现在的心情,谁不想多在家陪陪刚出生水嫩嫩的娃。但是我还是想把这段时间支持下来。

+

了解我的人都知道,我比较希望追求 work-life balance(迫于现在由于通勤太远无法实现),所以不会特别卷大家,这跟前几任领导人是一样的。在管理风格上,我也不是一个爱 push 大家去工作的风格,大家各自约定好自己的 promise 承诺,在约定时间交付成果,中间做好必要的反馈就可以。我可能和大部分程序员不太一样,我属于早起鸟类型的,可以早起但是不能熬夜。

+

关于我自己的稳定性问题,我在短期内是不会走的。其一是出于责任心,其二是出于房贷和两个娃的压力,当然公司给我大礼包让我走除外。

+

这段时间核心产品的需求我会分给每个同学去做,jz的业务方向会逐渐往核心业务转移,必要时也会给rp、kq分核心业务的需求。新同学ml尽快熟悉业务,估计三周后也可以逐渐接需求,缓解一些业务压力。再往后我们还有三个 HC,所以困难是延期的,未来是光明的。

+

最后,大家一定要多注意身体,规律作息,多运动,身体是革命的本钱。

+ +
+ + + + + + + +
+ + + + + + +
+ +
+ + + +
+ + + + + + + + +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2023/learn-pottery/1.jpeg b/2023/learn-pottery/1.jpeg new file mode 100644 index 0000000000..5bb5ed6cd3 Binary files /dev/null and b/2023/learn-pottery/1.jpeg differ diff --git a/2023/learn-pottery/2.jpeg b/2023/learn-pottery/2.jpeg new file mode 100644 index 0000000000..e7d5c595f5 Binary files /dev/null and b/2023/learn-pottery/2.jpeg differ diff --git a/2023/learn-pottery/3.jpeg b/2023/learn-pottery/3.jpeg new file mode 100644 index 0000000000..6b7a3d0b70 Binary files /dev/null and b/2023/learn-pottery/3.jpeg differ diff --git a/2023/learn-pottery/index.html b/2023/learn-pottery/index.html new file mode 100644 index 0000000000..11b7269731 --- /dev/null +++ b/2023/learn-pottery/index.html @@ -0,0 +1,509 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 念念在少年宫学陶艺 | 贾攀的流水账 + + + + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

贾攀的流水账

+ +
+

Panmax's Blog

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+ + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ 念念在少年宫学陶艺 +

+ + +
+ + + + +
+ + +

少年宫这个词我印象中只在我的小学阶段出现过,应该还是在一些青少年报刊杂志上看到的。

+

百度对其定义是:

+
+

我国在学校以外对少年儿童进行政治教育和开展集体文化活动的机构。

+
+

我的理解就是经国家认可的、公立性质的课外培训机构。

+
+

今年一月底,春节后从老家开车回北京,在服务区休息的时候遇到了另外一对也在休息的家长,稍微攀谈了一会发现是老乡,聊着聊着就聊到的对方爸爸的工作。对方爸爸在北京的少年宫里做老师,细问之下是在我们住的丰台区少年宫,再细问他教的是我娃一直想学的陶艺课程。

+

他说今年9月1日办理新学期入学,每周末上课,到时候如果我们想进的话可以联系他,他可以给我们塞个名额。正常来说少年宫是非常不好进的,几万人争几千个名额。8月份的时候我们联系他,他给我们加上了塞,让我们9月1日来上课就行了,价格也非常公道。

+

今天是念念第一天来少年宫上课,从家开车过来大概20分钟,这是我第一次带念念开车出门,之前也出过一次,不过距离很短就是从家门口到地铁站5分钟不到的路程,所以那次就不算了。

+

我让念念坐在后排,给她记上安全带,后排有一本装修公司之前给我们选装修风格和材料的书,她因为无聊就翻那本书看,时不时问我一些关于装修的问题,出奇的乖。

+

+

开到地方后发现少年宫不对外开放停车,而且因为是在一条繁华街道上,路上也停不了车。其他之前已经来过的家长会把车临时停一下,孩子下车后直接进去。我们是第一次来,人生地不熟,念念也不认识老师不知道教室在哪。于是我就跟她说我们需要找个地方停车,大概又开了5分钟,拐进一个胡同的小区里,找了地方停了车。

+

我看到导航上显示如果步行回去需要10分钟,再考虑到念念的步行速度可能就要15分钟了,上课就会迟到。我不想让念念第一次上课就迟到,于是跟她商量了一下,扫了一辆共享单车她坐在座位上我推着他走。时间还是有些紧张,中间有一段我就开始小跑,念念第一次坐在这么高的自行车座位上,脚够不到车蹬,既害怕又兴奋,刚开始跟我说她害怕,后边我跑起来后她说太刺激了😂。

+

跑到学校门口后我已经满身大汗,刚要进去保安拦住我说家长不能进,我和保安解释说我们第一次来,保安说孩子往里走有老师接她,后来来了个老师带着念念去了她们上课的教室。

+

看着念念进去后,我先步行回刚才停车的小区,把车开到离学校稍微近一些的另一个停车场停好了车,在附近买了瓶阿萨姆奶茶,坐在学校附近一条小路的石阶上用手机扣这篇流水账。

+

+

没多久老师把念念上课的照片发了过来,看到她满脸发自内心的喜悦,老父亲也就满足了。

+

+

在上课来的路上,念念说她以后要给我做酒杯、咖啡杯,哈哈,期待!

+ +
+ + + + + + + +
+ + + + + + +
+ +
+ + + +
+ + + + + + + + +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2023/less-flag/0.jpeg b/2023/less-flag/0.jpeg new file mode 100644 index 0000000000..401f074606 Binary files /dev/null and b/2023/less-flag/0.jpeg differ diff --git a/2023/less-flag/1.jpeg b/2023/less-flag/1.jpeg new file mode 100644 index 0000000000..20032435e4 Binary files /dev/null and b/2023/less-flag/1.jpeg differ diff --git a/2023/less-flag/2.jpeg b/2023/less-flag/2.jpeg new file mode 100644 index 0000000000..15698944ba Binary files /dev/null and b/2023/less-flag/2.jpeg differ diff --git a/2023/less-flag/3.jpeg b/2023/less-flag/3.jpeg new file mode 100644 index 0000000000..d080022535 Binary files /dev/null and b/2023/less-flag/3.jpeg differ diff --git a/2023/less-flag/4.jpeg b/2023/less-flag/4.jpeg new file mode 100644 index 0000000000..2d3ac16a8d Binary files /dev/null and b/2023/less-flag/4.jpeg differ diff --git a/2023/less-flag/5.jpeg b/2023/less-flag/5.jpeg new file mode 100644 index 0000000000..d4f8a07bd6 Binary files /dev/null and b/2023/less-flag/5.jpeg differ diff --git a/2023/less-flag/6.jpeg b/2023/less-flag/6.jpeg new file mode 100644 index 0000000000..d09c5f4e5d Binary files /dev/null and b/2023/less-flag/6.jpeg differ diff --git a/2023/less-flag/7.jpeg b/2023/less-flag/7.jpeg new file mode 100644 index 0000000000..76b6d11c06 Binary files /dev/null and b/2023/less-flag/7.jpeg differ diff --git a/2023/less-flag/8.jpeg b/2023/less-flag/8.jpeg new file mode 100644 index 0000000000..36984f0064 Binary files /dev/null and b/2023/less-flag/8.jpeg differ diff --git a/2023/less-flag/index.html b/2023/less-flag/index.html new file mode 100644 index 0000000000..0350d5d146 --- /dev/null +++ b/2023/less-flag/index.html @@ -0,0 +1,536 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 少立 flag | 贾攀的流水账 + + + + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

贾攀的流水账

+ +
+

Panmax's Blog

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+ + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ 少立 flag +

+ + +
+ + + + +
+ + +
+

flag 就像个咒语,立了基本都会反向达成。比如:今年我一定要减5斤,现在已经涨了5斤。

+
+

6月12日的时候,我写了一篇流水账,当时立了个 flag 说要在一天之内考下摩托车驾照,但第二天在科目二考试中挂了,拖着狼狈的身体在回家的高铁上,又写了篇流水账记录当时丧气、失望的心情。

+

前一天还「男人至死是少年」的豪言壮志,第二天就成了「摩托车也不是我的必需品」的泄了气的气球。

+

经过那次考试失利后,我脑海中一直回荡着考试过程的景象,反复在脑子里对那次考试进行复盘,设想如果我当时怎么怎么做就不会挂了。越是想让自己不去回忆这件事,反而回忆的更多(白熊效应)。

+

经过了好几天都无法消化这次失败,机缘巧合在一篇知乎回答里看到有用户推荐「哈佛幸福课(积极心理学)」,我觉得应该会对我有帮助,所以开始学习这门课程。

+

学到一半多的时候,我挥之不去的挫败感基本被课程中的观点治愈了,尤其是那些关于失败的论点:

+
    +
  • 学会失败,从失败中学习,要想进步就必须学会失败。
  • +
  • 要像接受我们所爱的人的失败那样去接受自己的失败。
  • +
  • 失败避免不了,你要从失败中学习。
  • +
  • 最成功的人往往是失败次数最多的。
  • +
  • 把失败看成成长的工具,这可以更好的了解自己。
  • +
  • 在成功或失败过后,会有大起大落,但我们会恢复过来,我们一生基本沿基准的幸福发展。
  • +
  • ……
  • +
+

于是在给自己做好心理建设后,我准备二战。

+

上一次考试是在我休陪产假期间,我选择了周二这个工作日。考试地点一周有三天可以考试:周二、周四、周六。这一次为了不耽误工作,只能选择周六考试。我在周四报名并缴纳了补考费。周五下午5点我去吃了个驴肉火烧,打包了一个火烧准备路上吃。6点多我提前下班从公司出发,7点多到达大巴车集合地点,再次坐上去德州的大巴车。这一天是6月30日。

+

为了积攒一些好运,我花了99元在小宇宙购买了「谐星聊天会特别季」节目。第一次去的时候就看到这个节目,但没有购买。当时心中有个念头一闪而过:我不会因为没买而挂吧?最后果然挂了,我在复盘的时候也想过有没有可能是没买这个节目导致的😂。

+

认真练习

7月1日凌晨1点多到达训练地点,开始为期7小时的「特种兵」训练,因为这次我只需要补考科目二,所以可以把所有精力都放在科目二上。由于是周末,考试的人很多,每练一轮要等20多分钟,这中间我没有休息,一直练到天亮。

+

凌晨2点:

+

凌晨3点:

+

凌晨4点:

+

凌晨5点:

+

早上6:30又吃了一次非常难吃的包子加小米粥:

+

准备考试

8点左右,我们被拉到车管所办事大厅办理考试报名,由于我是补考,手续比较简单。

+

这一批有3个人要补考,8点半提前把我们拉到了考试地点,9点开始考试,我们几个是当天最先考试的三个人,我是第二个考试,三个人都过了。

+

然后就是等待科目四考试,十点半左右满分考完了科目四,至此我的摩托车考试流程结束了,所有科目都是满分。

+

考完试后第一件事就是卸载了「驾考宝典」APP,感谢它的陪伴😂

+

考完科目四后在车管所门口的小卖店买了个雪糕奖励自己:

+

11点多新证就下来了

这一天是7月1日,和党的生日同一天:

+

+

定了下午的高铁票回老家,看看我的大儿子,还有大女儿。

+

打车去高铁站的路上让出租车司机推荐了一家当地人经常买的扒鸡店,买了3只扒鸡,一只给岳父家,两只我们吃。

+

这一次买的是无座票,在高铁上找了个角落席地而坐,2个小时就到家了。

+

+

结语

这一次来考试,我没有再立 flag,安静的来,安静的走。

+

因为没有立 flag,也没有人知道我又来考试了,所以这一次的心情也不一样,没有给自己太高的预期和压力,就当是来德州旅游,体验生活,如果再挂了就找机会再来。在练习场通宵练习,看着太阳落下,月亮升起;月亮落下,太阳升起,整个过程很奇妙。

+

其实我通常情况下都是先把事办成再对外宣布,因为担心对外宣布后就像泄露了天机,容易遭天谴而失败,这一次属实轻敌了,以后也不会轻易再立flag。

+ +
+ + + + + + + +
+ + + + + + +
+ +
+ + + +
+ + + + + + + + +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2023/my-motorcycle-is-gone/IMG_9606.jpeg b/2023/my-motorcycle-is-gone/IMG_9606.jpeg new file mode 100644 index 0000000000..6e8406a72c Binary files /dev/null and b/2023/my-motorcycle-is-gone/IMG_9606.jpeg differ diff --git a/2023/my-motorcycle-is-gone/IMG_9607.jpeg b/2023/my-motorcycle-is-gone/IMG_9607.jpeg new file mode 100644 index 0000000000..2d00faf573 Binary files /dev/null and b/2023/my-motorcycle-is-gone/IMG_9607.jpeg differ diff --git a/2023/my-motorcycle-is-gone/index.html b/2023/my-motorcycle-is-gone/index.html new file mode 100644 index 0000000000..a08984f6e3 --- /dev/null +++ b/2023/my-motorcycle-is-gone/index.html @@ -0,0 +1,498 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 我的小摩托没有了 | 贾攀的流水账 + + + + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

贾攀的流水账

+ +
+

Panmax's Blog

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+ + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ 我的小摩托没有了 +

+ + +
+ + + + +
+ + +

昨天刚信誓旦旦立了个flag准备1天内搞定摩托车驾照,然后买一辆本田幼兽。

+

山东德州这边允许一天考4项,昨晚8点从北京坐驾照大巴出发,凌晨1点到德州,一夜没睡,练习到早上8点,人比较多,教练也基本不看,练习效果就一般,然后开始考试。不出意外的出现了意外,我挂在了科目二上,科目一满分,科目三满分。我在训练时是表现比较好的,考试的时候和训练车带速不一样,第一次自己大意没有回头看导致撞杆了,第二次熄火两次,扣20分,坡道起步的时候离边线太远又扣10分,最后没有通过。

+

因为无法继续科目四考试,我买了最近的一班高铁回北京,不用晚上坐大巴回了。下次再考试需要等10天,我准备放弃了,就当2000块钱买了个教训吧∶捷径是世界上最远的距离。

+

而且摩托车也不是我的必需品,考完驾照后又要花更多的钱买车,倒不如及时止损,到此为止。

+

小幼兽,拜拜啦。

+

我心态超好!

+

+

打了败仗,溜了溜了。

+

+

前两天找人写了个扇面,内容是红楼梦里黛玉说的一句话「事若求全何所乐」,很应景。

+ +
+ + + + + + + +
+ + + + + + +
+ +
+ + + +
+ + + + + + + + +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2023/my-new-house/1.jpeg b/2023/my-new-house/1.jpeg new file mode 100644 index 0000000000..87fbfa11bd Binary files /dev/null and b/2023/my-new-house/1.jpeg differ diff --git a/2023/my-new-house/1.png b/2023/my-new-house/1.png new file mode 100644 index 0000000000..665b14f3d5 Binary files /dev/null and b/2023/my-new-house/1.png differ diff --git a/2023/my-new-house/10.jpeg b/2023/my-new-house/10.jpeg new file mode 100644 index 0000000000..47ed54eace Binary files /dev/null and b/2023/my-new-house/10.jpeg differ diff --git a/2023/my-new-house/11.jpeg b/2023/my-new-house/11.jpeg new file mode 100644 index 0000000000..8e33ab66fb Binary files /dev/null and b/2023/my-new-house/11.jpeg differ diff --git a/2023/my-new-house/12.jpeg b/2023/my-new-house/12.jpeg new file mode 100644 index 0000000000..da74c3ebf3 Binary files /dev/null and b/2023/my-new-house/12.jpeg differ diff --git a/2023/my-new-house/13.jpeg b/2023/my-new-house/13.jpeg new file mode 100644 index 0000000000..34d4135dfd Binary files /dev/null and b/2023/my-new-house/13.jpeg differ diff --git a/2023/my-new-house/2.jpeg b/2023/my-new-house/2.jpeg new file mode 100644 index 0000000000..3cb58740dc Binary files /dev/null and b/2023/my-new-house/2.jpeg differ diff --git a/2023/my-new-house/3.jpeg b/2023/my-new-house/3.jpeg new file mode 100644 index 0000000000..b641260b3b Binary files /dev/null and b/2023/my-new-house/3.jpeg differ diff --git a/2023/my-new-house/4.jpeg b/2023/my-new-house/4.jpeg new file mode 100644 index 0000000000..d3dad7038f Binary files /dev/null and b/2023/my-new-house/4.jpeg differ diff --git a/2023/my-new-house/5.jpeg b/2023/my-new-house/5.jpeg new file mode 100644 index 0000000000..59bb349913 Binary files /dev/null and b/2023/my-new-house/5.jpeg differ diff --git a/2023/my-new-house/6.jpeg b/2023/my-new-house/6.jpeg new file mode 100644 index 0000000000..34cd397dce Binary files /dev/null and b/2023/my-new-house/6.jpeg differ diff --git a/2023/my-new-house/7.jpeg b/2023/my-new-house/7.jpeg new file mode 100644 index 0000000000..658de5fd08 Binary files /dev/null and b/2023/my-new-house/7.jpeg differ diff --git a/2023/my-new-house/8.jpeg b/2023/my-new-house/8.jpeg new file mode 100644 index 0000000000..01a82c60a1 Binary files /dev/null and b/2023/my-new-house/8.jpeg differ diff --git a/2023/my-new-house/9.jpeg b/2023/my-new-house/9.jpeg new file mode 100644 index 0000000000..960c3d62aa Binary files /dev/null and b/2023/my-new-house/9.jpeg differ diff --git a/2023/my-new-house/index.html b/2023/my-new-house/index.html new file mode 100644 index 0000000000..e7cad46fed --- /dev/null +++ b/2023/my-new-house/index.html @@ -0,0 +1,532 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 新房子初步竣工 | 贾攀的流水账 + + + + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

贾攀的流水账

+ +
+

Panmax's Blog

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+ + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ 新房子初步竣工 +

+ + +
+ + + + +
+ + +

去年十一前在潘家园附近买了一套小三居,今年年后开始装修装到五一前,过程中陆续置办了一些家电。

+

今天去宜家给我的小书房兼卧室选了床和床垫,之前定的窗帘也约的今天上门安装,这样下来基本就可以入住了。

+

+

我选的床垫其实很厚,被压缩的状态下看起来有些单薄。

+

+

办公桌和衣柜比较实用,准备在办公桌上挂上一张洞洞板,挂一些小东西。

+

目前家具方面还差一张主卧的软床,一个电脑椅和一套餐桌椅。

+

电脑椅可选的太多了,一直没有选好,趁着这两天618活动,打算赶紧选一个,想选个白色的。

+

截止目前已经在装修上花费了30多万,真是太费钱了,下边是我在装修期间做的记录

+

+

房子有三个卧室,其中两个卧室住人,另外一个通阳台的卧室准备做一个游戏房,放一张大地毯,两个懒人沙发,装个投影仪,再搭配一套Xbox、PS5之类的。

+

下边是东边的主卧,需要再放置一张一米八的床,再单独配个床头柜。

+

+

下边是通阳台的卧室,准备做成游戏房

+

+

洗手间做了干湿分离,可以在客厅洗漱,洗手间洗澡上厕所

+

+

+

洗衣机和烘干机也放在了客厅,这次单独买了专业的烘干机,目的是为了不在阳台挂满衣服挡住阳光

+

+

洗衣机和烘干机是我很得意的两件家电,选的西门子,本来想选博世,后来得知西门子和博世祖上是一家,而且西门子的洗碗机和冰箱有比较喜欢的,所以洗衣机和洗碗机也就买了西门子的。

+

+

前段时间我二阳时来这小住了几天,用这俩设备洗烘了一次,出来的衣服非常舒适。

+

为了解决爱做饭不爱刷碗的问题,还在厨房配置了洗碗机,也是选的西门子的。

+

+

虽然冰箱不是双开门,但容量也非常大,有4个独立空间,燃气热水器也选的比较好的史密斯。

+

+

房子的阳台也非常给力,整体朝南,很长很大、采光非常好,后边没有其他楼层遮挡视线,楼下可以看到垂杨柳小学。

+

+

+

+

这套房子在交钱之前只知道是6层,交完钱后才知道,房间号是606,大吉大利。

+ +
+ + + + + + + +
+ + + + + + +
+ +
+ + + +
+ + + + + + + + +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2023/my-room-equipped-computer-chair/0.jpeg b/2023/my-room-equipped-computer-chair/0.jpeg new file mode 100644 index 0000000000..18e15b1a88 Binary files /dev/null and b/2023/my-room-equipped-computer-chair/0.jpeg differ diff --git a/2023/my-room-equipped-computer-chair/1.jpeg b/2023/my-room-equipped-computer-chair/1.jpeg new file mode 100644 index 0000000000..fa30d0f875 Binary files /dev/null and b/2023/my-room-equipped-computer-chair/1.jpeg differ diff --git a/2023/my-room-equipped-computer-chair/2.jpeg b/2023/my-room-equipped-computer-chair/2.jpeg new file mode 100644 index 0000000000..56ed4f2510 Binary files /dev/null and b/2023/my-room-equipped-computer-chair/2.jpeg differ diff --git a/2023/my-room-equipped-computer-chair/3.jpeg b/2023/my-room-equipped-computer-chair/3.jpeg new file mode 100644 index 0000000000..3eb3163dff Binary files /dev/null and b/2023/my-room-equipped-computer-chair/3.jpeg differ diff --git a/2023/my-room-equipped-computer-chair/index.html b/2023/my-room-equipped-computer-chair/index.html new file mode 100644 index 0000000000..4578780d49 --- /dev/null +++ b/2023/my-room-equipped-computer-chair/index.html @@ -0,0 +1,501 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 我的小屋配置了电脑椅 | 贾攀的流水账 + + + + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

贾攀的流水账

+ +
+

Panmax's Blog

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+ + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ 我的小屋配置了电脑椅 +

+ + +
+ + + + +
+ + +

在小红书上研究人体工学椅,琳琅满目看的我头晕眼花,有官方账号自己发的,有恰饭的,最后选的是网易严选探险家3D,趁着618有活动下单了。

+

+

选这个椅子没有什么特别的原因,就是选到最后实在不想选了,正好最后看到了这个,就定它了。

+

今天看物流信息显示已送达,但是并没有小哥和我联系,打电话过去问说放在了门口,我兴高采烈开车到新房,到了之后才发现没拿钥匙,这时候纠结是再开回去拿一趟钥匙还是找个跑腿给我送过来,虽然时间上差不多,但我选择了后者。

+

进屋后花了20分钟进行组装,之后坐下感受了一会,很舒适。

+

+

我买的是带脚踏的版本,累了还可以半躺着休息,官方还送了一个午休毯可以盖。

+

+

脚踏也可以收回当成正常的办公椅使用,有好几个地方可以调节的。

+

+

准备7月份搬进来,这样到公司的距离可以缩短一般,幸福指数可以有很大提升。

+ +
+ + + + + + + +
+ + + + + + +
+ +
+ + + +
+ + + + + + + + +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2023/my-son-name/1.jpeg b/2023/my-son-name/1.jpeg new file mode 100644 index 0000000000..8707b3ef06 Binary files /dev/null and b/2023/my-son-name/1.jpeg differ diff --git a/2023/my-son-name/index.html b/2023/my-son-name/index.html new file mode 100644 index 0000000000..57c6d9c6de --- /dev/null +++ b/2023/my-son-name/index.html @@ -0,0 +1,499 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 我儿子名字的由来 | 贾攀的流水账 + + + + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

贾攀的流水账

+ +
+

Panmax's Blog

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+ + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ 我儿子名字的由来 +

+ + +
+ + + + +
+ + +

我儿子叫「贾登一」

+

登一连起来的寓意是:「登峰造极,一路顺风」(然而并不是这个理由)

+

真实的情况是:

+

从小我给别人介绍我名字的时候都是说:我叫贾攀,攀登的攀

+

实话实说,在我小的时候压根不知道攀登是什么意思。

+

不过那个时候就对「登」这个字很感兴趣,所以一开始打算给儿子取名贾登。

+

后来想还是用三个字吧,于是在最后补了个「一」字,跟姐姐名字也能呼应。

+

以后我儿子给别人介绍自己名字的时候可以说:我叫贾登一,攀登的登。

+

我是攀登的

+

他是攀登的

+

一看就知道是父子俩。

+

+ +
+ + + + + + + +
+ + + + + + +
+ +
+ + + +
+ + + + + + + + +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2023/new-hhkb/1.png b/2023/new-hhkb/1.png new file mode 100644 index 0000000000..5942750ac2 Binary files /dev/null and b/2023/new-hhkb/1.png differ diff --git a/2023/new-hhkb/2.jpeg b/2023/new-hhkb/2.jpeg new file mode 100644 index 0000000000..8fb3d702dc Binary files /dev/null and b/2023/new-hhkb/2.jpeg differ diff --git a/2023/new-hhkb/3.png b/2023/new-hhkb/3.png new file mode 100644 index 0000000000..789df508a0 Binary files /dev/null and b/2023/new-hhkb/3.png differ diff --git a/2023/new-hhkb/4.jpeg b/2023/new-hhkb/4.jpeg new file mode 100644 index 0000000000..a0e058f6e9 Binary files /dev/null and b/2023/new-hhkb/4.jpeg differ diff --git a/2023/new-hhkb/5.jpeg b/2023/new-hhkb/5.jpeg new file mode 100644 index 0000000000..ec676a2763 Binary files /dev/null and b/2023/new-hhkb/5.jpeg differ diff --git a/2023/new-hhkb/6.jpeg b/2023/new-hhkb/6.jpeg new file mode 100644 index 0000000000..58134b5d37 Binary files /dev/null and b/2023/new-hhkb/6.jpeg differ diff --git a/2023/new-hhkb/7.jpeg b/2023/new-hhkb/7.jpeg new file mode 100644 index 0000000000..6e037f98b8 Binary files /dev/null and b/2023/new-hhkb/7.jpeg differ diff --git a/2023/new-hhkb/index.html b/2023/new-hhkb/index.html new file mode 100644 index 0000000000..65bf5b46fb --- /dev/null +++ b/2023/new-hhkb/index.html @@ -0,0 +1,519 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 又入手了一个 HHKB | 贾攀的流水账 + + + + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

贾攀的流水账

+ +
+

Panmax's Blog

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+ + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ 又入手了一个 HHKB +

+ + +
+ + + + +
+ + +

15年我刚工作不久,当时有个 App 叫网易海淘,我通过这个平台在日本亚马逊买到了自己的第一个机械键盘 HHKB,记得当时的价格是1550左右。大学时候好几个舍友都因为玩游戏买了机械键盘,但我一直用的都是罗技很便宜的那款。

+

这个 HHKB 键盘到现在陪伴了我8年多年,电脑换了好几个、工作换了好几份,没换的一直是这个键盘。有点像换马不换鞍的感觉,HHKB 一直作为我最亲密的战友陪伴着我,我在公司写代码、调戏妹子、和其他人对喷都离不开它,我在公司拍工位照片时也都有它的身影。

+

17年:

+

+

18年:

+

+

22年:

+

+

这个键盘我是越用越顺手,越用越喜欢,配合上 Keyboard Maestro,大部分工作都可以通过键盘完成,我也会在 IDE 里配置很多自己顺手的快捷键。

+

入职现在这家公司的时候,公司给我配的是一个15年的15寸 MacbookPro,感觉性能差,所以我在公司一直用的自己的19年有 Touchbar 版本的 Pro,21年左右把自己的 Mac 出了爱回收,换了 M1 Pro。

+

公司配的电脑就长期在家里搁置,周末的时候偶尔用来处理一些临时的工作,公司配的电脑性能又差电池也不够用,不插电源的状态下半小时就没电了。

+

在公司用公司配的电脑的同事陆续都换了新电脑,好一些的换了 M1,差一些的也换了我之前用的 Mac 同等的配置,我也在上个月初休陪产假前找 IT 换了个新的电脑,虽然不是 M1,但性能也不错,i9的 Intel 处理器。

+

用过这款 Mac 的都知道,这个系列最大的槽点就是蝶式键盘,键程极短,毫无打字体验,这就促成了我想在配一个键盘的想法,最开始想着在闲鱼上淘个便宜的,但看来看去没有心怡的,毕竟自己用过的只有 HHKB。

+

今天我在逛咸鱼的时候看到北京有个人卖 HHKB,他说这个键盘是公司年会奖品,拆封后用了一下不适应就收起来了,标价900。看他的配图是 Professional Classic 的无刻(键帽上没有字)版本,网上说这个版本就是我在用的 Professional2的升级版。我在淘宝查了下价格,基本在1650左右,如果真的如他描述只是拆开试了一下,那么这个900的价格还是很有吸引力的。

+

我在闲鱼上跟他交流了一下,通过他的回答来看确实是个外行,也不像是骗子,他说键盘在公司,公司在华茂写字楼,今天下午要去公司开会,可以面交。华茂写字楼离我不远,我和他一番周旋后讲到了850的价格,在交易前他再三让我确认是否会用这个键盘,我就说我可以学习。

+

因为今天北京下暴雨,取键盘的过程还是很坎坷的。我把车听错了停车场,听到了 SKP 购物中心的地下,从停车场上去后冒着雨找对方的写字楼,对方因为要开会,没办法给我,只能把键盘放在了大厅的一个角落里让一名保洁阿姨帮忙看着,我找了好久才找到他的写字楼,当时全身已经湿透了,拿到键盘后往回走找自己的车又找了好久,而且回去的时候才知道,负一层是互通的,早知道我就不冒这么大雨狂奔了。

+

到家后迫不及待打开盒子开始欣赏这个键盘,真新啊,非常喜欢 HHKB 这种设计的简洁感,HHKB 全名 Happy Hacking Keyboard,果然是程序员的开心键盘。

+

+

+

这篇文章就是我用新的键盘完成的,新的键盘相较于 Professional2 来说更软、更轻、更柔一些,相对更静音,Professional2稍微清脆一些,两者不分伯仲,我都喜欢。虽然新键盘上没有刻字,但用起来毫无违和感,毕竟之前的键盘已经用了8年多。

+

+

最开始打算用不到100的价格随便买个普通机械键盘,最后缺花了850买了个自己心怡的HHKB,虽然花了多8倍的价格,但真的是买到心坎儿里了。我对自己不熟悉的领域很谨慎,哪怕不到100块钱也不愿轻易去花,对自己热爱的东西很果断,花多一些钱也愿意。

+

如果你的男朋友是程序员,相信我,送他这款键盘准没错👨🏻‍💻

+
+

BTW,开车回家的路上还在下雨,我在一个十字路口亲眼目睹了一场车祸,两个车都赶在变黄灯前加速,一个左转一个执行,我眼看着两个车就想游乐场的碰碰车一样撞在了一起,听到 duang 一声、地面颤动了一下,事情太突然,当时的感觉不太真实,车上的人应该都没有大碍,过十字路口还是要注意安全,黄灯能不抢还是不抢。

+

+ +
+ + + + + + + +
+ + + + + + +
+ +
+ + + +
+ + + + + + + + +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2023/nian-nian-room/index.html b/2023/nian-nian-room/index.html new file mode 100644 index 0000000000..f6be08e635 --- /dev/null +++ b/2023/nian-nian-room/index.html @@ -0,0 +1,492 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 念念的房间 | 贾攀的流水账 + + + + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

贾攀的流水账

+ +
+

Panmax's Blog

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+ + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ 念念的房间 +

+ + +
+ + + + +
+ + +

昨晚又是一整晚没睡,因为一家人来新家开荒,除了我爸,其他人都在这里过夜。由于新家有一个卧室还没有安床,所以我妈和念念就睡在我的床上了,我打的地铺。但因为不太适应,整晚都没睡着。

+

半夜睡不着时,我想起了白天一件有点内疚的事:

+

我们有个卧室是专门给念念准备的,墙壁刷成了淡粉色,还买了她喜欢的床。装好床的那天,她高兴极了,在自己的床上蹦了好久,一直想着如何装饰自己的房间。

+

这次回来,她看到自己的床上放了登登的衣服,地上也有一些其他的杂物。于是,她把那些不属于她的东西全都扔到了其他房间。我当时很严肃地批评了她,告诉她如果不让别人把东西放到她的房间,她以后也就别进其他房间了。她当时一脸惶恐,赶紧把她刚才扔出去的东西一件件搬回来,以讨好我。

+

深夜静悄悄的时候,我想到念念在这件事上并没有错。既然我已经告诉过她那是她的房间,那么她就有权利让自己的房间保持干净和整洁。再者,还有一个月念念就6岁了,我们之前蜗居在60多平的房子里,她一直没有属于自己的空间。第一次拥有自己的房间肯定是非常想占为己有的,我可以理解她,因为我小时候也有这样的想法。想想自己小时候,如果得到了自己非常喜爱的东西,肯定也不愿意让别人糟蹋。在拥有自己房间这一点上,我觉得非常亏欠她,在北京这个寸土寸金的地方只能委屈一下她了。

+

我们计划国庆节前带念念去趟上海迪士尼实现她的公主梦,我对自己的唯一要求是对她多一些耐心,不要因为她的一些小孩子的无理要求而对她发脾气。我就她这么一个女儿,不宠着她宠谁呢。去迪士尼的钱用的是我准备买摩托车的钱,之前因为考试失利,摩托车驾照考了两次,第二次考完后摩托车就对我没那么大吸引力了,所以也迟迟没有订车,这笔钱拿出来带念念去玩一趟把。

+

距上次去远的地方玩刚好过去3年,上一次是离职上家公司入职 TT 之前,到新疆玩了一个星期,一晃三年过去了,时间真快。说到这里,我奉劝各位还没结婚、没生娃的朋友及时行乐,趁着自由能出去玩就多出去玩。也奉劝那些不想结婚、不想生娃的朋友,如果一个人过得开心,请坚持你们的想法。

+ +
+ + + + + + + +
+ + + + + + +
+ +
+ + + +
+ + + + + + + + +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2023/not-get-into-big-factory/index.html b/2023/not-get-into-big-factory/index.html new file mode 100644 index 0000000000..3bdea92acd --- /dev/null +++ b/2023/not-get-into-big-factory/index.html @@ -0,0 +1,508 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 注定进不去的大厂 | 贾攀的流水账 + + + + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

贾攀的流水账

+ +
+

Panmax's Blog

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+ + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ 注定进不去的大厂 +

+ + +
+ + + + +
+ + +

前几天,从我当前所在公司离职不久去了程序员终点站字节跳动的领导联系我,问我考不考虑机会,我考虑几分钟后委婉的拒绝了,这不是我第一次拒绝大厂基本唾手可得的机会,之前也有其他前领导联系过我去小红书负责他下边新开的业务线,也有过百度、快手之类的机会。

+

这篇流水账我想聊聊我选择不去大厂的几个原因。

+

换工作是件严肃的事

大学刚毕业时,因为年少轻狂,那时候互联网环境也比较好,两年内跳了3次。因为有过频繁换工作的经历,到后来我就对换工作这件事没那么强的意愿了,再换工作时会认真权衡利弊,而且给自己定下了之后每份工作要做3年以上的目标。

+

到今年我已经工作8年多了,已经换过不下4份工作,换工作都是一件成本极高的事,不管是对个人还是对前东家或者新东家。尤其是对个人,换工作后要重新熟悉环境,重新结交人脉、重新认识上下游、重新了解新公司的技术栈…

+

刚换工作后的半年内很多事情对我来说都会是全新的。因为成本极高,所以换工作一定一定要慎重,今年五月份我们组有过一轮人员地震,有三个同学因为出国或者回老家发展,在深思熟虑后选择了离职,还有两个看到突然走了好几个人心里痒,仓促的面了外边的机会,匆匆忙忙跳了槽,前段时间聊起来那些匆忙跳槽的都有些后悔。

+

工作时长

我现在所在公司,平均工作时间是10:30-19:30,去掉中午2小时休息时间,工作时长为7小时。尽管我中午不午休,拿这个时间来运动、看书、刷题、写流水账,但这也是一大块属于我自己的时间,不管上午的事情有没有完成,午休这段时间都不会有人来找我。

+

去大厂后,晚上七点半下班基本属于奢望了,至少会再多出2个多小时的工作时长,相比现在的工作时长多出了30%,按照现在的市场行情,我不确定我通过跳槽可以再获得30%以上的涨幅,而且即便是获得了30%涨幅,按照工作时长来算,我也只是平薪跳槽,划不来。

+

我现在的团队也招了2个从字节跳槽进来的新同学,这边让他们很满意的一点是晚上9点后不可能有人突然拉他们进会。我告诉他们,不仅晚上9点后不会,晚上8点后就不会有人再找你了,除非线上炸了。

+

不知道是不是自己身体不行,我是真的卷不动,下午7点后没有任何想工作的动力,不知道大厂里每天干到晚上十点多的同行们是怎么坚持下来的。

+

个人能力

这不是谦虚,我在很多方面都不具备大厂喜欢的能力,比如应试能力。我觉得大厂面试和中国的应试制度有些相似,通过背一些工作中实际用不到的八股问题进行面试,通过多伦面试后进入公司,而不是看一些更实际的能力,我也能理解这种做法,因为找工作的人太多了,这是最高效筛选人才的一种方法。

+

我在做面试官的时候不喜欢问八股文,我会主要关注对方在工作之余做了些什么、写过什么软件。如果一个人不爱一件事,他就不可能把它做得真正优秀,要是他很热爱编程,就不可避免地会开发自己的项目。

+

我那个去字节的领导跟我说他们在新员工入职第三个月的时候要做工作汇报,入职这三个月内并不是像我现在公司这样给新员工充分的时间安心学习新东西,而是上来就介入工作,在汇报时不仅要讲自己对这三个月工作的理解,还要讲工作的成果和输出。这种做汇报展示成果的能力也是我欠缺的,我也不擅长公众演讲。

+

换工作就是换Leader

大厂因为发展快,人员变动也相对较快,我遇到好几个朋友和我说他的 Leader 比他小,另一个说他的 Leader 是95后之类的。

+

一个好的直属 Leader 对工作体验太重要了,在工作中伴随我们最久对我们的影响最大的人就是直属 Leader。我不太相信一个工作两三年的人有特别好的管理能力。对管理的认识虽然可以靠书本学习一些驭人之术来提升,但更多的是靠人生阅历,前者是 PUA,后者是真正的管理。但要做到后者是需要时间的,就像我们不可能找10个孕妇来一个月内生出一个宝宝一样。

+

我那个去了字节的领导第三天就要求去参加季度规划会,之前他的话语权很重,大家都会听他的,但他在字节的第三天,就在会上比被自己小的产品经理diss,问他是不是不了解背景,质疑他的能力。这也是我前边说过的温情,一个稍微成熟点的,有点社会阅历的成年人不会对一个刚入职3天的人讲出那种话。我的自尊心很重、心眼很小,承受不了职场PUA…

+

有人生阅历的 Leader 更加善而坚定,更加有管理上的温情,这样的领导能站在员工的角度理解员工,照顾员工的感受,真正为员工着想。

+

另外大厂里还会有各种「嫡系」文化,在有裁员指标时,通常裁的不是能力不行的,而是非嫡系的。在有晋升指标时最先安排的也是嫡系里的“自己人”。

+

鸡头与凤尾

我深知人外有人天外有天,我可以在小公司里混的如鱼得水,但放到大厂的人才荟聚的地方也许就是一颗再普通不过的螺丝钉。

+

我不想在一个默默无闻的岗位工作,这种地方不会让我感受到成就感,很容易失去工作的动力。而大公司就是这么一个地方,大公司会使得每个员工的贡献平均化,这是一个问题。我觉得,大公司最大的困扰就是无法准确测量每个员工的贡献。

+

做宽与做专

我也许更擅长把一件事情从0做到80分,但从80做到100甚至120分不是我擅长的,而这是在大厂里需要具备的精益求精的能力。我更喜欢做宽而不是做专,喜欢做个八面手而不是一颗螺丝钉,由此也可以看出小公司更适合我一些。

+
+

我不想离开现在的公司的最主要原因还是工作时长方面,虽然现阶段的我需要钱,去大公司确实可以用时间换钱,但综合考虑各种因素,对于这个年龄和家庭情况的我已经不再合适。留给那些还年轻、还有梦想的年轻人们去闯一闯吧,未来属于他们。

+ +
+ + + + + + + +
+ + + + + + +
+ +
+ + + +
+ + + + + + + + +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2023/nuclear-wastewater/index.html b/2023/nuclear-wastewater/index.html new file mode 100644 index 0000000000..4623b32ab9 --- /dev/null +++ b/2023/nuclear-wastewater/index.html @@ -0,0 +1,498 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 由日本排放核废水引发的思考 | 贾攀的流水账 + + + + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

贾攀的流水账

+ +
+

Panmax's Blog

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+ + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ 由日本排放核废水引发的思考 +

+ + +
+ + + + +
+ + +

首先声明,我不是精日,只是想就事论事反思一下最近看到的新闻。

+

日本放出要排放核废水的消息后,国内声讨的新闻铺天盖地,官方对这件事大肆渲染,带百姓们的节奏。官方宣传这件事有他自己的目的,其中之一是最近出现的很多人民内部矛盾已经不可协调,需要一些外部事件来转移和宣泄人民的情绪。

+

官方的做法无可厚非,毕竟是一种维稳的政治手段,但百姓们的各种行为就让人大跌眼镜了。超市的盐被大妈们抢购一空,女士和小学生边撕心裂肺的哭边骂街,年轻人上街打砸日本车。各个年龄段的人民都在用自己的方式来宣泄情绪。

+

对比较理性的人来讲,他们通常不问做错事是否有理由,而是先确定当前是否做错了事。

+

在日本排放核废水这件事上,人们不判断日本的做法到底符不符合规范,只要是日本做的事,我们的人民从来都不判断对错,直接默认就是对方的错,上来就是破口大骂。在其他很多事情上也是类似。

+

实际上日本排放核废水是经过国际原子能机构同意的,已经达到了安全标准。同时韩国和中国也都参与了监管。

+

美国之音的原文如下:

+
+

日本政府決定將核廢水排入海洋的作法已獲得國際機構的背書。自2021年開始評估約兩年後,聯合國核監督機構國際原子能機構(IAEA)上個月初批准了日本排放的計畫,得出的結論是將核廢水排入海洋,對人類和環境的輻射影響“可以忽略不計”。

+
+

另外,日本排放核废水受影响最大的是哪个国家?肯定是日本自己本国,只能说我们的国民咸吃萝卜淡操心,自己过的不好还要去操心别人。

+

我们根本不需要成为所有领域的专家,只要有一点点批判性思维,先问有没有再问为什么,就能够避免大部分常识性错误。

+

由此可以看出,我们国家国民的思想进步还有很长的一段路要走。

+ +
+ + + + + + + +
+ + + + + + +
+ +
+ + + +
+ + + + + + + + +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2023/obsidian-to-notion/index.html b/2023/obsidian-to-notion/index.html new file mode 100644 index 0000000000..a37f74d8c2 --- /dev/null +++ b/2023/obsidian-to-notion/index.html @@ -0,0 +1,510 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Obsidian换成Notion | 贾攀的流水账 + + + + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

贾攀的流水账

+ +
+

Panmax's Blog

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+ + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ Obsidian换成Notion +

+ + +
+ + + + +
+ + +

我之前经常给别人吹嘘Obsidian的强大,甚至在公司的内部分享中也给大家推荐过Obsidian。

+

我现在最常用的是Notion,很早前就放弃了Obsidian。

+

我放弃Obsidian的几个主要原因是有:

+
    +
  • 手机端做的很糟糕。Obsidian应该就是直接把电脑端那套东西搬到了手机端,没有做太多适配,导致手机端体验非常不好,插件多一点就会卡顿。
  • +
  • 数据同步不及时。我一开始使用的同步方案是基于iCloud,完全是佛系同步。后来改用AWS,但每次使用前后都要自己手动执行同步,不够便利,配置也较为复杂。
  • +
  • 无法开箱即用。需要安装很多插件后才能用起来,配色方案还要自己选了又选,每个配色和风格都有自己不满足的地方。
  • +
  • 复杂的插件系统。插件有两种安装方式,可以通过软件内直接安装,也可以通过源码安装。不同的插件安装后配置的方式也有区别,有的插件是改配置文件,有的可以直接在界面上配置,和前边遇到的问题一样,插件配置的多终端同步也就很不方便。也因为存在插件系统,我永远不知道自己没有用上什么功能,就会沉浸在每天浏览插件的焦虑中。
  • +
+

这些缺点在Notion上都得到了解决,Notion本身就是基于Web的,数据自始至终都在云端,不存在数据同步问题。我一开始吹嘘Obsidian时用到的一个理由是数据属于我自己,不信任任何第三方,第三方跑路后你的数据就再也找不回来了。现在想想真是既可笑又狂妄,况且Notion支持数据导出,导出后的数据就是纯文本Markdown格式,很容易迁移。

+

Notion在手机端做了大量优化,弱网环境下也可以使用离线数据,离线编辑,有网后自动进行同步和合并,界面也极其流畅。

+

Notion完全可以开箱即用,你可以使用它的Web端,也可以使用它的客户端,即使一个全新的用户也能很快上手用起来。

+

Notion的数据库系统和模板库也很强大,我用数据库系统记录Twitter上感兴趣的推文,还用它来维护我在Github上star的Repo。这两个用到数据库的功能都是使用别人开源的代码实现的。模板系统我用的不多,公司项目有专门的项目管理工具,我的待办事项使用Things。在数据库系统和模板系统方面,我在今后有精力了还需要深入学习一下。

+

有一说一,Obsidian由双向链接构成的知识图谱确实非常强大,猛一看也很唬人,但一般用户很难维护起自己的网络结构。Obsidian的插件生态也很好,有各种强大的插件可以使用,但还是有一定的上手难度。

+

我觉得Obsidian和Notion这两个阵营很像手机操作系统中的安卓和苹果。

+

我在大学使用安卓手机的时候,最喜欢折腾的事情就是刷机、装插件、改主题、改字体等等。后来改用iPhone后一开始也喜欢折腾越狱之类的,后来随着苹果的生态越来越好,加上自己精力有限也就不折腾了。在折腾iPhone越狱期间,我发现我每次改完一个地方,过段时间就会刷回原生操作系统,比如改了个字体、加了个图标,过段时间腻了就又会刷回去,到头来还是觉得自带的顺眼、舒服,自己整的花里胡哨的一点用都没有。

+

想想也是,苹果这么大的公司,由那么多专业的设计师设计出来的界面、选择出的字体,一定是符合绝大多数用户的最佳方案。我一个非专业人事居然会认为自己改的风格会超过专业的设计师。

+
+

专业人士和业余爱好者的一个差别在于,是否了解极限的存在。

+
+

Notion也是一样,Notion内部一定有非常专业的产品经理和设计团队去考虑如何更好的为用户服务,让用户有更好的使用体验。我们作为普通用户,享受他们的服务就可以了,业余的水平再高也是业余的,专业的事就交给专业的人去做就好了。

+

既然专业的人做专业的事,同理专业的事应该交给专业的工具,所以我不会用 Notion 作为任务管理工具,因为他在这个领域并不专业。

+

Notion 的产品完整度很高,每个功能都进过了精细的打磨,整个产品用起来有很扎实稳重的感觉,而 Obsidian 给我一种轻飘飘的感觉。

+

苹果和Notion虽然一直在听取市场上用户们的需求,但他们的每次改动都是经过深思熟虑的。让用户满意并不等于迎合用户的一切要求。用户不了解所有可能的选择,也经常弄错自己真正想要的东西。

+

做一个好产品就像做一个好中医一样,不能头痛医头,脚痛医脚。病人告诉你症状,你必须找出他生病的真正原因,然后针对病因进行治疗。

+ +
+ + + + + + + +
+ + + + + + +
+ +
+ + + +
+ + + + + + + + +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2023/positive-psychology/1.png b/2023/positive-psychology/1.png new file mode 100644 index 0000000000..6d19000b8c Binary files /dev/null and b/2023/positive-psychology/1.png differ diff --git a/2023/positive-psychology/index.html b/2023/positive-psychology/index.html new file mode 100644 index 0000000000..e20fc191a3 --- /dev/null +++ b/2023/positive-psychology/index.html @@ -0,0 +1,584 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 《哈佛幸福课》让我受益的点 | 贾攀的流水账 + + + + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

贾攀的流水账

+ +
+

Panmax's Blog

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+ + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ 《哈佛幸福课》让我受益的点 +

+ + +
+ + + + +
+ + +

今天终于把哈佛幸福课的23集都看完了,每集一个半小时,一开始一天看一集,真的有点长,时间很紧张,而且每看完一集我都会通过 AI 提取出文章中的关键点,把这些内容读一遍改一改病句还要再花一些时间,每天花在学习幸福课上的时间超过2小时,导致我一整天的时间安排都很紧凑,学习这门课的初衷是让自己更幸福,现在反而更不幸福了,这段时间也通过这种方式水了好几篇文章。

+

+

一天我突然意识到,我为什么要这么匆匆忙忙的着急看完,课程中 Tal 说过一句话:「比物质充裕更能带来幸福的是时间充裕」,让自己慢下来,过程中更投入一些,所以剩下的课程是每天看25分钟,一周学一节课。同时我把之前水的那些又臭又长的文章删掉了,在这篇文章中用小量的篇幅记录几个对我影响最大的观点。

+

下边进入正题:

+

要多问积极的问题

积极的问题会引导人正向地思考。

+
+

如果我们只问消极的问题,比如「为什么这么多人失败」,我们就没法看到潜藏在每个人心中的伟大,如果我们只问「我的人际关系该怎样改善」,我们就无法看见身边的人所拥有的宝贵财富和奇迹。

+
+

我们要多问积极的问题:

+
    +
  • 我的人际关系中有什么好的方面?
  • +
  • 我的同伴有哪些优点?
  • +
  • 我自己有哪些优点?
  • +
  • 什么对我最有意义?
  • +
  • 什么能使我愉快?
  • +
  • 我擅长什么?
  • +
+

问题会带来探索,探索的内容取决于我们所问的问题。

+

信念创造现实

我们如何理解现实才是最后所得的结果。

+

信念常常会成为自我实现的预言,但它是如何作用的? 有两个机制:

+
    +
  • 一是动力
  • +
  • 二是一致性或相合性的概念:我们的大脑不喜欢内部与外部存在差异,我们的精神喜欢两者一致相合。
      +
    • 有时你的欢乐是微笑的源泉,但有时你的微笑也可以成为欢乐的源泉
    • +
    +
  • +
+

将卓越和平庸划分开的有两样东西:

+
    +
  • 一是他们总在问问题,总想学习到更多,心怀谦逊对成长、幸福和自尊尤为重要。
  • +
  • 其次,他们相信自我,他们有自信,他们有自我效能通往成功和进步。
  • +
+

如何提升信念?

通过拉伸自我(走出舒适区),多去尝试,挑战自我,通过具象化使我们明白自己可以做到。

+

学会失败,从失败中学习

勇气并不是没有畏惧,而是有了畏惧还坚持向前

+

研究表明,失败真的是成功之母。最成功的人往往是失败得最多的。学会面对自己的失败,在失败中学习。这是学习的不二法门。

+

爱迪生比任何科学家获得专利都多的人,同样也是失败过最多次的人。真正来自于失败的痛苦远小于我们想象的。

+

不要因为害怕失败而放弃去尝试自己真正想做的事,

+

允许自己为“人” (permission to be human)

允许自己有缺点、犯错误,允许自己做人而不是神。在合理合法的范围内,对自己宽容一点。

+

当经历感情创伤时,你会看着它说“我只是普通人,我很难过,真希望事情不是这样,但我接受它,就像接受重力定律一样,因为重力定律是一种物理本质,就像感情创伤是一种人性本质,允许为人。”

+

好东西太多有时也不是好事

过犹不及,多则劣,少则精。

+

两首好歌同时放,就是噪音。

+

留下自己真正想要的,扔掉并没有很想要的,就算它很珍贵。比物质充裕更能带来幸福的是时间充裕。

+

少做点事,可以完成得更多。时间充裕的人,往往更容易获得幸福感。

+

宁缺毋滥,简化与效率是以曲线形式存在。

+

果断坚决,在适当的时候学会说”不”,弄清楚你究竟真正想做的东西,然后去做。

+

你现在和未来所经营的一段亲密关系比世界任何事都重要

甚至比问问题更重要,比考试更重要,比我们有多成功,多被人景仰更重要得多。

+

最能给人幸福感的东西,是良好的人际关系。亲密关系比很多事情都重要,它会给人带去有治愈能力的爱和温暖。

+

最成功恋情的四个特点:

+
    +
  1. 经营爱情需要付出努力
  2. +
  3. 我们需要被了解而不是被认可
  4. +
  5. 爱情中冲突不可避免
      +
    • 冲突要针对行为而不是针对人。
    • +
    • 避免针对人身,认可本人,尽量赞赏对方,仅对其行为或是其想法观念不苟同。
    • +
    • 尽量在私下才争吵。
    • +
    • 可以有争执,但要将其保持在认知行为上而非情感的,感情的,蔑视的层面。
    • +
    +
  6. +
  7. 积极认知,做优点感知者,多赞赏对方
  8. +
+

性在长久美好恋情中很重要。爱情,准确地说,性的至高点使爱具体化,使爱具体化。

+

每周锻炼4次,每次30分钟

基因决定的基准幸福水平,当我们不锻炼时,就像打了镇静剂。

+

运动是一项对现在和未来的投资。

+

我们必须和本性抗争,和本性抗争是很难的,提升我们幸福的水平是很难的,而同时要和本性抗争则是难以想象的困难。

+

锻炼的好处:

    +
  • 心理层面:增强自信自尊、减轻焦虑和压力、有助于临床精神疾病的辅助治疗、提高认知功能。
  • +
  • 身体层面:减轻或保持体重、减少慢性病、更强大的免疫系统、更美妙的性生活。
  • +
+

其他提高幸福感的灵丹妙药还有:

    +
  • 冥想、深呼吸、瑜伽
  • +
  • 良好的睡眠
  • +
  • 触摸、拥抱
      +
    • 触摸有助于伤口愈合,有助于身体健康,增强免疫系统,改善性生活。
    • +
    +
  • +
+

被了解而非被证明

从希望被认可变成希望被了解。一个人很多时候不是因为完美而被喜欢,是因为真实而被喜欢。因为真实而被喜欢,才是持久、轻松、可持续发展的。

+

并不是说要完全去除我们依赖别人的自尊,而是明白更重要的是被了解;去表达自己,而非给他人留下印象。这样人生会变得更轻松,更简单。

+

休息的重要性

那些成功人士,一是他们有习惯,二是他们有恢复,有休息

+

我们要转变对生活的理解:

+
    +
  • 从马拉松运动员 变为 短跑运动员;
  • +
  • 从不停地跑跑跑 变为 短跑,恢复,短跑,恢复。
  • +
+

心怀感激

应心怀感激,不要等到不幸发生时才意识到。

+

有很多好事值得我们感激,但我们都把它们习以为常,认为理所当然。例如我们把父母、朋友对我们的好视为理所当然。

+

把感激培养成一种生活习惯,对身体的好处,包括心率变异性,它能预测我们是否能长寿,预测我们是否健康。当我们感激时,副交感神经系统功能增强,使我们变平静,从而加强免疫系统,当感激成为我们的性格。还有很多好处,所以感激不只上一种心情,也是一种性格。

+

表达感激时我们感觉很好,对方也会感觉很好,他们的获益良多,于是你创造了一个双赢的局面,一个上升的螺旋。

+

怎样培养感激?

+
    +
  • 通过一次又一次的感激来培养
  • +
  • 每天睡前写下 5 件让自己满意的事
  • +
+

每天两次花一分钟时间留意周遭的一切。

+

花一分钟的时间,在上班的路上看看美丽的草地,青翠的树,美丽的雪。

+

晚上用一分钟去回忆,回想你度过的一天,写下让你心怀感激的事物。

+ +
+ + + + + + + +
+ + + + + + +
+ +
+ + + +
+ + + + + + + + +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2023/prejudice-Eat-Drink-Man-Woman/1.png b/2023/prejudice-Eat-Drink-Man-Woman/1.png new file mode 100644 index 0000000000..ea61aa156c Binary files /dev/null and b/2023/prejudice-Eat-Drink-Man-Woman/1.png differ diff --git a/2023/prejudice-Eat-Drink-Man-Woman/2.png b/2023/prejudice-Eat-Drink-Man-Woman/2.png new file mode 100644 index 0000000000..e4217728ad Binary files /dev/null and b/2023/prejudice-Eat-Drink-Man-Woman/2.png differ diff --git a/2023/prejudice-Eat-Drink-Man-Woman/3.png b/2023/prejudice-Eat-Drink-Man-Woman/3.png new file mode 100644 index 0000000000..45806e2cf7 Binary files /dev/null and b/2023/prejudice-Eat-Drink-Man-Woman/3.png differ diff --git a/2023/prejudice-Eat-Drink-Man-Woman/4.png b/2023/prejudice-Eat-Drink-Man-Woman/4.png new file mode 100644 index 0000000000..8cadab2da5 Binary files /dev/null and b/2023/prejudice-Eat-Drink-Man-Woman/4.png differ diff --git a/2023/prejudice-Eat-Drink-Man-Woman/index.html b/2023/prejudice-Eat-Drink-Man-Woman/index.html new file mode 100644 index 0000000000..5e3f49ded0 --- /dev/null +++ b/2023/prejudice-Eat-Drink-Man-Woman/index.html @@ -0,0 +1,526 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 借《饮食男女》聊偏见 | 贾攀的流水账 + + + + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

贾攀的流水账

+ +
+

Panmax's Blog

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+ + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ 借《饮食男女》聊偏见 +

+ + +
+ + + + +
+ + +
+

道德的偏见会让我们在面对事情的时候,根本没有办法启动理性思维,而一个不成熟的社会会有特别多的道德偏见。

+
+

饮食男女

最近一两年我很少完整地看电影或电视剧,没有时间也没有机会去电影院,更多的是在短视频平台上看一些由小美和小帅主演的电影剪辑,不过有一部电影我在去年完整刷了两遍,叫《饮食男女》。这部电影是我很喜欢的一个叫《文化有限》做书影剧解读的播客节目推荐的。

+

《饮食男女》由李安在1994年出品,剧情讲述每周末等待三位女儿回家吃饭的退休厨师,面临的家庭问题与两代冲突。借由彼此的生活与冲突,建构出不同年龄层、不同职业的价值观,描述90年代台北都会的两代关系。

+

我为什么提到这部电影呢?因为看完这部电影后,我心中一直挥之不去的一个词关键词是「偏见」。不是说这部电影带有什么偏见,而是我们这些看这部电影的人可能会有的先入为主所带来的道德偏见。

+

从三个女儿说起

男主朱爸爸有三个女儿,大女儿家珍、二女儿家倩,三女儿家宁,三个女儿都和朱爸爸一起住,因为朱爸爸是大厨,每周末一家人都会有个聚餐仪式。

+

我的偏见体现在这三个女儿身上。

+

大女儿家珍,是一名化学老师,母亲过世后因为她是最大的孩子,自然就担任起家中母亲的角色。被初恋男友抛弃后(后边是有反转的,为了不剧透就先这样简单说明),心情失落看不到希望,就信了耶稣,平时也不化妆,家里人给她介绍对象也很抗拒。给人的感觉是压抑、冷漠、古板,一位非常传统的大龄剩女。

+

每每看到家珍的形象,就会让我想起小学时候的语文老师。

+

+

二女儿家倩,事业有成,担任航空公司副处长,很会打扮,知性、大方也很开放,有体力非常好的男朋友(你们懂得),还做得一手好菜。因为职场优秀,赚了不少钱,自己独立买了房子,房子是期房,再过一段时间才能盖好。

+

该说不说,吴倩莲真漂亮。

+

+

三女儿家宁,还在读大学,典型的乖乖女,有些唯唯诺诺,空闲时会在一家汉堡店兼职打工。

+

插句题外话,我们组之前招了一个新人也叫家宁,一直觉得这个名字我在哪里见过,而且我每次叫他都感觉特别顺嘴,今天写这篇文章的时候才意识到家宁是《饮食男女》中三女儿的名字。

+

+

偏见

经过这样的背景介绍,如果是你,猜一下三个女儿中谁会第一个搬出去住?谁最不可能搬出去住?

+

按照正常推理,二女儿家倩一定是第一个搬出去的,因为他有自己的房子,也有男朋友,事实上家倩确实是第一个提出要搬出去住的,可结果是房地产公司跑路,男朋友劈腿。

+

最不可能搬出去的是谁?我在第一遍看时确信一定是大女儿家珍,其次是二女儿家宁。家珍被男朋友分手后就成了不婚主义,平时上下班坐公交时都是把耳机放最大声听教会的圣歌。三女儿乖乖女,而且还在读大学,短时间内也不会离开家。

+

真相

可实际上,最早搬出去住的是三女儿家宁,非常出人意料。虽然家宁乖巧,但她的内心非常勇敢。她喜欢上了闺蜜的男朋友,并大胆在一起(这里我站家宁,剧情并不狗血,是闺蜜太傻逼)。两个小年轻一来二去,没多久家宁就怀孕了,对方是个富二代,家宁在一次晚宴上宣告这个消息后就搬去了男朋友家住。

+

第二个搬出去的是大女儿家珍,她在偶遇体育老师后周明道后,就被对方的真诚、热情吸引了,两个人也开始交往,家珍内心积攒的压抑也开始释放,不再自己承受孤单、不再沉默,也在一次家宴上提出要搬出去和男朋友住。

+

电影的最后,留在朱爸爸身边吃饭的人是二女儿家倩,二女儿家倩其实是最关心父亲的,但只是说不出口。

+

看似最不能守住传统和孤独的人,缺坚持到最后。

+

+

再补充一个刚刚发生在我身上的偏见

《饮食男女》关于偏见的这个角度我之前就想写一写,但最终触发我写出来的是我刚刚遇到的下边这件事。

+

周末我去星巴克点了一杯咖啡,读了一个多小时书。

+

坐在我隔壁桌的女人,看起来比我年长几岁,桌子上空空的,坐着看手机,我下意识认为她是来这里占便宜吹空调的。过了很久,我快要离开的时候,她点了一杯咖啡。原来人家只是暂时不想喝,而且不在意别人的看法,也不是为了吹会空调进来坐会,我当时这么揣测人家感到非常羞愧。

+

顺便给自己洗一下,我去星巴克读书不是为了装逼,而是确实需要一个高效阅读的环境,就像当年JK罗琳要去五星级酒店才有灵感写哈利波特,咱没有人家的经济实力,只能去个星巴克。我经常去的那家星巴克人很少,适合在里边看书。

+

在日常生活中我们着有各种各样的偏见,北京人对外地人的偏见,正式工对外包的偏见,其他省对河南、东北的偏见等等。荀子曰「凡人之患,蔽于一曲而暗于大理」。意思是:人的认识,由于受到视野范围的局限或由于个人认识上的偏见,大都易于被局部的小道理所蒙蔽,而看不到、认不清全局的大道理。

+

是人就会有偏见,我们习惯评判身边的人谁好谁不好,喜欢比较。红楼梦作者曹雪芹也在提醒我们,一个生命的存在自然有他存在的价值。小说的五十回之前,晴雯一直没有什么特殊表现,大家会觉得她大概也就是个配角,可是在第五十二回里晴雯因为病补雀金裘变成了主角。

+

我们都是凡夫俗子,无法避免偏见,只能通过尽可能扩展自己的认知来理解和尊重别人的不同。就连大思想家歌德也承认:「我能确保正直,却不能保证没有偏见。」

+

最后

《饮食男女》中最最最大的偏见(用偏见可能不太合适,意外或者惊喜更恰当一些)来自朱爸爸和锦荣,我不想再剧透,大家自行观看。豆瓣的评分9.2,非常高的分数。

+

这部电影前几分钟做菜的镜头行云流水,分镜用的也很好,据说做菜的片断常常作为一些学校影视基础课的范例。这部电影的故事情节也不止我说的这么简单,是一部非常有深度的家庭亲情剧,推荐大家去看一看。一万个人心中有一万个哈姆雷特,不同的人有不同的解读,这部剧也有很多关于「性」的解读,毕竟电影名取自礼记中的:「饮食男女,人之大欲存焉」。

+

最后值得一提的是,这部电影在做菜时的用配乐特别好听,很欢快、有烟火味、听的时候心情也会好起来,大家也可以听一下:https://music.163.com/song?id=531878137

+ +
+ + + + + + + +
+ + + + + + +
+ +
+ + + +
+ + + + + + + + +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2023/record-gratitude/0.png b/2023/record-gratitude/0.png new file mode 100644 index 0000000000..100fd11679 Binary files /dev/null and b/2023/record-gratitude/0.png differ diff --git a/2023/record-gratitude/1.png b/2023/record-gratitude/1.png new file mode 100644 index 0000000000..adf9bdd83c Binary files /dev/null and b/2023/record-gratitude/1.png differ diff --git a/2023/record-gratitude/2.png b/2023/record-gratitude/2.png new file mode 100644 index 0000000000..e558978b96 Binary files /dev/null and b/2023/record-gratitude/2.png differ diff --git a/2023/record-gratitude/3.png b/2023/record-gratitude/3.png new file mode 100644 index 0000000000..9e31666998 Binary files /dev/null and b/2023/record-gratitude/3.png differ diff --git a/2023/record-gratitude/4.png b/2023/record-gratitude/4.png new file mode 100644 index 0000000000..ae8a4f3a74 Binary files /dev/null and b/2023/record-gratitude/4.png differ diff --git a/2023/record-gratitude/index.html b/2023/record-gratitude/index.html new file mode 100644 index 0000000000..0004049be1 --- /dev/null +++ b/2023/record-gratitude/index.html @@ -0,0 +1,514 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 记录感激 | 贾攀的流水账 + + + + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

贾攀的流水账

+ +
+

Panmax's Blog

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+ + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ 记录感激 +

+ + +
+ + + + +
+ + +
+

“对生活的感激程度其实就是生活的充实程度。当我们对生活麻木,对一切习以为常的时候,其实我们的生活就已经死亡了”

+
+

「哈佛幸福课」的第8节,讲得是感激的重要性。作者建议我们把感激培养成一种习惯,当我们感激时,副交感神经系统功能增强,使我们变平静,从而加强免疫系统。

+

在提到如何培养感激时,作者提了一个行动方式:每天睡前写下5件值得感激的事。

+

培养一个能力的最佳方式就是实践,通过一次又一次感激来培养感激,我从6月21日开始实施这个行动,不过我稍稍给自己降低了一点点要求,每天记录3条值得感激的事,我同时把这个行动项录入到 Things 中对我进行每日提醒。

+

+

我是用 Notion 来记录这些感激内容的,每个月新建一个新的页面,每天一个大标题。使用 Notion 我可以随时随地记录,比如在地铁上、公司里、家里,从第一天开始记录到今天已经将近4个月了。

+

+

每天的持续记录使我发现,原来我身边有那么多事值得感激,但我之前已经习以为常,认为这些都理所当然。在写感激过程中,感激最多的肯定是在背后支持我的家人,除此之外我还会感激之前没有意识到的事物,感激的对象也不止有实实在在的人,还有身边给我提供便利的物品。

+

比如下边这段:

+

+

最上边两条我感激了两位同事,一位帮我一起沟通绩效结果,另一位是我现在的 Leader,和我一起梳理一些重点项目;接下来我还感激了「哈罗单车」,那一天是个周五,天气很好,晚上下班早,我骑着单车从公司回家,一路上风景也很好;第二天8月5日是个周六,我早上开车回老家,路上狂风大作电闪雷鸣遇上了大暴雨和大雾,我和我的车经过4小时路程,它安全的把我带回了家;最下边那条,是我回家后带念出去玩,突然感觉她长大了,之前在游乐场玩的时候一定要我陪着,这一次她可以自己玩耍了。

+

再来随便看两天的:

+

+

这两天也是周末,我感激了华为安装师傅、感激了家具安装师傅、感激了木工师傅、感激了北京的交通、感激了念念、感激了游戏厅的抓娃娃机。

+

在我写这篇流水账翻看这些感激记录的过程中,又能回忆起当时的喜悦,一定程度上起到了日记的作用。每条记录用一句话描述,不会有很大的写作压力,刚开始确实不容易发现那么多值得感激的事,随着自己记录的越来越多,就会越擅长发现生活中值得自己感激的地方。

+

有时我还会感激自己,比如下边几条:

+

+

在记录感激的时候,我不会强迫自己,如果某一天心情实在糟糕,可以允许自己只写一两条,某一天过得充实的时候也写过六七条。

+

通过记录这几个月的感激,我能很明显感受到自己情绪好了很多,不再那么偏激,能够从积极的方面思考问题了,注意力会放在积极正面的事情上,和其他人打交道时会思考对方有什么优点值得我学习,有时我还会把之前遇到后会非常生气的事换个思路去看。

+

我们应该心怀感激,而不是等到不幸发生时才意识到之前的自己错过了多么美好的时光。

+

世界上有很多美好的事物,但我们很快就会适应且不再察觉它们。每天两次花一分钟时间留意周遭的一切,花一分钟的时间,在上班的路上看看美丽的草地、青翠的树、美丽的雪。晚上用一分钟去回忆,回想你度过的一天,写下让你心怀感激的事物。

+ +
+ + + + + + + +
+ + + + + + +
+ +
+ + + +
+ + + + + + + + +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2023/rich-three-generations/1.png b/2023/rich-three-generations/1.png new file mode 100644 index 0000000000..846c5f684d Binary files /dev/null and b/2023/rich-three-generations/1.png differ diff --git a/2023/rich-three-generations/index.html b/2023/rich-three-generations/index.html new file mode 100644 index 0000000000..0a5feb0eb2 --- /dev/null +++ b/2023/rich-three-generations/index.html @@ -0,0 +1,508 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 富过三代才懂吃穿 | 贾攀的流水账 + + + + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

贾攀的流水账

+ +
+

Panmax's Blog

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+ + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ 富过三代才懂吃穿 +

+ + +
+ + + + +
+ + +

读过《红楼梦》的朋友一定对其中的一道菜印象深刻:「茄鲞」。王熙凤讲述这道菜的做法是:把才下来的茄子把皮削了,只要净肉,切成碎钉子,用鸡油炸了,再用鸡脯子肉并香菌、新笋、蘑菇,五香腐干、各色干果子,俱切成钉子,用鸡汤煨了,将香油一收,外加糟油一拌,盛在瓷罐子里封严,要吃时拿出来,用炒的鸡瓜一拌就是。

+

“鸡瓜子”是什么?就是用手撕出来的鸡小腿部分的腱子肉。因为常常活动,所以那块肉的弹性最好。富贵之家能把一个食之无味的茄子,经过这么复杂的环节来制作,做的这么精细。

+

还有一次宝玉被他的爸爸暴打后,王夫人问他想吃什么,宝玉回说:“也倒不想什么吃,倒是那一回做的那小荷叶儿莲蓬儿的汤还好”。这个莲蓬汤倒不是什么山珍海味,只是做起来很麻烦,当年元妃省亲时做过一次。因为是给皇帝准备吃的,非同小可,既不能过于奢华,又要十分讲究。莲蓬是用银模子刻出来的,库房的人把模子找出来后,薛姨妈看到后说:“你们府上都想绝了,吃碗汤还有这些样子。若不说出来,我见这个也不认得这是作什么用的”。薛姨妈也是大户人家,就连她都没见过这么精细的模子,可想而知贾家在饮食上有多么讲究了。

+

相较于富贵过好几代的家族,暴发户是不知道怎么吃的,以为大鱼大肉就叫吃了。富贵人家吃的其实并不是山珍海味,他们讲究的是做工的细腻,到最后就变成了文化。

+

除了在吃上,贾家在穿戴上也是非常讲究,举几个例子:贾母的软烟罗、平儿的虾须镯、宝玉的雀金裘、湘云的凫魇裘……。

+

通过上边这些内容,我想引出一个更普世的观点:没有钱的人永远无法想象有钱人过的是什么样的生活,平时会使用什么样的东西,就像段子中皇帝的金锄头一样。

+
+

下边用几个我使用过的稍微好一点的物品举例,这些物品价格确实会稍贵一点,但也不是什么奢侈品,限于我目前的人生阅历也只能用这些来说明了。

+

戴森吹风机

一个戴森吹风机3000多,普通家庭是绝对不会买的。我们家几年前一直在用其他品牌的吹风机,也没感觉有什么问题,后来我们帮一个保险销售介绍客户,完成了很多任务,作为奖励她送了我们一台戴森吹风机,自从用上以后就再也用不惯其他吹风机了。

+

前一阵子搬家,我把戴森拿到了新家使用,因为我爸妈还要在之前的房子住,那边需要一个新吹风机。我看到这两年一个国产的品牌「徕芬」吹风机很火,外形也和戴森很像,就买了一个给他们用。前两天我回家用了一次徕芬,实话实说,如果我之前没有用过戴森,我一定觉得这个吹风机非常好用,但用过了更好的对比之下才知道还有很大差距。

+

室内隐形门锁

+

传统的门锁都会外露一个弹簧的探头,用来在关门时将门卡住。探头上下两个角很尖,不注意时会磕碰到人,家里有小孩的情况下,如果小孩正好跟门锁差不多高,在跑来跑去时会更危险。日常关门时,因为探头要和门框上的凹槽摩擦,还会有很大的噪音。因为探头存在阻力,在关这种门时,通常是用手把门把手转到下边,再去将门关严,或者需要很用力地去关。

+

装修新房时,才知道现在已经有了无形的锁具,门在开启状态时探头是不会外漏的,只有将门关闭后探头才会弹出,避免了磕碰还更静音了。想把门关严时也不用捉着把手去关了,直接推门就可以。我没有研究它的原理,猜测是用了磁铁之类的。

+

花洒 && 零冷水

另一个和装修有关的是新家里的淋浴设备和零冷水燃气热水器。在我没有用新的花洒之前,没觉得之前用过的花洒有什么问题,用过之后觉得之前花洒水量太小了。前两天再去用之前的就觉得身上的沫子半天才能冲干净,新的淋浴一瞬间就冲完了。

+

还有支持恒温的零冷水燃气热水器,如果没有接触过,我真的不知道洗澡水居然可以不需要等待,每次打开直接出热水,温度也是之前设置好的恒定温度,完全不用担心忽冷忽热的问题。

+

自助餐

上个周末和家人去吃了一次比格自助,79一位。如果家庭条件一般,自助吃的比较少的话,就会觉得比格很不错了,当然比格在这个价位里也确实不错。但如果吃过更好的,就会知道比格的食材还差很多。

+

其实我也没什么资格评判比格,因为我吃的比较多的也是比格或者比格这个价位的自助,只是在公司团建的时候有幸吃过其他稍微高档一些的,比如第六季、水木锦堂之类的。但次数有限,那些更高级的,上千块的自助还没有体验过。

+
+

我现在只开过 20w 以内的车,已经觉得很好了。50w 以上的车还没有开过,更别提百万级别的豪车了。我相信我现在一定无法想象出开豪车的体验和惊喜。

+

如果我以后有机会能开上,再来更新使用体验😂

+ +
+ + + + + + + +
+ + + + + + +
+ +
+ + + +
+ + + + + + + + +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2023/ruined-half-a-day/index.html b/2023/ruined-half-a-day/index.html new file mode 100644 index 0000000000..579924fea5 --- /dev/null +++ b/2023/ruined-half-a-day/index.html @@ -0,0 +1,496 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 因为没控制好情绪毁了半天休息时间 | 贾攀的流水账 + + + + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

贾攀的流水账

+ +
+

Panmax's Blog

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+ + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ 因为没控制好情绪毁了半天休息时间 +

+ + +
+ + + + +
+ + +

今天是8月30日,星期三,娃的幼儿园过两天开学,需要开个家长会,时间是下午两点半。考虑到上班来回的路程,加上最近也想在工作日放松一天了,索性就请了一整天的假。

+

昨天运维在切换一台生产环境网关机的时候,我们有一个调用第三方的业务产生了大量报错,出现了一段时间不可用。可能是我们的调用方式有问题,原因还没有查清楚。

+

那个业务重要程度不高,而且是最近刚上线,用来提升用户体验的一个功能。但因为影响的请求数超过了天级的千分之一,按照惯例需要进行复盘。

+

因为影响的业务不太重要,而且原因还没查清楚,需要查一下根因,我就没有准备复盘工作。今天早上 SRE 直接给我定了会议室和时间,要求我复盘,看到我请假了就问我能否让另一个同事参加。我的防御心理一下子就开启了,在我的潜意识中认为复盘是我做错了什么事情。另外一点是我不想让其他人的时间被这种偶然复杂事件耽误,况且那段代码的底层调用逻辑也不是他写的,写这段代码的人已经离职了。因为这也算做一个故障,让同事参与可能会让他认为需要他来背故障责任。

+

当时我就一下子就暴怒了,在群里用比较激动的言辞指责SRE。结果就是整个上午我都在和SRE那边掰扯这件事,心情非常糟糕,而且那个群里还有我组内的其他同事,他们也看到了我情绪激动的言辞。

+

等我情绪宣泄完,心情平复后我就又开始后悔了,事后还跟SRE那边委婉的道了歉。

+

我当时的处理方式也有问题,SRE本来的计划是我如果不方便参加,他就和我另一个同事排查一下问题,把复盘做了,我因为在气头上,不让我同事配合,跟SRE说后边我查清楚了再和他们复盘。我做错了两点,首先我不应该认为这个事情会耽误其他同事工作,这也许是一个锻炼他排查问题的好机会,他可能也很乐意排查。其次我不应该把这件事揽到自己头上,我今天请假,本该今天做的事情挪到了周四周五,排查这个问题可能就要占用我一天的时间,时间根本不够用。

+

我当时正确的做法应该跟SRE和我的同事说先尝试定位下问题,能定位到今天就进行复盘,定位不到就等我回去了再一起看下。这样既可以留下今天先不复盘的 buffer,也可以让同事没那么大压力。

+

以后千万不能再在情绪激动的情况下发消息回消息了😭

+

休假期间也尽量不回消息、不读消息。

+

时刻牢记宝钗的金玉良言「事不关己莫开口,一问摇头三不知」。

+ +
+ + + + + + + +
+ + + + + + +
+ +
+ + + +
+ + + + + + + + +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2023/skipping-rope/1.mp4 b/2023/skipping-rope/1.mp4 new file mode 100644 index 0000000000..65d9d994bb Binary files /dev/null and b/2023/skipping-rope/1.mp4 differ diff --git a/2023/skipping-rope/1.png b/2023/skipping-rope/1.png new file mode 100644 index 0000000000..27df0ec1fe Binary files /dev/null and b/2023/skipping-rope/1.png differ diff --git a/2023/skipping-rope/2.jpeg b/2023/skipping-rope/2.jpeg new file mode 100644 index 0000000000..54f5a33c08 Binary files /dev/null and b/2023/skipping-rope/2.jpeg differ diff --git a/2023/skipping-rope/3.jpeg b/2023/skipping-rope/3.jpeg new file mode 100644 index 0000000000..7db3400e99 Binary files /dev/null and b/2023/skipping-rope/3.jpeg differ diff --git a/2023/skipping-rope/4.jpeg b/2023/skipping-rope/4.jpeg new file mode 100644 index 0000000000..acc5a9250d Binary files /dev/null and b/2023/skipping-rope/4.jpeg differ diff --git a/2023/skipping-rope/5.jpeg b/2023/skipping-rope/5.jpeg new file mode 100644 index 0000000000..fbd3b0506b Binary files /dev/null and b/2023/skipping-rope/5.jpeg differ diff --git a/2023/skipping-rope/6.jpeg b/2023/skipping-rope/6.jpeg new file mode 100644 index 0000000000..ebf583c106 Binary files /dev/null and b/2023/skipping-rope/6.jpeg differ diff --git a/2023/skipping-rope/index.html b/2023/skipping-rope/index.html new file mode 100644 index 0000000000..893acc295b --- /dev/null +++ b/2023/skipping-rope/index.html @@ -0,0 +1,521 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 分享一个我已经持续半年的运动 | 贾攀的流水账 + + + + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

贾攀的流水账

+ +
+

Panmax's Blog

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+ + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ 分享一个我已经持续半年的运动 +

+ + +
+ + + + +
+ + +

今年2月中旬,我开始尝试一个新运动:跳绳。

+

到今天已经持续半年多,最开始使用无绳跳绳跳2000个,逐渐到5000,一个多月后改成有绳跳2000,逐渐到5000。

+

虽然体重没有太大变化,但精神状态好多了,每次跳完后都是暴汗,多巴胺大量分泌,跳绳过程中也会冒出一些灵感,有工作上的也有生活上的。

+

最近换成了一个重量比较大的绳(半斤重),根据当天精神状态跳2500-3000个,分成250个一组,每组中间休息20秒。每次运动时间大概花25分钟,加上运动后换洗衣服总耗时约35分钟,每周平均运动3-4次。

+

用大重量绳的好处是可以顺便锻炼手臂,同时还能节约时间,追求质量不再追求数量,跳的太多对膝盖也有负担。因为绳子重量较大,即便数量少了一些出汗效果一点也不差。在换成用大重量跳绳的过程中我还发现对耐力上限的阈值可以不断调教,之前用大重量的绳最多跳500个胳膊就抡不动了,而且在中午吃饭时会手抖,现在可以持续3000个。

+

我在公司放了一件运动T恤、一条运动短裤,还有一双运动鞋。每天中午12点多换上运动鞋拿上运动衣和跳绳到公司办公楼28层——这一层是空的,在洗手间换好衣服,带上耳机打开YouTube随机播一集圆桌派,边听边跳。跳完后再去洗手间把汗擦干,换回便装,运动衣用清水rua一把晾回工位,第二天再用时也就干了。

+

回工位后休息一下就可以下楼吃饭了,这时候吃饭的人已经不多,可以找个地方悠闲的吃个饭。我通常去一个称重计价的自助餐厅,中午一点半后6折,不到20块钱就能吃的非常好,公司餐补30元,剩下10块还能用来喝杯咖啡😂。如果想在1点半后来这家吃,我回工位后会看一会书,或者写篇流水账,一点半前进行5到10分钟冥想,1点半准时下楼吃饭。

+

下边推荐几个跳绳过程中使用的装备:

跳绳

跳绳一共买了6、7条,最推荐的是超飞跃家的。我买了两条超飞跃,一条6mm 的,一条8mm,8mm 的那条有半斤重。

+

我将6mm 的打了个结,可以稍微提高一些摇绳时的惯性。

+

+

下边这条是8mm 的:

+

+

6mm 的价格是69.9,8mm 的价格是130,从京东购入。

+

强烈建议再从拼多多买羽毛球拍防汗带把手柄缠上,这样手感非常好。

+

运动监控

我用 AppleWatch 采集心率,通过 YaoYao 这个跳绳专用 App 在运动期间查看心率并进行间歇训练计数。

+

我会将心率心率控制在135左右,中间会有几十秒努力将心率提升至极限150+,有效训练心肺功能。

+

+

+

AirPods Pro2 耳机

因为是在室内,摇绳的声音就比较大,再加上升级成半斤重的跳绳后,甩绳子的声音整个楼层都能听到,带上 AirPods Pro2 开启降噪整个空间都是我的。

+

每次跳绳都会听一集圆桌派,既可以在运动过程中分散坚持不住时的注意力,又可以长见识,听听大咖们思考问题的方式。YouTube 已经完全学会了我的喜好,每次中午只要一打开它,列表中第一个一定是一集我没有听过的圆桌派,而且不是按顺序播放的甚至推荐的不是同一季。

+

我跳绳穿的是一双国产品牌的运动鞋,叫「必迈」,具体型号是远征者4.0Plus。因为非常舒服我买了两双,一双放公司专门用来在中午跳绳用,另一双放在家运动或者散步时穿。第一双是半年前买的价格是330,第二双是前两周买的,价格降到了295,都是在拼多多买的。

+

我没有穿过非常贵的运动鞋,但我可以说这双鞋在300这个价位内绝对是无敌的存在,鞋子很轻、鞋底较厚且非常有弹性。

+

+

跳绳是项对场地要求很低的运动,只需要2平米的空间就可以开始,枯燥的时候还可以加上各种花式动作,比如下边这个视频就非常酷炫,一看就会一学就废。

+ + +

看了下今天的数据,我已经累计跳了48万次,继续加油。

+

+ +
+ + + + + + + +
+ + + + + + +
+ +
+ + + +
+ + + + + + + + +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2023/success-failure/index.html b/2023/success-failure/index.html new file mode 100644 index 0000000000..0d36af4a7a --- /dev/null +++ b/2023/success-failure/index.html @@ -0,0 +1,509 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 讨论成功与失败 | 贾攀的流水账 + + + + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

贾攀的流水账

+ +
+

Panmax's Blog

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+ + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ 讨论成功与失败 +

+ + +
+ + + + +
+ + +

关于成功和失败,有我见过两派说法,一派认为成功的经历很重要,另一派认为失败的经历很重要。

+

认为成功的经历很重要的人们这样说:对那些仅仅满足不失败的人来讲,失败的教训可以让他们避免犯同样的错误;但是对于想成功的人而言,失败的教训远没有成功的经验重要。一个经常失败的人会习惯性失败,相反,成功才是成功之母。 虽然人很难做一件事情就成功一件,但总该尽量避免失败,这样才能少受挫折。

+

认为失败的经历很重要的人们这样说:学会失败,从失败中学习,要想进步就必须学会失败。把失败看成成长的工具,历史上最成功的艺术家、科学家,也是失败最多的,经历过最多的失败。

+
+

我们不要有二极管思维,上述两种观点乍一看似乎相互矛盾,但实际上它们各有各的道理。它们从不同的角度阐述了成功和失败的经验。

+

认为成功更重要,是在强调成功的经验比失败的经验更重要,只有不断积小胜才能获大胜。成功会让一个人越来越自信,而失败会让一个人越来越对失败免疫(有点类似于习得性无助)。

+

认为失败更重要,强调的是不要把失败看作是过不去的坎,因为失败无法避免,真正来自失败的痛苦远小于我们的想象。面对失败是磨练心性和个人成长的绝佳机会。

+

一个强调成功的经验很宝贵,一个强调失败的痛苦并不可怕。

+

许多讲述失败重要性的故事都会以爱迪生为例,说他在找到最适合电灯泡的灯丝前失败了5000多次实验。但他们没有说的是,爱迪生在1879年发明出灯泡前已经有过多次成功发明经历,例如1868年的投票计数器、1870年的印刷机、1877年的留声机等等。正是因为有这么多次成功经验才给了爱迪生很大的信心。我相信,爱迪生在进行电灯泡实验时,更多的经验一定来自之前成功发明的经历。

+

成本和机会这两个因素也极其重要,穷人家的孩子失败一次就再没有重试的机会了,而富人家的孩子失败了还可以重头再来,他们有可以试错的资本。

+

在初始成功和失败率相同的情况下,富人家的孩子有更多的重试机会。随着重试次数增多,成功次数也随之增加,成功经验也会积累得更多,这样就越不容易失败。很多企业家的儿子就是很好的例子,比如王思聪。

+

作为普通人,我们既无法避免失败,也没有太多试错成本,我们能做些什么?

+
    +
  • 积极的重建,把失败看成成长的工具,这可以更好的了解自己
  • +
  • 要认命,及时止损。不要总想着挽回损失,这样损失就会被限制在局部
  • +
  • 对于已经无法挽回的错误,不是懊恨不已,而是承认往日错误已是人力无法企及的范畴,既不能从头来过,也不能改变结果
  • +
  • 行动前多推演,行动后多复盘
  • +
  • 同时具备两种互相冲突的信念
      +
    • 一方面,要像初生牛犊一样,对自己的能力信心万丈
    • +
    • 另一方面,你又要像历经沧桑的老人一样,对自己的能力抱着怀疑态度
    • +
    +
  • +
+

成功的经验稀缺不可多得,失败的经历也不可或缺。

+ +
+ + + + + + + +
+ + + + + + +
+ +
+ + + +
+ + + + + + + + +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2023/temptation-is-hidden/index.html b/2023/temptation-is-hidden/index.html new file mode 100644 index 0000000000..a6b73a3217 --- /dev/null +++ b/2023/temptation-is-hidden/index.html @@ -0,0 +1,494 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 诱惑本身是隐藏 | 贾攀的流水账 + + + + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

贾攀的流水账

+ +
+

Panmax's Blog

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+ + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ 诱惑本身是隐藏 +

+ + +
+ + + + +
+ + +

问各位男同胞一个问题,你们觉得裸女更有吸引力,还是穿着半遮半掩的衣服时更有吸引力?

+

我会选择后者,把一切看穿就没有那么大诱惑了。对方在半遮半掩的状态下会给你无尽的关于性想象,各位看过小电影的同学也一定会有同感吧。

+

红楼梦中,晴雯生病,一个新入行的太医来给她瞧病,因为古代讲究男女授受不亲,需要把幔帐放下来,只把手漏出去给大夫把脉。原文是这样写的:「有三四个老嬷嬷放下暖阁上的大红绣幔,晴雯从幔帐中单伸出手去。那太医见这只手上有两根指甲,足有二三寸长,尚有金凤花染的通红的痕迹」。年轻太医哪见过这场面,一个漂亮的手上留着长长的指甲,还用金凤花涂过颜色,最主要的是还看不到本人长什么样,这一下子把太医迷的五迷三道的,整个人脸红心跳呆在那里,后来另一个老嬷嬷拿手帕把手盖住太医才平缓下来继续治疗。

+

如果晴雯真的直接站在他的面前,大概也不会有这么大诱惑。

+

和这个场景类似的是胡君荣给尤二姐看病那一会,尤二姐本来是怀孕了,医生先通过把脉,看到这么美的女孩的手已经心神不定了,无法断定病因,后来要求再看一看脸。在古代男性根本没有机会直接看到贵族女性,都是隔着帘子,这会带来极大的诱惑。就是因为隔着一层帘子,性的幻想就会特别严重。医生在这种情况下开除的药,结果就可想而知了。

+

东方自古以来都喜欢半遮半掩、半掩半开、犹抱琵琶半遮面的朦胧美。后边尤三姐调戏贾珍和贾琏时这样写到:「尤三姐松松挽着头发,大红袄子半掩半开」。了不起就在「半掩半开」上,如果全开了就没什么意思了,那就不是红楼梦,而是金瓶梅的写法了。

+

在穿衣方面,东方也喜欢把鲜艳颜色的衣服穿在里边,比如红内裤。西方常常把艳的东西放在外面,东方常常把艳的东西放在里面。用很典雅的话来讲叫做含蓄,用比较不典雅的话叫做闷骚。

+

最后再说一点关于隐藏的阴暗面。我们这个国家就因为隐藏太多,才让人们有非常大的偷窥欲,社会应该多给大家提供面对事物真相的机会。

+

古代讲究儿媳妇要回避公公,也许就是这么严防死守、过度回避,才产生了秦可卿淫丧天香楼的悲剧。回避太多,隐藏太多,反而会产生更多诱惑。

+ +
+ + + + + + + +
+ + + + + + +
+ +
+ + + +
+ + + + + + + + +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2023/the-importance-of-third-parties/index.html b/2023/the-importance-of-third-parties/index.html new file mode 100644 index 0000000000..425d69b3c7 --- /dev/null +++ b/2023/the-importance-of-third-parties/index.html @@ -0,0 +1,504 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 第三者的重要性 | 贾攀的流水账 + + + + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

贾攀的流水账

+ +
+

Panmax's Blog

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+ + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ 第三者的重要性 +

+ + +
+ + + + +
+ + +

看到第三者这个词是不是想歪了?我这里指的是一个事件中的第三方参与者。

+

我举个例子,你媳妇和你妈吵架,你在他们中间就属于第三者,你起到的作用举足轻重,处理好能家和万事兴,处理不好能鸡飞狗跳。我不知道其他人,我是非常不擅长处理这种事的,我经历的鸡飞狗跳太多了🥲。

+

我不是一个合格的第三者,但我非常敬佩能把事处理的非常妥当的那些第三者。在我看来合格的第三者应该像袭人那样,大事化小、小事化无。

+

有一回宝玉去薛姨妈家,伺候宝玉的李奶妈拦着不让他多吃酒,回去后李奶妈还把宝玉给晴雯留的豆腐皮包子吃了,宝玉要喝茶时小丫头们说李奶妈把他泡好的枫露茶喝了,宝玉气的摔了茶碗,嚷嚷着要把李奶妈赶出去。不一会贾母房里的小丫头就来问发生了什么事,袭人站出来说是她不小心喝水时打碎了杯子,她不想大晚上的让贾母担心,没有提任何李奶妈的事。

+

后边还有一回李奶妈把宝玉留给袭人的酥酪吃了,袭人外出回来后,宝玉让人去把酥酪取来,丫鬟们回李奶妈吃了,宝玉正要发火,袭人说“原来是留的这个,多谢费心。前儿我吃的时候好吃,吃过了好肚子疼,足的吐了才好。他吃了倒好,搁在这里白糟蹋了。”就这样又化解了这一次危机。如果换成其他爱作妖的丫鬟,比如晴雯这种爆炭脾气的,非得把事闹大了不可(一会我说个关于晴雯的事)。

+

宝玉也有很多大事化小、小事化无的高光时刻,说一个例子,一次藕官在大观园里烧纸钱祭奠已经死去的、她之前的戏搭子菂官,被一个老婆子撞见,老婆子抓着藕官要去找太太。宝玉经过遇到此事,按常理,宝玉也可能会责备烧纸钱的人,但他看到藕官满面泪痕,他心想这个小戏子一定有她的心事,背后有无法言说的委屈,宝玉先把事情的真实原因放在后边,先自己站出来说是他让藕官烧的,就这样救下了藕官。

+
+

只要将心比心,你就会对一个人的伤心有所关怀,它既不是法律,也不是道德,而是在法律跟道德之外人内心最柔软的那个部分。

+
+

公司里,小领导在不同场合对自己的下属进行评价也能看出是否是一个合格的第三者。大老板们不了解一线员工的状态,需要小领导来反馈一下,如果小领导总抓着其他人的缺点去评判,不能避重就轻、善于发现别人的优点是万万不可的。你随随便便几句话,可能带给对方的就是天差地别的结果。

+

我们不要做老好人,也不要做煽风点火、唯恐天下不乱的人。如果能预测到一件小事在往恶性的方面发展,而你又是参与其中的一个人,不妨尝试化解一下。

+

不光宝玉身边的袭人,凤姐的特别助理平儿在这方面做的也非常出色,最著名的一回莫过于「俏平儿情掩虾须镯」,这一回中平儿和晴雯的处理方式形成了极大的反差。平儿在大雪天跟宝玉、湘云一起在野外烧烤,吃鹿肉时把镯子摘下放在了一旁,吃完后发现不见了,经过排查发现是宝玉屋里小丫鬟坠儿偷拿了。

+

平儿考虑到宝玉对丫鬟们很好,原文是这样写的:“我赶忙接了镯子,想了一想:宝玉是偏在你们身上留心用意、争胜要强的。”,「留心用意」,是说宝玉没有用管理丫头的方法管理她们,他相信人性有一种更高的自觉;「争强要胜」是说他希望自己房里的丫头,没有严格的法的约束也能有人性的自觉。如果平儿把这件事爆出来宝玉肯定会被人议论过于放纵自己的丫鬟,而那个丫鬟也会被赶出去,在那个社会如果一个丫鬟被一个大户人家赶出去基本就等于判了死刑(后边晴雯就是这么死的),所以平儿想把这件事掩盖下来,以后让大家提防着点坠儿就好了。她和麝月商议后打算不把这件事告诉正在生病的爆炭脾气的晴雯,谁成想他们的对话被宝玉听到了,宝玉还是转述给了晴雯,晴雯气的对那个小丫鬟又打又骂,假借宝玉之名把坠儿赶了出去。

+
+

思考:坠儿出了这种事,等于是宝玉对人性实验的一次失败,可是最大的为难在于,十次有九次失败,我们还要不要为那一次留下余地。

+
+

同样的事件,用不同的方式表达,起到的效果也大不一样,比如一个总打败仗的将军,我们可以说他屡战屡败,也可以说他屡败屡战,两个读起来相近的句子,含义却差了十万八千里,这又涉及到了语言的艺术。

+

最后讲个有趣的典故吧,大家都听过一个顺口溜「二十三,糖瓜粘」。”糖瓜”是一种用黄米和麦芽熬制成的粘性很大的糖,为什么腊月二十三要做糖瓜呢,因为传说这一天灶王爷要去天上,像玉帝报告每户人家这一年做了好事还是坏事,所以百姓们就把糖黏在炉口来贿赂灶王爷,意思是让灶王爷嘴巴甜一点,上天以后讲这一家人的好话。

+

民间的有趣就在于,他们会觉得没有什么东西是躲不过去的,就看你用什么方法。这个跟人的生命力有关。所谓生命力,就是灾难不再是灾难,危机不再是危机。我们在生活中,有时候遇到一点小事就觉得过不去了,其实就是生命力弱了。

+ +
+ + + + + + + +
+ + + + + + +
+ +
+ + + +
+ + + + + + + + +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2023/thoughts-about-morning-meetings/index.html b/2023/thoughts-about-morning-meetings/index.html new file mode 100644 index 0000000000..59bcad9d91 --- /dev/null +++ b/2023/thoughts-about-morning-meetings/index.html @@ -0,0 +1,499 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 关于早会的思考 | 贾攀的流水账 + + + + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

贾攀的流水账

+ +
+

Panmax's Blog

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+ + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ 关于早会的思考 +

+ + +
+ + + + +
+ + +

今天周一,照例我们下午开了全组的周会,我思考了很久决定取消每日晨会,下边是我准备的发言稿。

+
+

本月最后一天是我入职 TT 的三周年,我依然向往我刚入职 TT 后近一年左右的时光,那个时候 TT 还有一点点外企文化,不具体展开讲了,用几个词形容就是:包容、信任、自驱、敢于试错。我那时也非常庆幸自己入职一家好公司,当时的 TT 被称为互联网最后一片净土,也确实对得起小而美的称号。

+

我一年半前主动要求过一次转岗,从直播转到推荐,刚来推荐组的时候,每次晨会听到大家工作那么饱和我都很焦虑,所以我也能体会大家现在的感受。

+

上周有一天kq因为白天开了一整天的会,但他手里的一个技术驱动项目进度还差一些,晚上下班后我问他走不走,他说得加班把技术驱动搞完,不然第二天早会没得说。我知道他是在开玩笑,不过那句「不然第二天早会没得说」这句话我确实也在心中说过好多次。

+

我不希望大家每天为了考虑早会上要说什么而有压力,甚至出现为了说点什么而被迫找点琐碎而无意义的事情做,也不希望大家靠堆砌很多工作量来证明自己的能力和重要性。我希望大家的工作可以更专注、聚焦、深入、认真、细致一些,不要东一榔头西一棒槌。我特别喜欢一句话:不要用战术上的勤奋,来掩盖战略的懒惰。

+

所以我打算尝试取消早会,取消也许是长期的,也许是暂时的,还要看取消后的效果和公司的要求。对于我来说开晨会是正确地做事,现在取消周会是做正确的事(大家可以想想这两句话的区别),结果是否正确现在不得而知。

+

不开晨会建立在大家自驱的基础上,也建立在我对大家充分了解和信任的基础上,我一直相信信任是促使人们进步的最大动力,因为信任能够让人们表现出自己最好的一面。

+

我们组内的方向比较多,每个人的工作内容不尽相同,每日同步给所有人的意义不是很大,靠每周周会来做一次相互了解和同步就够了。

+

我们现在早会最大的益处其实是收集大家日常工作中遇到的问题,我们取消了早会,大家的问题就不要再等到第二天早会上再提了,有了问题随时提,不要因为没了早会的要求就掩盖问题,如果后边发现出现了问题被掩盖的现象,我们还会恢复早会。

+

在团队划分上,为了便于管理和领域打通,jw 没有再把工程和核心拆成两条线,但大家也能看到kq在推荐工程上的经验比我多的多,而且在核心需求比较多的时候我也确实无法两头都顾及到。再加上由于取消早会后反馈周期的加长,项目的跟进上不可避免会相较之前难度更大,所以我在这里也给kq提个要求,后边我们两个做下分工,所有核心项目我这边都会去了解背景、方案、进度和风险,所有推荐项目kq也要做到这几点,包括内部、产品和对外支持的项目。

+

再回到大家的工作上,大家在有项目、有工作任务的时候就聚焦于手头的工作,力求完美。如果有几天真的没有那么忙时就适当放松,学习一些感兴趣的东西,工作应该有张有弛,一直紧绷和一直放松都不是正常的状态。大家学习的时候尽量学习和我们业务相关的东西,我们组包含了公司内两大块最重要的业务:推荐和 IM,所以要想学肯定是有的学的。我也非常鼓励大家去发现、解决、优化工作中遇到的业务和技术痛点,这会让大家获取更大收益,包括能力上的和绩效结果上的。如果公司内的业务无法满足自己,也可以学习其他自己感兴趣的东西,比如 Web3或者学一门新的编程语言等等。我推荐作为程序员的大家,有精力的话每年学一门新的语言。编程语言会限制我们的思维模式,如果你长期使用某种语言,你就会慢慢按照这种语言的思维模式进行思考。

+

除了工作还有大家的工作状态,每个月总有那么几天不想工作,实在不想工作的那一天就让自己松弛一些。我自己很容易焦虑,所以我很羡慕能拥有松驰感的人。根据我的经验,一个正常排期3-5天的项目如果在状态佳而且无打扰的情况下,大概率一天就能把代码写完,这种状态也叫心流,有本叫《心流》的书大家感兴趣也可以看看。

+

最后,希望大家未来有一天回忆起在 TT 的工作(或实习)经历觉得是有意义的,而不是给大家留下痛苦、无效忙碌的一段经历。

+ +
+ + + + + + + +
+ + + + + + +
+ +
+ + + +
+ + + + + + + + +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2023/trust-crash/index.html b/2023/trust-crash/index.html new file mode 100644 index 0000000000..e38be57ded --- /dev/null +++ b/2023/trust-crash/index.html @@ -0,0 +1,528 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 信托暴雷 | 贾攀的流水账 + + + + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

贾攀的流水账

+ +
+

Panmax's Blog

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+ + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ 信托暴雷 +

+ + +
+ + + + +
+ + +

这两天有不少信托暴雷相关的新闻,之前听说「信托」过这个词,但不知道具体是什么意思,于是想探索了一下,在这里做个记录。

+

信托

信托(Trust)是指委托人基于对受托人的信任,将其财产权委托给受托人,由受托人按委托人的意愿以自己的名义,为受益人的利益或特定目的,进行管理和处分的行为。受托人有责任确保信托资产的安全和管理。信托可以投资于各种资产,包括股票、债券、不动产等。

+
+

信托就像一个保险箱。当一个人有很多钱或贵重物品时,他们可能不想自己管理它们,所以他们把钱或物品放进了保险箱。信托就是这样一个保险箱,它帮助人们管理和保护他们的财产。

+

信托由一个叫做信托公司的专业机构管理。他们会根据人们的要求,把钱或贵重物品放进信托中,并负责管理它们。信托公司的工作就像一个看守人,他们会确保财产安全,并按照人们的指示处理这些财产。比如,当一个人长大后,他们可以告诉信托公司把钱用来支付学费。信托公司也可以帮助人们管理财产直到他们长大。

+

信托还有一个好处是可以帮助人们避免纳税问题。当一个人把钱放进信托时,他们可以减少需要支付的税款。这就像一个特殊的规则,可以帮助人们保留更多的钱。

+
+

暴雷原因

在国外信托的主要用途是保障资金安全,比如家族继承、公司避税、企业破产、富豪离婚这类才用得上信托,但是国内信托成了中高产家庭获取高收益的理财产品

+

国内的销售人员在销售信托产品时夸大其词,盲目追求业绩,承诺回报率在年化8%-12%。这个操作是不是很眼熟?前几年暴雷的 P2P 也是这个套路,我一直以为 P2P 后再没有这么高的收益,没想到是因为我自己没接触到更高端的圈子才不知道这些信息。

+

如果整个池子一直有源源不断的新钱进来,或者市场行情确实不错,用这些钱投资其他产品的回报能获取更高收益,cover 住成本,整个游戏还是可以玩的,但近两年流进来的资金越来越少,其中一个原因是近几年理财产品合规性要求,这些信托产品无法再通过银行渠道销售,损失了很大的销售渠道。

+

再加上市场行情低迷,因为信托公司拿到的这些钱后,大多还是投在上市公司中,近一两年的大 A 股行情惨目忍睹,稳定在3000点左右国家都拉不动。

+

更糟糕的是还有不少储户要提现退出,出现了挤兑最后形成崩盘暴雷,这么来看本质上还是回归到了庞氏骗局。当然国内信托的初衷一定不是想做成庞氏骗局,只是在过程中产生了变形,由于体量太大最后无法挽回。

+

通过这次探索,我也刷新了对中国中高产家庭存款的认知,300万是基操,几千万很常见,最高的能到50亿。

+

一些常见的理财产品

理财是指通过投资来增加财富。理财产品通常包括存款、基金、债券等。理财的风险和收益因产品而异。

+

股票

股票是公司发行的证券之一,代表着公司的一部分所有权。股票的价格在证券市场上波动,投资者可以通过买入和卖出股票来获得利润。

+
+

股票就像是你买了一小部分一家公司的东西,比如买了一小块蛋糕。如果这家公司做得好,蛋糕会变大,你会得到更多的蛋糕。

+
+

基金

基金是由一群投资者的资金组成的投资组合,由专业的基金经理进行管理。基金通常投资于股票、债券、商品等各种资产,以实现投资组合的分散化和风险控制。

+
+

基金就像是一个大大的钱袋子,里面有很多人的钱。这些钱会被专业的人士拿去买很多的蛋糕,也就是投资不同的东西。赚到的蛋糕会分给里面的每个人。

+
+

私募基金

私募基金是只向特定投资者销售的基金,通常要求投资者有一定的财务资格和投资经验。私募基金通常能够提供更高的收益和更高的风险,因为它们不受公开市场的监管。

+
+

私募基金是一种特别的钱袋子,只有一些特别有钱的人才能买。这些人把自己的钱放进去,让专业的人帮他们买更好的蛋糕,帮他们赚更多的钱。

+
+

公募基金

公募基金是向公众开放的基金,任何人都可以购买。公募基金通常受到监管,在投资组合和风险方面有一定的限制。

+
+

公募基金是大家都能买的钱袋子。任何人都可以把自己的钱放进去,由专业的人来帮助大家买蛋糕,一起分享赚到的钱。

+
+

债券

债券是企业或政府发行的借款证券,代表着借款人向债权人的债务。债券的价格通常与市场利率相关,投资者可以通过购买债券来获得固定收益。

+
+

债权就像是你借给别人的钱,别人会约定在一定的时间还给你。就像你借给小朋友一块糖,他会答应过一会还给你。

+
+

保险

保险是一种金融产品,向投保人提供赔偿保障。保险公司通过收取保费来为投保人提供保障。各种类型的保险产品包括寿险、医疗保险、汽车保险等。

+
+

保险就像是一把伞,可以帮助你在出现问题的时候得到帮助。就像下雨时,伞可以遮挡雨水,保护你不被淋湿。

+
+

反思

市场是残酷且真实的,不论你的研究多么到位、预测多么合理,面对整个市场你都是汪洋大海上的一叶扁舟,一个浪头打过来,一切可能瞬间就不复存在。哪怕你真得赢了几次,都可能是在为后面更大的失败埋下伏笔。

+

理解市场、尊重市场、敬畏市场,长久地活下去,才是获得成功的正道。

+

作为个人应该多学习理财知识,适当投资一些美股、港股,分散投资做好资产配置。还是那句话:「不要把鸡蛋放在一个篮子里」。

+

但话说回来,整个市场是个整体,没有一个人是无辜的,表面上是那些富人损失惨重,但所有人都要承担后果,有没有可能这是多米诺骨牌开始倒塌的开始?网上更惊悚的描述是「中国版雷曼兄弟」。

+ +
+ + + + + + + +
+ + + + + + +
+ +
+ + + +
+ + + + + + + + +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2023/use-gist-manage-config/0.png b/2023/use-gist-manage-config/0.png new file mode 100644 index 0000000000..768a953356 Binary files /dev/null and b/2023/use-gist-manage-config/0.png differ diff --git a/2023/use-gist-manage-config/1.png b/2023/use-gist-manage-config/1.png new file mode 100644 index 0000000000..6fa98520ce Binary files /dev/null and b/2023/use-gist-manage-config/1.png differ diff --git a/2023/use-gist-manage-config/2.png b/2023/use-gist-manage-config/2.png new file mode 100644 index 0000000000..cc660de84a Binary files /dev/null and b/2023/use-gist-manage-config/2.png differ diff --git a/2023/use-gist-manage-config/3.png b/2023/use-gist-manage-config/3.png new file mode 100644 index 0000000000..dc8d1a83a8 Binary files /dev/null and b/2023/use-gist-manage-config/3.png differ diff --git a/2023/use-gist-manage-config/index.html b/2023/use-gist-manage-config/index.html new file mode 100644 index 0000000000..36cbb56240 --- /dev/null +++ b/2023/use-gist-manage-config/index.html @@ -0,0 +1,507 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 使用 gist 管理动态配置 | 贾攀的流水账 + + + + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

贾攀的流水账

+ +
+

Panmax's Blog

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+ + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ 使用 gist 管理动态配置 +

+ + +
+ + + + +
+ + +

上一篇文章中提到,我找到了一个非常方便的方法来管理token,那就是使用Github提供的 Gist 功能。

+

https://gist.github.com/ 是 Github 的一个子服务,通常用于托管或分享一些代码片段。与 git 不同的是,无需创建仓库,一个文件就是一个 gist。在打开 gist 首页后,可以直接填写文件描述和文件内容。

+

+

点击右下角会默认创建一个私密的 Gist,但它并不是真正的私密,Github 只是保证其他人在不知道这个 Gist 链接的情况下看不到其中的内容,且里面的内容不会被搜索引擎索引。当你分享这个 Gist 链接后,任何拿到链接的人都可以访问它。

+

例如,我刚刚创建的 gist 链接是:https://gist.github.com/Panmax/5e3444141772e987719147a316782f54

+

分享

通过浏览器的无痕模式打开这个链接:

+

+

编辑

如果我是这个文件的所有者,还可以对文件内容进行更新,在浏览界面点 edit 按钮,或者直接在 url 最后拼上 /edit 访问,如: https://gist.github.com/Panmax/5e3444141772e987719147a316782f54/edit 就可以进入编辑页面。

+

+

获取原始内容

链接尾部加上 /raw 可以取得原始内容。

+

如:https://gist.githubusercontent.com/Panmax/5e3444141772e987719147a316782f54/raw/

+

+

通过上述特性,我们可以将Gist用作动态配置的管理工具。

+

程序可以通过使用 /raw 获取动态内容,我们可以使用 /edit 页面更新内容。理论上,只要不泄露 Gist 链接,您的内容就不会泄露。当然,还要确保源代码不会泄露。

+

有些人可能认为这样不安全,因为数据存放在互联网上,获取链接的人就可以访问数据。

+

但我不这样认为。我们可以将 Gist 链接后面的路径看作数字钱包的私钥。如果你泄露了私钥,谁也帮不了你。只要你妥善保管私钥,通常情况下就没有问题。通过碰撞来暴力破解路径的成本极高,而且里面的内容大部分情况下只是一个简单的代码片段,比数字钱包私钥的价值要小得多。所以黑客们不会费力不讨好地去破解这个东西。

+ +
+ + + + + + + +
+ + + + + + +
+ +
+ + + +
+ + + + + + + + +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2023/using-cash/index.html b/2023/using-cash/index.html new file mode 100644 index 0000000000..b51e87824d --- /dev/null +++ b/2023/using-cash/index.html @@ -0,0 +1,497 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 用现金 | 贾攀的流水账 + + + + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

贾攀的流水账

+ +
+

Panmax's Blog

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+ + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ 用现金 +

+ + +
+ + + + +
+ + +

前两天萌发了一个想法,随着电子支付越来给方便,现在的孩子会不会对金钱越来越没有概念?

+

他们只看到大人在买东西时用手机扫个二维码就可以把东西拿走,好像我们没有减少任何东西、没有任何损失,这种情况恶化后,孩子可能就会出现看到什么就想买什么的情况。

+

《黑客与画家》这本书的作者有个观点:

+
+

以前的青少年似乎也更尊敬成年人,因为成年人都是看得见的专家,会传授他们所要学习的技能。如今的大多数青少年,对他们的家长在遥远的办公室所从事的工作几乎一无所知。他们看不到学校作业与未来走上社会后从事的工作有何联系。

+
+

我们家之前是个体户,自己开门市的,所以我每天都能看到父母在做什么,怎么赚钱。那时候即便父母是在上班或者在事业单位工作,孩子们也有机会到父母工作地点去参观,了解父母的工作内容。现在这种机会非常少,尤其在一二线城市。

+

对孩子来说,如今坐办公室工作的父母就是个黑盒,除了看到父母早出晚归,其他就一无所知了,也无法通过观察父母来学习。

+

以前家里的桌椅板凳小电器坏了、衣服破了,大多是由父母自己来修补,还有句老话:「新三年,旧三年,缝缝补补又三年」。现在大部分父母的做法都是找人上门维修,或者干脆不要了、直接换新的,孩子们不再对大人产生崇拜感,自然也不会有之前的那种尊敬。

+

基于上边的原因,我准备做一些尝试:在孩子面前使用现金。让他们对钱的概念更具象。要让他们看到在买东西时是使用了物理上的钱来交换的,看到父母从钱包或者口袋里掏钱的动作,买了东西之后,钱包或者口袋里的钱会减少。

+

同时也要让他们知道钱有不同的面值,拿大面值买东西,可能会找回小面值,小面值再花出去就没了,大面值的钱尺寸也更大,小面值的钱相对较小。

+

这样做目的也不是为了让孩子们节俭、少花钱,而是让他们对钱有概念,从小产生理财意识,孩子对于买东西的渴望是无尽的,要让他们去思考什么该买什么不该买,当然这也是作为大人要思考的。

+ +
+ + + + + + + +
+ + + + + + +
+ +
+ + + +
+ + + + + + + + +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2023/weight-and-english/0.jpeg b/2023/weight-and-english/0.jpeg new file mode 100644 index 0000000000..8363f70ca3 Binary files /dev/null and b/2023/weight-and-english/0.jpeg differ diff --git a/2023/weight-and-english/1.jpeg b/2023/weight-and-english/1.jpeg new file mode 100644 index 0000000000..6c3d7be92a Binary files /dev/null and b/2023/weight-and-english/1.jpeg differ diff --git a/2023/weight-and-english/2.jpeg b/2023/weight-and-english/2.jpeg new file mode 100644 index 0000000000..516c019fdb Binary files /dev/null and b/2023/weight-and-english/2.jpeg differ diff --git a/2023/weight-and-english/index.html b/2023/weight-and-english/index.html new file mode 100644 index 0000000000..4d8eb9bf9a --- /dev/null +++ b/2023/weight-and-english/index.html @@ -0,0 +1,512 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 记体重与学英语 | 贾攀的流水账 + + + + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

贾攀的流水账

+ +
+

Panmax's Blog

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+ + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ 记体重与学英语 +

+ + +
+ + + + +
+ + +

记录体重

人们常说减肥是一辈子的事情,我以前也是这么认为的。我也一直在为减肥而努力,最早从大学就开始努力了(for a girl)。直到今年放下了,不再那么挣扎了。

+

因为减肥,我养成了一个习惯:记录体重。

+

尽管现在的体重还在超重线以上,但也不再追求降低体重了,减几斤肉实在太难了。虽然不以减肥为目的,现在仍然每天称一次体重,已经养成了一个固定的习惯。

+

+

我使用「瘦身旅程」APP 来记录体重,最早一次的记录产生于2015年8月1日,记录半年后断了2年。之后又持续记录,从最早的记录点算起到现在已经8年了。

+

我曾经最重达到90kg,最轻的时候是65kg。最高和最低相差25kg,也就是50斤。这么大的跨度我说自己减肥成功过不过分吧?

+

把时间拉到一个月为纬度时,可以发现一个规律:每周一早上的体重为波谷,周六早上的体重为波峰。这是因为周一至周五为工作日,作息相对规律,中午还会有运动,饮食也比较注意,所以周六早上的体重是一段时间内的最低点。

+

周六日的放纵会导致体重反弹至高点。周六、周日两天我会出去找好吃的,炫冰淇淋、可乐、炸鸡。我知道这是个非常不好的习惯,但很难改掉。五天的工作让我的欲望被压抑,只能在这两天得到释放。

+

+

持续记录体重有好处,可以观察身体变化。如果今天体重低了,可以回想一下前一天做了什么;如果今天体重高了,也可以回忆一下昨天的饮食。虽然不再追求减肥,但仍在努力阻止熵增。我知道在没有外力的作用下,我的体重只会无限制上涨。

+

此外,持之以恒地记录使我每次看到历史跨度很长的趋势图时都很有成就感。

+

学习英语

我养成的另一个长期习惯是学习英语。以前我用了「不背单词」这个APP,在连续365天后解锁了其中所有的权益,后来突然有一天感觉没有意思,就卸载了。

+

现在我在使用「多邻国」学习英语,它不需要背单词,而是通过语境学习每个单元,包括完整的句子、对话和语法。到今天,我已经持续学习了将近500天。

+

+

我每天学习英语的时间点是早晨上厕所(shit)的时候,利用10分钟学习,时间刚刚好可以学到获得当天宝石的进度。选择这个时间点的最主要原因是,这段时间是当天最早一次长时间使用手机的时间段,我不想把每天最早的宝贵时间用在刷无用的内容上,而是用学习英语当做一天好的开始。

+

上完厕所、学完英语后就是就是称体重环节,两个习惯就这么串起来了。这种叠加方式也是「掌控习惯」这本书中介绍过的培养习惯的一个非常好的方式:继【当前习惯】之后,我将会养成【新习惯】。

+

将学习英语和称体重这两个习惯作为每天早上的固定仪式,就像是生活中的支点一样,因为生活和工作的节奏太快,需要给自己找到一些确定性,以便能够掌控自己的生活。虽然这两件事很小,但它们帮我获得了很大的掌控力。

+

这种超长期周期的持续也让我明白了一个道理:

+
+

坚持一件小事,是靠意志力;长期坚持一件小事,是靠习惯。

+

坚持一件大事,是靠价值观;长期坚持一件大事,是靠信仰。

+
+ +
+ + + + + + + +
+ + + + + + +
+ +
+ + + +
+ + + + + + + + +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2023/why-baoyu-is-gemini/Untitled 1.png b/2023/why-baoyu-is-gemini/Untitled 1.png new file mode 100644 index 0000000000..c20f368e94 Binary files /dev/null and b/2023/why-baoyu-is-gemini/Untitled 1.png differ diff --git a/2023/why-baoyu-is-gemini/Untitled 2.png b/2023/why-baoyu-is-gemini/Untitled 2.png new file mode 100644 index 0000000000..51f2c97da9 Binary files /dev/null and b/2023/why-baoyu-is-gemini/Untitled 2.png differ diff --git a/2023/why-baoyu-is-gemini/Untitled 3.png b/2023/why-baoyu-is-gemini/Untitled 3.png new file mode 100644 index 0000000000..5d173df9e5 Binary files /dev/null and b/2023/why-baoyu-is-gemini/Untitled 3.png differ diff --git a/2023/why-baoyu-is-gemini/Untitled.png b/2023/why-baoyu-is-gemini/Untitled.png new file mode 100644 index 0000000000..a0039d7efc Binary files /dev/null and b/2023/why-baoyu-is-gemini/Untitled.png differ diff --git a/2023/why-baoyu-is-gemini/index.html b/2023/why-baoyu-is-gemini/index.html new file mode 100644 index 0000000000..61f78e740b --- /dev/null +++ b/2023/why-baoyu-is-gemini/index.html @@ -0,0 +1,502 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 为什么我说宝玉是双子座 | 贾攀的流水账 + + + + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

贾攀的流水账

+ +
+

Panmax's Blog

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+ + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ 为什么我说宝玉是双子座 +

+ + +
+ + + + +
+ + +

我在上一篇流水账中提到贾宝玉是双子座,虽然宝玉生日在《红楼梦》原文中一直没有明确写明,但通过一些线索可以推断出宝玉生日是在夏天,且是在农历四、五月左右。证据如下:

+
    +
  • 在六十三回「寿怡红群芳开夜宴」一回中,宝玉晚上想搞个 party,林之孝家过来象征性嘱咐了两句:“还没睡呢?如今天长夜短了,该早些睡,明儿起的方早……”「天长夜短」很明显是夏天的特点。
  • +
  • 六十二回「呆香菱情解石榴裙」中,由于香菱和其他姐妹玩斗草游戏才把石榴裙弄脏了,而且也已经提到这一天是宝玉的生日。「斗草」是端午节的游戏,前文中没有描述过斗草,也没提到端午节,所以姐妹们大概率是在端午节前玩的。
  • +
  • 还是六十二回「憨湘云醉眠芍药裀」,北京地区芍药的盛花期是阳历四五月间,芍药花飞了湘云一身,说明已经是凋谢期了,按照花期来推的话应该是阳历五月底。
  • +
  • 网上还有很多证据说宝玉生日就是农历四月二十六,比如根据张道士说的话、送花神等等
  • +
+

不管哪个证据,宝玉肯定是农历四月底五月初的生日,既阳历(公历)五月底六月初。我按照农历四月二十六这个日期,随机查了几个农历对应的公历(能力有限,只能查到1900年以后的),都是落在双子座的时间范围内。双子座的时间范围是公历5月21日~6月21日。

+

+

+

+

+ +
+ + + + + + + +
+ + + + + + +
+ +
+ + + +
+ + + + + + + + +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2023/why-use-personal-blog/index.html b/2023/why-use-personal-blog/index.html new file mode 100644 index 0000000000..fa7caf6d11 --- /dev/null +++ b/2023/why-use-personal-blog/index.html @@ -0,0 +1,516 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 为什么用个人博客 | 贾攀的流水账 + + + + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

贾攀的流水账

+ +
+

Panmax's Blog

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+ + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ 为什么用个人博客 +

+ + +
+ + + + +
+ + +

为什么我要用一个关联了个人域名,毫不起眼的个人博客写流水账,而不是在时下更流行的公众号、简书、知乎之类这些平台发布?有以下几个原因:

+

不想被熟人看到

我有时会写一些不想在日常中表达的内容,这些内容不太想被熟悉的人看到。

+

但互联网是公开的,既然我把内容发在了网上就应该有被看到的准备,即使未来某一天看到了其实也不要紧。

+

更自由

使用自己的博客想写点什么就写点什么。

+

我的这个博客域名 jiapan.me 在国外注册、没有实名、没有备案,所以基本上没有被审查的风险,但我通常也会遵纪守法,所幸我的域名还没有被墙,站点也托管在 Cloudflare,国内大部分地区也是可以正常访问的。

+

更个性

使用自己的博客想怎么魔改就怎么魔改。

+

博客实际上是个网站,只要你懂点前端就可以对自己的站点就行修改,如果用的是一个开源的博客生成工具,那么可以直接使用其他人提前写好的主题,那么多主题总有一款符合你的审美。

+

再搭配上独一无二的域名,更能体现出个性了。

+

无压力

公众号后台可以看到粉丝数、浏览量这些数据,这无形中给了写作者很大的压力,每次写文章都会考虑这篇文章可以涨几个粉、能带来多少阅读量之类的数据。

+

我这个站点干脆不统计这些数据,我不在意有多少人读、多少人访问。不用每天绞尽脑汁去想如何打造10万+,如何打造爆款。

+

无主题限制

使用自己的博客想写什么主题就写什么主题。

+

在平台上写作还要为垂直领域而困扰,不同领域要迎合不同的读者,在个人网站上就没这个困扰了,我的地盘听我的。

+

在这里,我心情不好时可以吐槽,有了兴趣可以聊聊技术,郁闷时可以抒发感情,激情时可以干一碗鸡汤。

+

无广告打扰

在商业平台内发布,如果这个平台需要变现的话,会在你的文章中间或者四周插入一些广告,有些广告会很 low,很影响阅读体验。

+

不考虑变现

除了平台会插入广告,在一些平台上写作时写作者也可以允许平台插入广告,和平台进行广告收益分成,我目前没有将写流水账当成个营收手段的计划。

+

有一个词叫「私域流量」或者「私域变现」,我总觉得这种词带一些诈骗性质,我天然反感这种硬生生造出来的词。好多人说做公众号就是做私域,这也致使我反感去运营一个公众号。

+

发布

使用自己的博客想什么时候写就什么时候写。

+

在平台上写,发布是一个问题,登录后台困难重重,要经过好几道验证,发布的时候也很麻烦,需要确认一堆内容,而且大部分平台不支持Markdown,但程序员最喜欢的编辑格式就是Markdown。

+

在公众号发文章,还有发布频率限制,修改起来也很麻烦,何必受这个窝囊气。

+

符合我的习惯

公众号这类的平台采用的是 push 模式,写完一篇文章后会 push 给你的订阅者,订阅者们会陷入在一个围墙内,只能看到他们订阅的内容,逐渐生成知识壁垒,每次收到 push 来的新内容还会产生焦虑感。

+

博客类的站点才用的是pull 模式,内容写完后就放在这里,各位看官想什么时候看就什么时候看,在你需要的时候来它就静静的在这里。

+

这也跟我的性格相符,我喜欢自己做决定,不喜欢被 push 的感觉。

+

活的时间可能比平台长

微信只是国内的一个聊天工具,虽然国外用的也比较多,但绝大部分还是中国用户。包括知乎、简书这些平台,我不保证它们在50年后还能活着,如果它真的不在了,用户在上边发布的内容是不是也就不在了。

+

自己搭建一个博客,给域名续上几十年费,页面托管在一个国际主流服务商上,比如 Cloudflare或者 Github,基本就可以永存了。

+

不被平台限制

在平台上写作需要遵守平台规范、更加谨言慎行,一个不注意触犯了平台上的限制那篇文章可能就没了,更严重一些的整个账号就没了,意味着之前发布的文章跟着受到了牵连,之前经营的成果付之一炬。

+

大部分平台,尤其是公众号,是很封闭的,这也意味着你写的内容在搜索引擎上检索不到,不仅搜索引擎搜不到,出了微信就很少看到了。

+

我发现谷歌对独立的博客还是很友好的,我有好几篇流水账通过某个关键字可以排在首页,而且有几篇我随手记问题解决方案帮助过很多人。

+

证明我来过

Web 和域名都是伟大的发明,就像我前边说的,微信未来有一天会死去,但 Web 和域名服务一定会长期留在这个世界上。

+

虽然我是个悲观主义者,但我还是非常向往活着,我希望我的这些碎碎念能一直留在这个世界上,证明我存在过。

+

就像《寻梦环游记》里所说的:死亡不是生命的终点,遗忘才是。

+ +
+ + + + + + + +
+ + + + + + +
+ +
+ + + +
+ + + + + + + + +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2023/yesterday-is-my-birthday/fly.png b/2023/yesterday-is-my-birthday/fly.png new file mode 100644 index 0000000000..db0612d132 Binary files /dev/null and b/2023/yesterday-is-my-birthday/fly.png differ diff --git a/2023/yesterday-is-my-birthday/index.html b/2023/yesterday-is-my-birthday/index.html new file mode 100644 index 0000000000..a566a37a85 --- /dev/null +++ b/2023/yesterday-is-my-birthday/index.html @@ -0,0 +1,509 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 昨天是我31岁生日 | 贾攀的流水账 + + + + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

贾攀的流水账

+ +
+

Panmax's Blog

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+ + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ 昨天是我31岁生日 +

+ + +
+ + + + +
+ + +

昨天是我的生日,最近也是我很长时间以来最灰暗的一段时期。

+

再前一天是我来这家公司的第1000天。

+

这段时间一个人在北京,昨天凌晨一点吃了片安眠药胡乱睡了几个小时。

+

最近组内突然离职很多人,有出国的、有回老家躺平的。

+

我去年因为不想做太多管理工作,并且希望在技术上更精进一些,所以在去年这个月份申请转岗到了现在的新组。

+

但转过来后还是有一部分精力花在虚线带人上,技术上也没什么长进。

+

因为上级离职,最近又转成了实现带人,但是之前的虚线组也还挂在我身上,实线人员招齐后会带一个10人团队,加上虚线组一共能有25人,是在有点应付不过来,打算跟老板聊聊把虚线分出去。

+

最近业务需求压力也非常大,接需求的带宽又很小,我每天忙的脚不沾地,恨不得把自己分成几个人用,跟同事开玩笑说我快学会飞了。

+

+

这段时间的作息是:

+
    +
  • 早上七点起床,七点半出门
  • +
  • 九点到公司赶紧写会代码
  • +
  • 十点半后被拉着开各种会,或者处理各种线上问题
  • +
  • 晚上八点半下班十点到家短暂休息几个小时
  • +
+

希望这段灰暗的时间赶快过去,一切都会好起来的。

+

此刻心情非常down,花几分钟时间记录一下,也算有个释放的口子,自己诉一诉苦。

+

想到基督山伯爵里的一句话(虽然我还没看过全书):

+
+

人类的一切智慧是包含在这四个字里面的:「等待」和「希望」

+
+ +
+ + + + + + + +
+ + + + + + +
+ +
+ + + +
+ + + + + + + + +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2024/The-Psychology-of-Money/index.html b/2024/The-Psychology-of-Money/index.html new file mode 100644 index 0000000000..3b7fc04d1b --- /dev/null +++ b/2024/The-Psychology-of-Money/index.html @@ -0,0 +1,708 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 《金钱心理学》摘抄 | 贾攀的流水账 + + + + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

贾攀的流水账

+ +
+

Panmax's Blog

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+ + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ 《金钱心理学》摘抄 +

+ + +
+ + + + +
+ + +

我所读过的理财类书籍并不多,在国庆后由于人性的贪婪,在股市中损失了(对我来说)一大笔钱,机缘巧合下读了这本名叫《金钱心理学》的理财类书籍。这是我读的为数不多的觉得写的非常好的理财书之一,哪怕不限于理财类,它也是一本用来了解人性和世界观的好书,由于得到了非常好的阅读体验,从另一方面来说这次的投资失利也许对我来说属于因祸得福了。

+

在读《金钱心理学》时,我脑海中经常飘出一句励志的话:「种一棵树最好的时间是十年前,其次是现在」,刚刚查了一下这句话的来源,出自非洲经济学家丹比萨·莫约的《援助的死亡》一书,巧合的是也是一位经济学家说的。我现在已经开始了超长线的定投计划,用十年时间来定投黄金和标普500,自从开始定投后就出现了两种有冲突的念头:既想让时间过快一点,好让我完成我的定投目标,见证时间和复利带来的强大效果,又想让时间过慢一些,自己还不想那么快的老去,想再多一些时间陪伴孩子们,更不想眼睁睁看着父母一天天的老去。这本书还纠正了我一个错误观念,我之前认为财富跟赚钱多少成非常强的正相关性,这本书告诉我并不是这样,收入当然是一部分,但对大部分人来说更重要的是节俭和储蓄。

+

这本书中没有教我们认识各种指标,都是一些软技能,下边是我从这本书中摘录下的句子,通过这些句子也能感受到这本书再讲的是什么样的理财观念。最后我会在写一写我准备开启的一段超长线投资计划。

+

我最喜欢的句子

    +
  • 人们习惯把别人的失败归咎于错误的决策,而把自己的失败归咎于糟糕的运气。
  • +
  • 现代资本擅长创造两种东西:财富和嫉妒。
  • +
  • 时间自由是财富能带给你的最大红利。
  • +
  • 富有的最高级形式是,每天早上起床后你都可以说:“今天我能做我想做的任何事。”
  • +
  • 通过用金钱购买昂贵之物获得的尊重和羡慕可能远比你想象中少。
  • +
  • 历史是对变化的研究,但具有讽刺性的是,人们却将历史当作预测未来的工具。
  • +
  • 杠杆——以负债的方式进行投资——把常规风险扩大到了足以导致毁灭的程度。
  • +
  • 只有当你能给一项计划数年或数十年的时间去成长时,复利的效应才能得到最佳体现。
  • +
  • 无论在工作生涯的哪个节点,都要定下这样均衡的目标:每年做好适中的储蓄,给自己适度的自由时间,让通勤不超过适当的时长,至少花适量的时间来陪伴家人。
  • +
  • 如果你把波动看作要买的入场券,情况就会完全不同。
  • +
  • 市场回报永远不会是免费的,现在不是,将来也不会是。你需要支付一定的费用,就像要花钱购买一件产品一样。
  • +
  • 在做计划的时候,我们会专注于我们想做的和能做的事情,而忽略了他人的计划和能力,但他人的决策也会对结果产生影响。
  • +
  • 用能让你睡踏实的方式来理财。
  • +
  • 如果你想提高投资回报,最简单而有效的方法就是拉长时间。时间是投资中最强大的力量。
  • +
  • 增长是由复利驱动的,而复利通常需要时间。毁灭却可能由独立的致命因素导致,可以在很短的时间内发生;它也可能由失去信心引发,而信心可以在一瞬间崩塌。
  • +
+

全部摘抄的句子

    +
  • 一个无法控制个人情绪的天才或许会引发财务上的灾难,但反过来看——那些没有接受过专业金融教育的普通人,也可以凭借与智商衡量标准无关的良好行为习惯,最终走向富裕。
  • +
  • 财务方面的成功并不是一门硬科学,而是一种软技能——你怎么做,比你掌握多少知识更重要。
  • +
  • 有两种事物会影响每一个人,不管你是否对它们感兴趣——健康和金钱。
  • +
  • 我认为,这种现象的主要原因是,我们思考和学习理财的方式更像学习物理的(涉及很多法则和定律),而不像学习心理学的(关注情感及其微妙变
  • +
  • 关于金钱的知识和经验可以被用于生活中的其他许多问题,比如风险、信心和幸福中。很少有其他事物能像金钱这样,仿佛一面强有力的放大镜,帮助你理解人们为何会做出某些举动。
  • +
  • 人类涉及金钱的行为是地球上最伟大的表演之一。
  • +
  • 历史从来不会重复,人类却总会重蹈覆辙。
  • +
  • 你对金钱的个人经验可能只有0.00000001%符合实际,但它构成了你对世界运作方式的主观判断的80%。
  • +
  • 研究股市的历史后,你会觉得自己明白了某些事,但只有亲身经历过,感受过它的巨大影响,你才可能真正改变自己的行为
  • +
  • 有些事只有真正经历过才会懂。
  • +
  • 人们一生中的投资决策在很大程度上取决于其生活经历——尤其是成年后的早期经历。
  • +
  • 每个人对金钱的体验都是不同的,即使是在那些你觉得经历很相似的人之间。
  • +
  • 个体的不同经历可能导致他们对那些看似没有争议的话题出现完全不同的看法。
  • +
  • 人们做的与金钱相关的每个决定都有其合理的一面,因为这些决定是他们在掌握了当时所能掌握的信息,然后将其纳入自己对世界运作方式的独特认知框架后做出的。
  • +
  • 每个关于金钱的决定对当时的他们来说都是合理的,是建立在他们当时具备的条件之上的选择。
  • +
  • 我们之所以经常在金钱方面做出看似疯狂的决策,是因为相较之下在这场游戏里我们都是新手,而在你看来不可理喻的行为对我而言却合乎情理。但是,没有谁真的失去了理智——我们都在依靠自己独特的经验做出选择,而这些经验在特定的时间点和情境下都是合理的。
  • +
  • 生活中的每一个结果都受到个人努力之外的其他作用的影响。
  • +
  • 任何事都没有表面看来那样美好或糟糕。
  • +
  • 在生活这场游戏中起作用的除了我们自己,还有其他70亿人,同时还存在着无数的变量。那些在你控制之外的行为产生的意外影响可能比你有意识的行为产生的影响更大。
  • +
  • 因为运气难以被量化,把他人的成功归咎于运气又是一种不礼貌的举动,所以我们大多数时候会自动忽略运气在成功中扮演的重要角色。
  • +
  • 在评价别人时,将成就归功于运气会显得你很嫉妒和刻薄,哪怕我们知道的确存在运气的成分;而在评价自己时,将成就归功于运气则会令自己感到气馁,难以接受。
  • +
  • 人们习惯把别人的失败归咎于错误的决策,而把自己的失败归咎于糟糕的运气
  • +
  • 不要太关注具体的个人和案例研究,而要看到具有普适性的模式。
  • +
  • 预防失败的诀窍是:做好你的财务规划,使其不至于因为一次糟糕的投资和未能达成的财务目标而全盘崩溃,保证自己能在投资道路上持续前进,一直等到好运降临的那一刻。
  • +
  • 风险的存在也意味着在评价自身的失败时,我们应该原谅和理解自己。
  • +
  • 为了赚他们并未拥有也不需要的钱,他们拿自己已经拥有并确实需要的东西去冒险了。这是愚蠢至极的做法。冒着失去重要东西的风险去争取并不重要的东西的行为毫无道理可言。
  • +
  • 最难的理财技能是让逐利适可而止。
  • +
  • 现代资本擅长创造两种东西:财富和嫉妒。
  • +
  • 幸福是你拥有的减去你期待的。
  • +
  • 攀比就像一场没有人能打赢的战役,取胜的唯一办法是根本不要加入这场战争——用知足的态度接受一切,即使这意味着自己比周围的人逊色
  • +
  • 如果你无法拒绝潜在的金钱诱惑,那么欲望最终可能将你吞没。
  • +
  • 一个领域里的知识和经验常常可以为其他领域提供重要的借鉴。
  • +
  • 冰期形成的主要原因并非极寒的冬季,而是凉爽的夏季。
  • +
  • 地球冰川形成的关键并不一定是大量的降雪,而是雪能累积下来,无论量有多少。
  • +
  • 成功的投资并不需要你一直做出成功的决定。你只要做到一直不把事情搞砸就够了。
  • +
  • 但守富的方式却只有一种:在保持节俭的同时,还需要一些谨小慎微。
  • +
  • 致富和守富是两种完全不同的技能。
  • +
  • 致富需要的是冒险精神、乐观心态,以及放手一搏的勇气。
  • +
  • 守富需要谦逊和敬畏之心,需要清楚财富来得有多快,去得就有多容易。守富需要节俭,并要承认你获得的财富中一部分源自运气,所以不要指望无限复制过去的成功。
  • +
  • 生存应该成为你一切策略的基础,无论是关于投资、规划个人职业还是经营生意的
  • +
  • 没有任何收益值得你冒失去一切的风险。
  • +
  • 你的财务规划要求的具体前提条件越多,你的财务状况就越脆弱。
  • +
  • 从长远看结果是积极的,但从短期看过程可能很糟糕”这一点乍看之下不符合直觉,但生活中很多事确实是这样的。
  • +
  • 经济、市场和个人职业生涯通常也会遵循一条相似的路径——在不断的损失中持续增长的过程。
  • +
  • 对一个投资者来说,为了避免心态膨胀,付出再大的代价都是值得的。
  • +
  • 当投资者持有这些藏品的时间足够长,这系列投资组合的整体收益就会趋近其中表现最好的部分的收益
  • +
  • 一个投资者在一半的时间里都看走了眼,最后却仍然能致富,这个事实是不符合直觉的。它也意味着我们低估了许多事物失败的频率,所以当失败发生时,我们就会反应过度
  • +
  • 任何规模巨大、利润丰厚、声名远播或影响力深远的事物都源自某个尾事件——从几千甚至几百万个事件中脱颖而出的一个。
  • +
  • 拿破仑对军事天才的定义是“当身边所有人都进入非理性状态时还能继续正常行事的人”。
  • +
  • “当下”其实并没有那么重要。作为投资者,你今天、明天或下周做的决定远不如你一生中个别几天做的决定重要。
  • +
  • 一个投资天才也应该是一个当身边所有人都进入非理性状态时还能继续正常行事的人。
  • +
  • 如果你是一个优秀的雇员,在经过三番五次的尝试和试验后,你终究会在适合自己的领域找到适合自己的公司。
  • +
  • 当我们特别关注某些榜样的成功时,我们就会忽视这样一个事实:他们的成功来自他们全部行为中的一小部分。这种忽视会让我们觉得我们自己的失败、亏损和挫折是因为我们做错了什么。
  • +
  • “重要的不是你对了还是错了,”“金融大鳄”乔治·索罗斯(George Soros)曾说,“而是当你对的时候,你能赚到多少,或者当你错的时候,你会损失多少。”你即使有一半的时间都在犯错,到最后依然能赢。
  • +
  • 时间自由是财富能带给你的最大红利。
  • +
  • 富有的最高级形式是,每天早上起床后你都可以说:“今天我能做我想做的任何事。”
  • +
  • 幸福是一个复杂的话题,因为每个人的幸福观都不同,但如果幸福的分数有一个公分母——一种普遍的快乐源泉——那就是对生活的全面掌控。
  • +
  • 在自己喜欢的任何时候和自己喜欢的对象做想做的事,而且想做多久就做多久,这样的自由是极其珍贵的,而这就是金钱能带给我们的最大红利
  • +
  • 不是工资多少,不是房子大小,也不是工作好坏,而是对自己想做什么、什么时候做、和谁一起做拥有掌控能力。这是生活中决定幸福感的通用变量。
  • +
  • 金钱最大的内在价值是它能赋予你掌控自己时间的能力——这句话没有任何夸张的成分。
  • +
  • 拥有更多财富则意味着在失业后可以从容地等待更好的职业机会,而不必急于抓住遇到的第一根救命稻草。这种能力可以改变一个人的生活。
  • +
  • 拥有更多财富则意味着可以选择一份待遇不高但时间灵活的,或是通勤时间比较短的工作
  • +
  • 做一份自己喜欢却无法掌控时间的事和做自己讨厌的事没什么区别。
  • +
  • 与前几代人相比,我们对时间的控制力降低了。正因为控制时间是影响幸福感的关键因素,所以我们无须对尽管现在的我们更富有了,但我们没有感到更快乐这一事实感到惊讶。
  • +
  • 这里存在一个悖论:我们都想通过财富来告诉其他人,自己应该受到他们的爱慕与敬仰。但事实上,其他人常常会跳过敬仰你这一步。这并不是因为他们觉得你的财富不值得羡慕,而是因为他们会把你的财富当作标尺,转而表达自己渴望被爱慕与敬仰的愿望。
  • +
  • 你或许觉得你需要一辆昂贵的车子、一块豪华的手表和一座很大的房子,但我想告诉你的是,你并非真想得到这些东西本身。你真想得到的是来自他人的尊重和羡慕。你觉得拥有昂贵的东西会让别人尊重和羡慕你,但可惜,别人不会——尤其是那些你希望得到其尊重和羡慕的人。
  • +
  • 通过用金钱购买昂贵之物获得的尊重和羡慕可能远比你想象中少。
  • +
  • 比起豪车,谦虚、善良和同情心等人格特质才能帮你获得更多尊重。
  • +
  • 炫富是让财富流失的最快途径。
  • +
  • 我们总是喜欢用看到的东西为标准来判断一个人是否富有,因为这些是摆在我们面前、实实在在的东西。
  • +
  • 现代资本主义致力于帮助人们通过超前消费的方式来享受原本力不能及的物质生活,并将这种消费观发展为一个备受推崇的产业。
  • +
  • 财富并不是我们能看到的外在部分。
  • +
  • 财富是由未被转化为实物的金融资产体现的
  • +
  • 让自己感到富有的最佳方式莫过于把大笔钱花在那些真正美好的东西上。但想真变得富有,你需要做的是花自己已经有的钱,而不是透支还不属于自己的钱。事情就是这么简单。
  • +
  • 想真变得富有,唯一的途径就是别去消耗你拥有的财富。这不仅仅是积累财富的唯一方式,也是富有的真正定义
  • +
  • 人们对一次身体锻炼所能燃烧的能量的估值比实际消耗的能量高了4倍,而他们接下来平均摄入的能量大约是运动中消耗的能量的2倍。
  • +
  • 我们很容易找到有钱的人做榜样,但想找到富有的人却不容易,因为从性质上讲,他们的成功更隐蔽。
  • +
  • 富有的前提其实是克制。
  • +
  • 我们擅长通过模仿来学习,但财富看不见的特性让我们很难模仿和学习他人的经验。
  • +
  • 这个世界上有很多看起来低调但实际上很富有的人,还有很多看上去很有钱却生活在破产边缘的人。
  • +
  • 个人的节俭和储蓄行为——在金融方面的节约和高效——是金钱等式中你具备更强控制力的部分,而且在未来也会像今天一样,是百分百行得通的方法。
  • +
  • 财富是对收入扣除开支后剩下的部分进行积累的结果。
  • +
  • 即使你收入不高,你依然可以积累财富,但如果你的储蓄率不高,你绝不可能积累财富——两相对比,孰轻孰重显而易见。
  • +
  • 如果你学会用更少的钱来获得同样多的幸福感,你的欲望和所得之间就会产生积极的落差。你也可以通过提升收入来造就这种落差,但欲望和所得之间的落差才是你更容易控制的。
  • +
  • 但在金钱收支公式的两端,人们在一端投入了大量的精力,在另一端却鲜有作为。这就给了大多数人一个机会
  • +
  • 当你把存款定义为虚荣的自我和收入之差时,你就能明白,为什么很多收入不低的人很难存下钱来,因为他们每天都在和自己想要尽情炫耀并与其他炫富者攀比的本能抗争。
  • +
  • 在一个智力方面的竞争已经白热化,而很多旧有技术已经被自动化技术取代的世界里,竞争优势开始转向更加细微的软件层面,比如沟通能力、共情能力,以及最重要的一点——一个人的灵活度。
  • +
  • 当智力不再是一种持久的优势时,拥有别人没有的灵活度是少数几种能帮你拉开与别人的距离的特质。
  • +
  • 在做投资决策时,不要试图保持绝对理性,而要做出对你而言合乎情理,也就是更好接受的选择。
  • +
  • 坚持对理财来说才是至关重要的一点。
  • +
  • 医生的职责不是简单地治好病,而是使用能让病人接受的人性化手段治好病。
  • +
  • 在影响收益表现(包括收益额和在一定时间内有所收益的概率)的诸多金融参数中,相关性最大的莫过于在经济不景气的年份对投资策略的长期坚持。
  • +
  • 任何能让你留在投资游戏中的因素都会在时间方面增强你的优势。
  • +
  • 如果你一开始就对投资对象很感兴趣——这家企业的使命、产品、团队和技术等方面都非常合你的口味——那么当它因为收益下滑或需要帮助而进入不可避免的低谷期时,你至少会因为感到自己在做一件有意义的事而对损失没有那么在意。
  • +
  • 在其他一些涉及金钱的情况下,做个现实主义者也比做个绝对理性主义者强。
  • +
  • 大多数对经济和股市走向的预测都极不靠谱,但是做预测这种行为本身是合乎情理的。
  • +
  • 人生中很少有理论与现实一致的时候。
  • +
  • 历史是对变化的研究,但具有讽刺性的是,人们却将历史当作预测未来的工具。
  • +
  • 世界上总在发生过去从来没有发生过的事。
  • +
  • 历史研究的主要内容是意料之外的事件,但投资者和经济学家们却经常将其看作对未来不容置疑的指南。
  • +
  • 但是投资并非硬科学。投资从本质上说,是规模巨大的一群人根据有限的信息针对将给他们生活幸福度带来巨大影响的事情做出不完美决策的行为,而这会让最聪明的人也变得紧张、贪婪和疑神疑鬼。
  • +
  • 和金钱相关的任何事背后最重要的驱动力,是人们对各种现象的合理化解释以及对商品和服务的偏好。
  • +
  • 历史上最重要的事件往往是一些重大的、史无前例的意外事件。
  • +
  • 人们对刺激物的反应会随着时间前进而趋于稳定。
  • +
  • 为错误留出余地的行为的智慧就在于承认不确定性、随机性和概率——“一切未知情况”——的存在
  • +
  • 在尽量扩大预测与实际可能发生情况的概率之差的同时,为自己留出即使预测错误也能从头再来的余地。
  • +
  • 安全边际的目的在于让预测变得不再必要。
  • +
  • 那就是我们不能把眼前的世界看成黑白分明的
  • +
  • 在你可以接受可能出现的各种结果的灰色区域展开追求,才是最明智的前进方式。
  • +
  • 但在和金钱有关的几乎所有事务中,人们都低估了容错空间的必要性。
  • +
  • 我们不愿意留出容错空间的原因有两个。第一,我们认为一定有人知道未来会发生什么,因此承认未来的不可知性会让我们感到不舒服。第二,一旦预测成真,你就错过了充分利用该预测去采取行动的时机,会让自己蒙受损失。
  • +
  • 我们很容易低估30%的金融资产损失对自己心理产生的影响。你的信心可能在机会最好的时候严重受挫
  • +
  • 理论上的承受力和情感上的承受力之间的差距是人们常常忽略的一种容错空间。
  • +
  • 获得幸福的最佳方式是把目标定得低一些。
  • +
  • 冒险行为中的乐观偏见”或者“‘俄罗斯轮盘赌在统计学上是可行的’综合征”——当不利结果无论如何都无法接受时,人们一厢情愿地认为出现有利结果的可能性更大的现象。
  • +
  • 如果一件事有95%的概率成功,那么剩下的5%的失败概率就意味着在你人生中的某个时间点,你一定会遭遇失败。如果这种失败意味着输得精光,那么即使出现有利局面的概率是95%,这个险也不值得你去冒,无论它看上去多么诱人。
  • +
  • 杠杆——以负债的方式进行投资——把常规风险扩大到了足以导致毁灭的程度。
  • +
  • 大多数时候的理性乐观主义会让人们忽视极端少数情况下倾家荡产的可能性。
  • +
  • 随时可以做自己想做的事而且想做多久就做多久的能力,才是无限投资回报的源泉。
  • +
  • 在金钱方面,隐患最大的单点故障便是短期开支全部依靠工资,而没有在计划中的开支和将来可能需要的开支之间用存款来建立缓冲空间。
  • +
  • 如果你的理财规划只为已知的风险做准备,那么它会缺乏足够大的安全边际,是无法经受现实世界考验的。
  • +
  • 事实上,每个计划中最重要的部分,就是为计划赶不上变化的情况做好预案。
  • +
  • 我们很难预料到自己未来的想法。
  • +
  • 每个5岁小男孩在成长过程中都有过开拖拉机的梦想。在一个小男孩的眼中,没有什么工作能比每天开着拖拉机,喊着“呜呜呜,嘟嘟嘟,大拖拉机来啦”更美好的事了。
  • +
  • 一个30多岁的新手父母对人生目标的规划是18岁时的他或她无法想象的。
  • +
  • 在我们生命中的每个阶段,我们都会做出一些决定。这些决定会深刻地影响我们未来的生活。当我们实现了曾经的梦想后,我们并不总会对自己当初的决定感到开心。所以我们看到,青少年花了大价钱文身,在长大后又要花大价钱洗掉;有人年轻时急着和某人结婚,上了年纪后却盼着和同一个人离婚;有人中年时努力想得到的东西,年老后却又拼命想放弃……这样的例子不胜枚举。
  • +
  • 只有当你能给一项计划数年或数十年的时间去成长时,复利的效应才能得到最佳体现。
  • +
  • 无论在工作生涯的哪个节点,都要定下这样均衡的目标:每年做好适中的储蓄,给自己适度的自由时间,让通勤不超过适当的时长,至少花适量的时间来陪伴家人。
  • +
  • 在一个人人都随着时间改变的世界上,沉没成本——过去的决策导致的无法收回的支出——就像一头拦路虎。
  • +
  • 投资的成功需要投资者付出相应的代价,但衡量这种代价的不是金钱,而是波动、恐惧、怀疑、不确定感和悔恨——如果你不是那个直接面对它们的人,这些代价都容易被你忽视
  • +
  • 财富之神并不青睐那些只求回报却不愿付出的。
  • +
  • 投资成功需要付出的代价是我们无法立刻看到的。它不会被直观地写在标签上。所以,当你需要支付这种账单时,你会觉得这笔钱并不是为购买好东西而支付的价钱,反倒更像做错事后必须缴纳的罚款,虽然在人们看来,付账是很正常的事,缴纳罚款却是应该避免的,所以人们觉得应该采取某些明智的预防措施,让自己避免受罚。
  • +
  • 把市场波动看作要支付的价钱而不是该缴纳的罚款的视角看似微不足道,却是培养正确理财心态的重要部分
  • +
  • 如果你把波动看作要买的入场券,情况就会完全不同。
  • +
  • 几乎所有波动都是一种费用,而非一笔罚款。
  • +
  • 市场回报永远不会是免费的,现在不是,将来也不会是。你需要支付一定的费用,就像要花钱购买一件产品一样。
  • +
  • 人们常常在缺乏足够信息和不讲逻辑的情况下做出一些理财决定,之后又悔不当初。但站在他们当时的角度来看,这些决定是有道理的。
  • +
  • 投资者们总是会天真地向那些和自己情况不一样的人学习理财经验。
  • +
  • 当投资者的目标和时间规划不同时——在任何一种投资中都会出现这种情况——在一个人看来不合理的价格在另一个人看来也许是可以接受的,因为他们各自关注的因素是不同的
  • +
  • 金融领域内的一条铁律是:金钱会追逐回报的最大化。
  • +
  • 当交易者推高短期回报,更多的投资者就会开始入场。不久之后——通常时间都不会太久——短线投资者就成了最有权威的股市定价者了。
  • +
  • 泡沫与估值上升的关系不大。它体现的其实是另一种情况:随着越来越多的短线投资者入场,交易周期变得越来越短。
  • +
  • 泡沫之所以会形成,并不是因为人们在非理性地参与长期投资,而是因为人们在某种程度上堪称理性地转向短线交易,以追逐不断滚雪球式增长的积极动量。
  • +
  • 很多金融和投资决策都建立在对他人的观察、模仿或与他人对赌的基础上,但如果你不知道为什么有些人会那样做,你就不知道他们那种行为会持续多久,什么会让他们改变主意,或者他们是否会吸取教训并做出调整。
  • +
  • 尽可能努力明确自己玩的是什么游戏。
  • +
  • 但在多年前,我曾这样总结我的理财哲学:我是一名被动的投资者,但对这个世界创造货真价实的经济增长的能力持乐观态度。我相信在接下来的30年里,这种增长会让我的财富不断增加。
  • +
  • 出于一些我无法理解的原因,人们总喜欢听别人说这个世界要完蛋了。
  • +
  • 对绝大多数人来说,保持乐观都是最好的选择,因为这个世界在大多时候对大多数人来说都是越变越好的
  • +
  • 乐观主义是一种信念,相信就算过程中充满坎坷,随着时间过去,你心目中好结果出现的概率也比坏结果出现的概率大
  • +
  • 如果你告诉人们一切都会变得很好,他们可能会不以为然,或者用怀疑的目光看着你。但如果你说他们正处于危险中,你就会获得他们的全部注意力。
  • +
  • 一个在众人心怀绝望时满怀希望的人不会被看重,但一个在众人都心怀希望时满怀绝望的人却会被视为圣人
  • +
  • 人类对失去的过度厌恶是在演化过程中形成的一种保护机制。
  • +
  • 在进行直接比较或权衡时,失去带给我们的精神影响比得到更大。
  • +
  • 相比机遇,对威胁反应更快的生物成功生存和繁殖的可能性才更大
  • +
  • 悲观主义者在推测未来趋势时经常没有将市场会如何适应局势纳入考虑。
  • +
  • 经济学中有一条铁律:极好和极糟的环境都很难长期维持,因为市场的供需会以很难预测的方式对环境进行适应。
  • +
  • 眼前的问题有多糟糕,人们解决问题的动力就有多强——这是经济史中普遍存在的一种现象,却很容易被悲观主义者忽视,
  • +
  • 进步发生得太慢,让人难以发觉,但挫折却出现得太快,让人难以忽视。
  • +
  • 增长是由复利驱动的,而复利通常需要时间。毁灭却可能由独立的致命因素导致,可以在很短的时间内发生;它也可能由失去信心引发,而信心可以在一瞬间崩塌。
  • +
  • 在投资中,你必须认识到成功的代价——在长期增长的背景下出现的波动和损失——并做好为其买单的准备。
  • +
  • 故事是现存的对经济影响最大的潜在力量之一。
  • +
  • 故事是经济发展中最强大的一股力量。
  • +
  • 你越希望某事是真的,你就越容易相信一个高估其成真可能性的故事。
  • +
  • 金融领域内的很多投资观点都带有这样的特性:一旦你听从它们,选择了某种策略或方法,你就同时在金钱和心理上进行了双重投资。
  • +
  • 每个人对世界的看法都是不完整的,但每个人都会编织完整的故事来弥补其中的空白。
  • +
  • 后见之明,即人们解释过去事件的能力,给了我们一种仿佛这个世界可以被理解的错觉,也给了我们一种仿佛这个世界自有其原则的错觉,哪怕在实际上一团混乱的情况下。这是我们在很多领域犯错的重要原因。
  • +
  • 对控制力的幻想比充满不确定性的现实更容易让人接受,所以我们死死抓着某些故事不放,骗自己以为结果尽在掌握。
  • +
  • 在做计划的时候,我们会专注于我们想做的和能做的事情,而忽略了他人的计划和能力,但他人的决策也会对结果产生影响。
  • +
  • 无论是在解释过去还是预测未来时,我们都专注于技能起到的因果性作用,而忽略了运气的重要影响。
  • +
  • 我们专注于我们知道的,忽视了我们不知道的,而这让我们对自己的想法过于自信。
  • +
  • 当事态朝正确的方向发展时,要保持谦逊;当事态朝错误的方向发展时,要心怀谅解或同情。这是因为任何事都没有表面看来那样美好或糟糕。
  • +
  • 虚荣越少,财富越多。你能存下多少钱,要看你彰显自我的需求与你的收入之间的差距,而财富恰恰存在于看不到的地方
  • +
  • 用能让你睡踏实的方式来理财。
  • +
  • 如果你想提高投资回报,最简单而有效的方法就是拉长时间。时间是投资中最强大的力量。
  • +
  • 你应该始终通过衡量自己的整体投资情况,而不是根据某一笔投资的成败来评价自己的表现。
  • +
  • 利用财富来获取对时间的掌控,因为对人生的幸福感而言,最严重而普遍的扣分项就是时间上的不自由。在任何时候和喜欢的人去做喜欢的事而且想做多久就做多久的能力,才是财富能带给你的最大红利。
  • +
  • 多一些善意,少一些奢侈。
  • +
  • 存钱。存就是了。存钱不需要什么特定理由。
  • +
  • 明确成功需要付出的代价。然后做好支付的准备,因为没有什么有价值的东西是免费的。
  • +
  • 你应该喜欢风险,因为长期看它能带给你回报
  • +
  • 这些决策的目的往往不是追求最高的回报,而是尽量降低让伴侣或孩子失望的可能。
  • +
  • 我的目的并不是赚大钱。我想要的不过是独立自主而已
  • +
  • 最主要的秘诀是控制你的欲望,在能力范围内尽可能节俭地生活。自主性与你的收入水平无关,而是由你的储蓄率决定的。而当你的收入超过一定水平后,你的储蓄率是通过控制自己对生活方式的欲望决定的。
  • +
  • 在你负担得起的范围内舒适地生活,不产生过多欲望,你会避免现代西方世界中许多人要承受的巨大社会压力。
  • +
  • 退出无谓的激烈竞争,以获得内心平静为目标来调节你的行为,才是真正的成功。
  • +
  • 比起让金融资产的长期收益最大化,不用每个月还贷款的选择让我们感觉更好,因为这让我感到独立和自由。
  • +
  • 查理·芒格说:“复利的第一条原则是:除非万不得已,永远不要打断这个过程。”
  • +
  • 对大多数投资者来说,用平均成本法【一种以定期及定额投资去积累资产(包括股票及基金)的方法,即“定投”。】去投资低成本的指数基金将是长线投资成功率最高的选择。
  • +
  • 我始终坚持的投资理念是,在投资领域,努力和结果之间几乎没有关系。这是因为世界是由尾事件驱动的——少数几个变量是大部分回报的来源。
  • +
  • 我的投资策略并不依靠选择正确的行业或者把握下一次经济衰退的机会,而是依靠高储蓄率、耐心和认为接下来几十年里全球经济将不断创造价值的乐观态度。
  • +
  • 历史不过是糟心事接踵而至的过程。
  • +
  • 在第二次世界大战结束后,美国人花了75年的时间,培养出了普通家庭对债务文化的高接受度。
  • +
  • 虽然每个群体呐喊的具体细节不同,但他们呐喊的原因——至少部分原因——是在第二次世界大战后形成的对社会本该大体平等的预期落空了。他们没能获得别人获得的利益。
  • +
  • 想想这种心态一旦受到社交媒体和有线新闻网强大的传播力量催化后会变成什么样。在这些平台上,人们比以往任何时候都更容易看到别人是怎样生活的——这就像火上浇油一样。
  • +
  • 互联网让人们越频繁地接触新观点,人们对这些新观点的存在就越感到愤怒
  • +
  • 预期的调整总是晚于实际情况的变化。
  • +
+

长期理财规划

我为自己设计了一个为期至少10年的理财规划,规则很简单:通过蚂蚁财富,每天分别定投100元到「黄金ETF」 和「标普500ETF」,如果行情出现1%的回撤时则当日加仓50元,当盈利超过30%时则赎回30%的仓位,后边再慢慢加仓定投回去。

+

我们来粗略估算了一下10年下来的投资金额:

+
1
2
3
4
5
6
7
8
# 每年大约有250个交易日,每年的投资额为:
250天*100元*2只股票=5万元

# 多预备10%的钱来补仓
5万+0.5万=5.5万

# 10年下来就是
5.5万*10=55万
+

按照10年的投资回报率80%(虽然市场不可预测,但人总要有些盼头,况且我定投的两项在过去十年中回报都超过了200%),这样10年下来大概会有44万的投资回报,加上本金刚好100万。虽然这么长的时间只回报44万看起来不多,但这对我来说这是一种无痛的投资方式,用作者的话是:「用能让你睡踏实的方式来理财」。当然在这个过程中有可能还会根据我的生活水平来调整定投金额,比如5年后我提前把房贷还完了,也许就能出多个2、3倍的闲钱用于定投。另外,本段开头也说了,我这个投资计划短则持续10年,长则持续20年、30年,也许在复利的作用下收益远远不止于我上边计算出来的那么多。

+ +
+ + + + + + + +
+ + + + + + +
+ +
+ + + +
+ + + + + + + + +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2024/Why-not-update-for-a-long-time/index.html b/2024/Why-not-update-for-a-long-time/index.html new file mode 100644 index 0000000000..7d8057510b --- /dev/null +++ b/2024/Why-not-update-for-a-long-time/index.html @@ -0,0 +1,494 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 为什么好久没更新了 | 贾攀的流水账 + + + + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

贾攀的流水账

+ +
+

Panmax's Blog

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+ + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ 为什么好久没更新了 +

+ + +
+ + + + +
+ + +

可以看到我在去年8、9月份频繁更新了一批文章,然后在11月就戛然而止了。

+

昨天早上坐在旁边的同事告诉我,他的女朋友周末把我的博客通过文字转语音的方式边听边做家务,并且想要人肉催更。每次听到有人说读了我的博客,而且希望催更,我都既兴奋又诚惶诚恐。兴奋是因为有人能喜欢读我喜欢写的东西,惶恐是因为居然有人喜欢我写的东西。

+

实际在看似停更的这小半年来我并没有停,并且再坚持每日一更,只不过内容放在了另一个站点上,域名是 https://diary.jiapan.me/ 。从域名可以看出,这是我写日记的地方,站点标题叫「小小的避难所」,灵感来自毛姆写的《阅读是一座随身携带的避难所》这本书的书名,正如名字写的这样,我把那里作为我的避难所来记录、倾诉我的所感所想。当然那个站点上的内容也不是每天都会更新发布,而是根据我的心情,想起来了就整理一批我在Notion中写的内容发布出去。

+

进入避难所有一点小小的门槛,需要留下你的邮箱和阅读原因,邮箱只要是常见域名就可以,会收到一个入场验证码,输入验证码后再说明原因就可以进入了,原因我并不会审核,只要大于5个字符就可以了。

+

设置门槛的原因有两个,首先是我希望让这些内容可控,我需要知道都被谁访问过,其次是我不希望这些内容会被爬虫抓到,或者说可以通过搜索引擎搜到。

+

为什么我把那些内容单独隔离到了另一个站点内,而没有放在这里,因为那些都是我的日常碎碎念、流水账,每一篇内容都写的很零散,每天晚上我会回顾一下今天值得纪念的事情。拿出几样来记录一下,没有任何主题。

+

这个博客内大部分内容都是围绕着一个主题来写的,但这种写法很费精力,而且实话实说我并没有那么多干货。当然我也知道写这种结构化的文章相比写流水账,对个人来说会有更好的提升,我想先通过记录流水账的方式把写作这个习惯培养起来,然后再慢慢进阶。

+

所以,如果想继续读我流水账的朋友可以左转进去我的小小避难所,但我也先在这里做个免责声明(狗头保命),那些内容确实不体系化,没有营养,没有干货,读后可能会让你大失所望。引用曹公的一句话:满纸荒唐言。

+

顺便说一句,昨天和老板提了离职,准备开启一段新的征程大海,去向暂时保密,等未来有了水花再回来聊一聊这段经历叭。

+ +
+ + + + + + + +
+ + + + + + +
+ +
+ + + +
+ + + + + + + + +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2024/gong-ye-chang/index.html b/2024/gong-ye-chang/index.html new file mode 100644 index 0000000000..a50831b65c --- /dev/null +++ b/2024/gong-ye-chang/index.html @@ -0,0 +1,533 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 《论语》公冶长篇中孔子点评过的弟子 | 贾攀的流水账 + + + + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

贾攀的流水账

+ +
+

Panmax's Blog

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+ + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ 《论语》公冶长篇中孔子点评过的弟子 +

+ + +
+ + + + +
+ + +

公冶长,字子芝

公冶长一生治学,德才兼备,虽然做过牢,可孔子不觉得他有罪,就把女儿嫁给了他;

+
+

子谓公冶长:“可妻也,虽在缧绁之中,非其罪也!”以其子妻之。

+
+

南宫括,字子容

南宫适知晓分寸,进退有度,好社会能干、坏社会能自保,孔子把侄女嫁给了他;

+
+

子谓南容:“邦有道不废;邦无道免于刑戮。”以其兄之子妻之。

+
+

宓不齐,字子贱

宓子贱仁德好学,刚正不阿;

+
+

子谓子贱:“君子哉若人!鲁无君子者,斯焉取斯?”

+
+

端木赐,字子贡

子贡心高气傲,有点喜欢自我标榜,孔子总是苦口婆心地对他旁敲侧击;

+
+

子贡问曰:“赐也何如?”子曰:“女,器也。”曰:“何器也?”曰:“瑚琏也。”
子贡曰:“我不欲人之加诸我也,吾亦欲无加诸人。”子曰:“赐也,非尔所及也。”

+
+

冉雍,字仲弓

冉雍朴素踏实、勇于实干;

+
+

或曰:“雍也仁而不佞。”子曰:“焉用佞?御人以口给,屡憎于人。不知其仁,焉用佞?”

+
+

漆雕开,字子开

漆雕开稳重谦虚,志向高远但也沉得住气;

+
+

子使漆雕开仕,对曰:“吾斯之未能信。”子说。

+
+

仲由,字子路

子路是个急性子,自视甚高,直来直去,有什么说什么;

+
+

子曰:“道不行,乘桴浮于海,从我者其由与?”子路闻之喜,子曰:“由也好勇过我,无所取材。”
子路有闻,未之能行,唯恐有闻。

+
+

公西赤,字子华

公西赤善于外交,口才一流;

+
+

“赤也何如?”子曰:“赤也,束带立于朝,可使与宾客言也。不知其仁也。”

+
+

冉求,字子有

冉求多才多艺,特别会管钱,但是因为帮季氏敛财,受到孔子的严厉批评,后来跟孔子学习之后逐渐成为仁德之人;

+
+

“求也何如?”子曰:“求也,千室之邑,百乘之家,可使为之宰也,不知其仁也。”、

+
+

宰予,字子我

宰我调皮捣蛋,能言善辩,他最出名的事就是白天睡觉被老师骂;

+
+

宰予昼寝,子曰:“朽木不可雕也,粪土之墙不可杇也,于予与何诛?”子曰:“始吾于人也,听其言而信其行;今吾于人也,听其言而观其行。于予与改是。”

+
+

申枨(申党),字周

申枨精通六艺,但欲望比较强,孔子觉得他还没培养出刚健的气质。

+
+

子曰:“吾未见刚者。”或对曰:“申枨。”子曰:“枨也欲,焉得刚。”

+
+

参考:

+ + +
+ + + + + + + +
+ + + + + + +
+ +
+ + + +
+ + + + + + + + +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2024/springboot-azure-openai/index.html b/2024/springboot-azure-openai/index.html new file mode 100644 index 0000000000..b48afcc1ae --- /dev/null +++ b/2024/springboot-azure-openai/index.html @@ -0,0 +1,525 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 解决低版本SpringBoot使用langchain4j Azure 冲突问题 | 贾攀的流水账 + + + + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

贾攀的流水账

+ +
+

Panmax's Blog

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+ + +
+
+
+ + +
+ + + + + +
+ + + + + +
+

+ 解决低版本SpringBoot使用langchain4j Azure 冲突问题 +

+ + +
+ + + + +
+ + +

新公司使用的Java技术栈,我们有部分新业务需要调用 OpenAI 的接口进行交互,之前我找了一个比较轻量的SDK来调用OpenAI的接口,地址是:https://github.com/Lambdua/openai4j ,这个库作为日常使用足够了,但是一些高阶能力无法满足,而这些也是我们未来会用到的,比如:

+
    +
  • 对接微软 Azure 上部署的 GPT 模型
  • +
  • Function Calling
  • +
  • RAG
  • +
+

把第一版功能完成后,这几天工作不是那么多,于是我从Github上找到了这个库https://github.com/langchain4j/langchain4j ,从名字就能看出来,这个项目是参考的 Python 的LangChain,Java 库的命名很有意思,很喜欢叫 xxxx4j,4j 的意思是 for Java,比如 log4j。

+

我大致看了一下介绍,功能还算完备,给出的demo来看使用方式上可读性也很高,更重要的一点是支持古老的Java8。于是我在项目中进行了引入,将已有代码进行了改造,在跑直接调用 OpenAI 的例子时很顺利,当我切换为 Azure 后问题出现了,报错堆栈如下:

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
Exception in thread "main" java.lang.NoClassDefFoundError: reactor/core/Disposable
at java.lang.ClassLoader.defineClass1(Native Method)
at java.lang.ClassLoader.defineClass(ClassLoader.java:756)
at java.security.SecureClassLoader.defineClass(SecureClassLoader.java:142)
at java.net.URLClassLoader.defineClass(URLClassLoader.java:473)
at java.net.URLClassLoader.access$100(URLClassLoader.java:74)
at java.net.URLClassLoader$1.run(URLClassLoader.java:369)
at java.net.URLClassLoader$1.run(URLClassLoader.java:363)
at java.security.AccessController.doPrivileged(Native Method)
at java.net.URLClassLoader.findClass(URLClassLoader.java:362)
at java.lang.ClassLoader.loadClass(ClassLoader.java:418)
at sun.misc.Launcher$AppClassLoader.loadClass(Launcher.java:352)
at java.lang.ClassLoader.loadClass(ClassLoader.java:351)
at com.azure.core.http.netty.NettyAsyncHttpClientProvider.createInstance(NettyAsyncHttpClientProvider.java:81)
at dev.langchain4j.model.azure.InternalAzureOpenAiHelper.setupOpenAIClientBuilder(InternalAzureOpenAiHelper.java:71)
at dev.langchain4j.model.azure.InternalAzureOpenAiHelper.setupSyncClient(InternalAzureOpenAiHelper.java:51)
at dev.langchain4j.model.azure.AzureOpenAiChatModel.<init>(AzureOpenAiChatModel.java:123)
at dev.langchain4j.model.azure.AzureOpenAiChatModel$Builder.build(AzureOpenAiChatModel.java:536)
+

我按照堆栈的引导,一步一步去看代码,发现是在创建 HttpClient 对象时挂了,我进到 ConnectionProvider 源码中查看,确实找不到上边说的 Disposable 类,这个类来自 reactor-core 包。通过IDE跳转进的路径看到,目前项目中所使用的 reactor-core 版本是 2.0.8.RELEASE,我找到最新 3.6.7 版本的 reactor-core 源码看了下是有Disposable 这个类的。

+

一开始我认为是 langchain4j 的这个项目有问题,去 Github 的 Issue 中搜了下并没有相关的提问,于是我自己开始尝试动手解决,尝试了以下几种方式都不行:

+
    +
  1. 直接在项目中引入最新版本的 reactor-core
  2. +
  3. 排除(exclusions) langchain4j-azure-open-ai 下的 reactor-core 依赖,保证我自己引入的最新版本生效
  4. +
  5. 引入 reactor-netty-core 的最新版
  6. +
  7. 引入全部 langchain4j 的依赖
  8. +
  9. 重启IDE
  10. +
  11. 重启电脑
  12. +
+

在做上边的第2步时,启动调试后可以看到,IDE在进入ConnectionProvider 后确实可以正常跳转进Disposable 了,但最终还是报错。通过依赖分析也没有发现和 reactor 的任何冲突,一直搞到晚上下班也没解决。

+

今天早上上班后我换了个思路来排查这个项目,创建了一个新项目,只引入 langchain4j 的依赖,可以正常执行,接下来我把我们项目中其他依赖项引进来,发现还是没问题,当我把 parent 引入后问题出现了。虽然 parent 的 pom 文件在远端,但IDEA提供了一个功能,可以修改本地的文件来进行调试,我用二分法删除 parent 中的依赖,最终将问题定位在了:

+
1
2
3
4
5
6
7
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>${spring.boot.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
+

parent 中 spring.boot.version 的值是 1.5.7.RELEASE ,我在上上家公司写Java时就有这个版本了,是个非常老的版本,但升级 SpringBoot 关联的问题会更多。我继续深入进去看,在 spring-boot-dependencies 的 pom 文件中 properties 指定了reactor.version2.0.8.RELEASE,这下破案了。之前我无法通过依赖分析找到冲突,也是因为依赖是在 parent 指定的,且这个依赖版本无法在后续进行修改。

+

有种覆盖 parent 版本号的方式是在自己项目的父 pom 中的dependencyManagement 下进行声明,我尝试在 dependencyManagement 加上如下片段:

+
1
2
3
4
5
<dependency>
<groupId>io.projectreactor</groupId>
<artifactId>reactor-core</artifactId>
<version>3.6.7</version>
</dependency>
+

此时报了另一个错误:

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
java.lang.VerifyError: class io.netty.channel.kqueue.AbstractKQueueChannel$AbstractKQueueUnsafe overrides final method close.(Lio/netty/channel/ChannelPromise;)V

at java.lang.ClassLoader.defineClass1(Native Method)
at java.lang.ClassLoader.defineClass(ClassLoader.java:756)
at java.security.SecureClassLoader.defineClass(SecureClassLoader.java:142)
at java.net.URLClassLoader.defineClass(URLClassLoader.java:473)
at java.net.URLClassLoader.access$100(URLClassLoader.java:74)
at java.net.URLClassLoader$1.run(URLClassLoader.java:369)
at java.net.URLClassLoader$1.run(URLClassLoader.java:363)
at java.security.AccessController.doPrivileged(Native Method)
at java.net.URLClassLoader.findClass(URLClassLoader.java:362)
at java.lang.ClassLoader.loadClass(ClassLoader.java:418)
at sun.misc.Launcher$AppClassLoader.loadClass(Launcher.java:352)
at java.lang.ClassLoader.loadClass(ClassLoader.java:351)
at reactor.netty.resources.DefaultLoopKQueue.getChannel(DefaultLoopKQueue.java:50)
at reactor.netty.resources.LoopResources.onChannel(LoopResources.java:243)
at reactor.netty.tcp.TcpResources.onChannel(TcpResources.java:251)
at reactor.netty.transport.TransportConfig.lambda$connectionFactory$1(TransportConfig.java:277)
at reactor.netty.transport.TransportConnector.doInitAndRegister(TransportConnector.java:277)
at reactor.netty.transport.TransportConnector.connect(TransportConnector.java:164)
at reactor.netty.transport.TransportConnector.connect(TransportConnector.java:123)
at reactor.netty.resources.DefaultPooledConnectionProvider$PooledConnectionAllocator.lambda$connectChannel$0(DefaultPooledConnectionProvider.java:519)
at reactor.core.publisher.MonoCreate.subscribe(MonoCreate.java:61)
at reactor.core.publisher.Mono.subscribe(Mono.java:4568)
at reactor.core.publisher.Mono.subscribeWith(Mono.java:4634)
at reactor.core.publisher.Mono.subscribe(Mono.java:4534)
at reactor.core.publisher.Mono.subscribe(Mono.java:4470)
at reactor.netty.internal.shaded.reactor.pool.SimpleDequePool.drainLoop(SimpleDequePool.java:437)
at reactor.netty.internal.shaded.reactor.pool.SimpleDequePool.pendingOffer(SimpleDequePool.java:600)
at reactor.netty.internal.shaded.reactor.pool.SimpleDequePool.doAcquire(SimpleDequePool.java:296)
at reactor.netty.internal.shaded.reactor.pool.AbstractPool$Borrower.request(AbstractPool.java:430)
at reactor.netty.resources.DefaultPooledConnectionProvider$DisposableAcquire.onSubscribe(DefaultPooledConnectionProvider.java:204)
at reactor.netty.internal.shaded.reactor.pool.SimpleDequePool$QueueBorrowerMono.subscribe(SimpleDequePool.java:720)
at reactor.netty.resources.PooledConnectionProvider.lambda$acquire$2(PooledConnectionProvider.java:170)
at reactor.core.publisher.MonoCreate.subscribe(MonoCreate.java:61)
at reactor.netty.http.client.HttpClientConnect$MonoHttpConnect.lambda$subscribe$0(HttpClientConnect.java:273)
at reactor.core.publisher.MonoCreate.subscribe(MonoCreate.java:61)
at reactor.core.publisher.FluxRetryWhen.subscribe(FluxRetryWhen.java:81)
at reactor.core.publisher.MonoRetryWhen.subscribeOrReturn(MonoRetryWhen.java:46)
at reactor.core.publisher.InternalMonoOperator.subscribe(InternalMonoOperator.java:63)
at reactor.netty.http.client.HttpClientConnect$MonoHttpConnect.subscribe(HttpClientConnect.java:276)
at reactor.core.publisher.Mono.subscribe(Mono.java:4568)
at reactor.core.publisher.Mono.block(Mono.java:1778)
at com.azure.core.http.netty.NettyAsyncHttpClient.sendSync(NettyAsyncHttpClient.java:199)
at com.azure.core.http.HttpPipelineNextSyncPolicy.processSync(HttpPipelineNextSyncPolicy.java:51)
at com.azure.core.http.policy.HttpLoggingPolicy.processSync(HttpLoggingPolicy.java:183)
at com.azure.core.http.HttpPipelineNextSyncPolicy.processSync(HttpPipelineNextSyncPolicy.java:53)
at com.azure.core.implementation.http.policy.InstrumentationPolicy.processSync(InstrumentationPolicy.java:101)
at com.azure.core.http.HttpPipelineNextSyncPolicy.processSync(HttpPipelineNextSyncPolicy.java:53)
at com.azure.core.http.policy.KeyCredentialPolicy.processSync(KeyCredentialPolicy.java:115)
at com.azure.core.http.HttpPipelineNextSyncPolicy.processSync(HttpPipelineNextSyncPolicy.java:53)
at com.azure.core.http.policy.CookiePolicy.processSync(CookiePolicy.java:73)
at com.azure.core.http.HttpPipelineNextSyncPolicy.processSync(HttpPipelineNextSyncPolicy.java:53)
at com.azure.core.http.policy.AddDatePolicy.processSync(AddDatePolicy.java:50)
at com.azure.core.http.HttpPipelineNextSyncPolicy.processSync(HttpPipelineNextSyncPolicy.java:53)
at com.azure.core.http.policy.RetryPolicy.attemptSync(RetryPolicy.java:211)
at com.azure.core.http.policy.RetryPolicy.processSync(RetryPolicy.java:161)
at com.azure.core.http.HttpPipelineNextSyncPolicy.processSync(HttpPipelineNextSyncPolicy.java:53)
at com.azure.core.http.policy.AddHeadersPolicy.processSync(AddHeadersPolicy.java:66)
at com.azure.core.http.HttpPipelineNextSyncPolicy.processSync(HttpPipelineNextSyncPolicy.java:53)
at com.azure.core.http.policy.AddHeadersFromContextPolicy.processSync(AddHeadersFromContextPolicy.java:67)
at com.azure.core.http.HttpPipelineNextSyncPolicy.processSync(HttpPipelineNextSyncPolicy.java:53)
at com.azure.core.http.policy.RequestIdPolicy.processSync(RequestIdPolicy.java:77)
at com.azure.core.http.HttpPipelineNextSyncPolicy.processSync(HttpPipelineNextSyncPolicy.java:53)
at com.azure.core.http.policy.HttpPipelineSyncPolicy.processSync(HttpPipelineSyncPolicy.java:51)
at com.azure.core.http.policy.UserAgentPolicy.processSync(UserAgentPolicy.java:174)
at com.azure.core.http.HttpPipelineNextSyncPolicy.processSync(HttpPipelineNextSyncPolicy.java:53)
at com.azure.core.http.HttpPipeline.sendSync(HttpPipeline.java:138)
at com.azure.core.implementation.http.rest.SyncRestProxy.send(SyncRestProxy.java:62)
at com.azure.core.implementation.http.rest.SyncRestProxy.invoke(SyncRestProxy.java:83)
at com.azure.core.implementation.http.rest.RestProxyBase.invoke(RestProxyBase.java:124)
at com.azure.core.http.rest.RestProxy.invoke(RestProxy.java:95)
at com.sun.proxy.$Proxy24.getChatCompletionsSync(Unknown Source)
at com.azure.ai.openai.implementation.OpenAIClientImpl.getChatCompletionsWithResponse(OpenAIClientImpl.java:1444)
at com.azure.ai.openai.OpenAIClient.getChatCompletionsWithResponse(OpenAIClient.java:318)
at com.azure.ai.openai.OpenAIClient.getChatCompletions(OpenAIClient.java:685)
at dev.langchain4j.model.azure.AzureOpenAiChatModel.generate(AzureOpenAiChatModel.java:257)
at dev.langchain4j.model.azure.AzureOpenAiChatModel.generate(AzureOpenAiChatModel.java:215)
+

回到最开始的问题,报错误的根本原因是,初始化 Azure模型时需要构造一个 HttpClient,默认情况下会使用ConnectionProvider 来构造。看了下 AzureOpenAiChatModel 的 builder 方法,支持自己传入 OpenAIClient,而 OpenAIClient 可以自己构造 HttpClient,通过这个文档看到 https://learn.microsoft.com/en-us/azure/developer/java/sdk/http-client-pipeline HttpClient 有多种实现,其中可以用 OkHttpClient 来实现,于是我进行了以下魔改:

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
private static OpenAIClient setupSyncClient(String endpoint, String serviceVersion, Object credential, Duration timeout, Integer maxRetries, ProxyOptions proxyOptions, boolean logRequestsAndResponses) {
OpenAIClientBuilder openAIClientBuilder = setupOpenAIClientBuilder(endpoint, serviceVersion, credential, timeout, maxRetries, proxyOptions, logRequestsAndResponses);
return openAIClientBuilder.buildClient();
}

private static OpenAIClientBuilder setupOpenAIClientBuilder(String endpoint, String serviceVersion, Object credential, Duration timeout, Integer maxRetries, ProxyOptions proxyOptions, boolean logRequestsAndResponses) {
timeout = getOrDefault(timeout, ofSeconds(60));
HttpClientOptions clientOptions = new HttpClientOptions();
clientOptions.setConnectTimeout(timeout);
clientOptions.setResponseTimeout(timeout);
clientOptions.setReadTimeout(timeout);
clientOptions.setWriteTimeout(timeout);
clientOptions.setProxyOptions(proxyOptions);

Header header = new Header("User-Agent", "langchain4j-azure-openai");
clientOptions.setHeaders(Collections.singletonList(header));
// HttpClient httpClient = new NettyAsyncHttpClientProvider().createInstance(clientOptions);
HttpClient httpClient = new OkHttpAsyncClientProvider().createInstance(clientOptions);

HttpLogOptions httpLogOptions = new HttpLogOptions();
if (logRequestsAndResponses) {
httpLogOptions.setLogLevel(HttpLogDetailLevel.BODY_AND_HEADERS);
}

maxRetries = getOrDefault(maxRetries, 3);
ExponentialBackoffOptions exponentialBackoffOptions = new ExponentialBackoffOptions();
exponentialBackoffOptions.setMaxRetries(maxRetries);
RetryOptions retryOptions = new RetryOptions(exponentialBackoffOptions);

OpenAIClientBuilder openAIClientBuilder = new OpenAIClientBuilder()
.endpoint(ensureNotBlank(endpoint, "endpoint"))
.serviceVersion(getOpenAIServiceVersion(serviceVersion))
.httpClient(httpClient)
.clientOptions(clientOptions)
.httpLogOptions(httpLogOptions)
.retryOptions(retryOptions);

if (credential instanceof String) {
openAIClientBuilder.credential(new AzureKeyCredential((String) credential));
} else if (credential instanceof KeyCredential) {
openAIClientBuilder.credential((KeyCredential) credential);
} else if (credential instanceof TokenCredential) {
openAIClientBuilder.credential((TokenCredential) credential);
} else {
throw new IllegalArgumentException("Unsupported credential type: " + credential.getClass());
}

return openAIClientBuilder;
}

private static OpenAIServiceVersion getOpenAIServiceVersion(String serviceVersion) {
for (OpenAIServiceVersion version : OpenAIServiceVersion.values()) {
if (version.getVersion().equals(serviceVersion)) {
return version;
}
}
return OpenAIServiceVersion.getLatest();
}
+

从开源代码中拷贝出 setupSyncClientsetupOpenAIClientBuilder 方法,并对setupOpenAIClientBuilder 中的HttpClient httpClient 的创建逻辑进行了调整

+
1
2
3
4
// before
HttpClient httpClient = new NettyAsyncHttpClientProvider().createInstance(clientOptions);
// after
HttpClient httpClient = new OkHttpAsyncClientProvider().createInstance(clientOptions);
+

初始化Azure模型时传入我自己的 client:

+
1
2
3
4
5
6
7
8
9
// 默认生成的client使用NettyAsyncHttpClientProvider和SpringBoot所依赖的版本不兼容,改用OkHttpAsyncClientProvider进行重写
OpenAIClient client = setupSyncClient(System.getenv("AZURE_OPENAI_ENDPOINT"), "",
System.getenv("AZURE_OPENAI_API_KEY"), ofSeconds(30), 2, null, true);

model = AzureOpenAiChatModel.builder()
.openAIClient(client)
.deploymentName(modelName)
.temperature(0.0)
.build();
+

并在工程中引入 azure-core-http-okhttp 的依赖

+
1
2
3
4
5
<dependency>
<groupId>com.azure</groupId>
<artifactId>azure-core-http-okhttp</artifactId>
<version>1.12.0</version>
</dependency>
+

再次执行还是报错了,不过这次的错误变为:

+
1
2
3
4
5
6
7
java.lang.NoClassDefFoundError: reactor/util/context/ContextView

at com.azure.core.http.rest.RestProxy.<init>(RestProxy.java:56)
at com.azure.core.http.rest.RestProxy.create(RestProxy.java:140)
at com.azure.ai.openai.implementation.OpenAIClientImpl.<init>(OpenAIClientImpl.java:144)
at com.azure.ai.openai.OpenAIClientBuilder.buildInnerClient(OpenAIClientBuilder.java:283)
at com.azure.ai.openai.OpenAIClientBuilder.buildClient(OpenAIClientBuilder.java:351)
+

还是 reactor 的问题,但可以看到,现在已经不再使用 reactor.core.Disposable 了,也许升级一下 reactor-core 可以解决,我再次在项目的 parent 的dependencyManagement 下引入

+
1
2
3
4
5
<dependency>
<groupId>io.projectreactor</groupId>
<artifactId>reactor-core</artifactId>
<version>3.6.7</version>
</dependency>
+

再次尝试,问题解决。

+ +
+ + + + + + + +
+ + + + + + +
+ +
+ + + +
+ + + + + + + + +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/CNAME b/CNAME new file mode 100644 index 0000000000..39487e7208 --- /dev/null +++ b/CNAME @@ -0,0 +1 @@ +jiapan.me diff --git a/IMG_7996.JPG b/IMG_7996.JPG new file mode 100644 index 0000000000..05efd6700d Binary files /dev/null and b/IMG_7996.JPG differ diff --git a/about/index.html b/about/index.html new file mode 100644 index 0000000000..27632e775d --- /dev/null +++ b/about/index.html @@ -0,0 +1,449 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 关于我 | 贾攀的流水账 + + + + + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

贾攀的流水账

+ +
+

Panmax's Blog

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+ + +
+
+
+ + + + +
+ + + + + +
+
+ +

关于我 +

+ + + +
+ + + + +
+

贾攀 / 92年

+ + +

Currently working on 探探科技🦊

+

BackendEngineer / FullStack

+

Language: Golang / Python / Java

+

Github:https://github.com/Panmax

+

Mail: hi@jiapan.me

+

好读书,不求甚解:我的书单

+

我的作品:

+ +
+ + + +
+ + + + + + + +
+ +
+ + + +
+ + + + + + + + +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/about/index/1.jpeg b/about/index/1.jpeg new file mode 100644 index 0000000000..057815a61e Binary files /dev/null and b/about/index/1.jpeg differ diff --git a/ads.txt b/ads.txt new file mode 100644 index 0000000000..62070e8c7e --- /dev/null +++ b/ads.txt @@ -0,0 +1 @@ +google.com, pub-7771759338768779, DIRECT, f08c47fec0942fa0 \ No newline at end of file diff --git a/archives/2015/10/index.html b/archives/2015/10/index.html new file mode 100644 index 0000000000..be087d0e2d --- /dev/null +++ b/archives/2015/10/index.html @@ -0,0 +1,542 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Archive | 贾攀的流水账 + + + + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

贾攀的流水账

+ +
+

Panmax's Blog

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+ + +
+
+
+ + +
+ + + + + +
+
+
+ + Excellent! 357 posts in total. Keep on posting. +
+ + +
+ 2015 +
+ + + + + + + + + + + + + + +
+
+ + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/archives/2015/12/index.html b/archives/2015/12/index.html new file mode 100644 index 0000000000..2598fdb611 --- /dev/null +++ b/archives/2015/12/index.html @@ -0,0 +1,562 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Archive | 贾攀的流水账 + + + + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

贾攀的流水账

+ +
+

Panmax's Blog

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+ + +
+
+
+ + +
+ + + + + +
+
+
+ + Excellent! 357 posts in total. Keep on posting. +
+ + +
+ 2015 +
+ + + + + + + + + + + + + + + + +
+
+ + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/archives/2015/index.html b/archives/2015/index.html new file mode 100644 index 0000000000..a8ed193937 --- /dev/null +++ b/archives/2015/index.html @@ -0,0 +1,682 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Archive | 贾攀的流水账 + + + + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

贾攀的流水账

+ +
+

Panmax's Blog

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+ + +
+
+
+ + +
+ + + + + +
+
+
+ + Excellent! 357 posts in total. Keep on posting. +
+ + +
+ 2015 +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+ + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/archives/2016/01/index.html b/archives/2016/01/index.html new file mode 100644 index 0000000000..9dcb2d4d8a --- /dev/null +++ b/archives/2016/01/index.html @@ -0,0 +1,462 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Archive | 贾攀的流水账 + + + + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

贾攀的流水账

+ +
+

Panmax's Blog

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+ + +
+
+
+ + +
+ + + + + +
+
+
+ + Excellent! 357 posts in total. Keep on posting. +
+ + +
+ 2016 +
+ + + + + + +
+
+ + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/archives/2016/02/index.html b/archives/2016/02/index.html new file mode 100644 index 0000000000..432f951995 --- /dev/null +++ b/archives/2016/02/index.html @@ -0,0 +1,642 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Archive | 贾攀的流水账 + + + + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

贾攀的流水账

+ +
+

Panmax's Blog

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+ + +
+
+
+ + +
+ + + + + +
+
+
+ + Excellent! 357 posts in total. Keep on posting. +
+ + +
+ 2016 +
+ + + + + + + + + + + + + + + + + + + + + + + + +
+
+ + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/archives/2016/03/index.html b/archives/2016/03/index.html new file mode 100644 index 0000000000..8aa6d19071 --- /dev/null +++ b/archives/2016/03/index.html @@ -0,0 +1,642 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Archive | 贾攀的流水账 + + + + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

贾攀的流水账

+ +
+

Panmax's Blog

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+ + +
+
+
+ + +
+ + + + + +
+
+
+ + Excellent! 357 posts in total. Keep on posting. +
+ + +
+ 2016 +
+ + + + + + + + + + + + + + + + + + + + + + + + +
+
+ + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/archives/2016/04/index.html b/archives/2016/04/index.html new file mode 100644 index 0000000000..5dc1726e9b --- /dev/null +++ b/archives/2016/04/index.html @@ -0,0 +1,462 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Archive | 贾攀的流水账 + + + + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

贾攀的流水账

+ +
+

Panmax's Blog

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+ + +
+
+
+ + +
+ + + + + +
+
+
+ + Excellent! 357 posts in total. Keep on posting. +
+ + +
+ 2016 +
+ + + + + + +
+
+ + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/archives/2016/07/index.html b/archives/2016/07/index.html new file mode 100644 index 0000000000..f39f559a80 --- /dev/null +++ b/archives/2016/07/index.html @@ -0,0 +1,462 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Archive | 贾攀的流水账 + + + + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

贾攀的流水账

+ +
+

Panmax's Blog

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+ + +
+
+
+ + +
+ + + + + +
+
+
+ + Excellent! 357 posts in total. Keep on posting. +
+ + +
+ 2016 +
+ + + + + + +
+
+ + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/archives/2016/08/index.html b/archives/2016/08/index.html new file mode 100644 index 0000000000..e2ab16ac8f --- /dev/null +++ b/archives/2016/08/index.html @@ -0,0 +1,442 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Archive | 贾攀的流水账 + + + + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

贾攀的流水账

+ +
+

Panmax's Blog

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+ + +
+
+
+ + +
+ + + + + +
+
+
+ + Excellent! 357 posts in total. Keep on posting. +
+ + +
+ 2016 +
+ + + + +
+
+ + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/archives/2016/10/index.html b/archives/2016/10/index.html new file mode 100644 index 0000000000..5bff11757e --- /dev/null +++ b/archives/2016/10/index.html @@ -0,0 +1,442 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Archive | 贾攀的流水账 + + + + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

贾攀的流水账

+ +
+

Panmax's Blog

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+ + +
+
+
+ + +
+ + + + + +
+
+
+ + Excellent! 357 posts in total. Keep on posting. +
+ + +
+ 2016 +
+ + + + +
+
+ + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/archives/2016/index.html b/archives/2016/index.html new file mode 100644 index 0000000000..c300f62337 --- /dev/null +++ b/archives/2016/index.html @@ -0,0 +1,825 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Archive | 贾攀的流水账 + + + + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

贾攀的流水账

+ +
+

Panmax's Blog

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+ + +
+
+
+ + +
+ + + + + +
+
+
+ + Excellent! 357 posts in total. Keep on posting. +
+ + +
+ 2016 +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+ + + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/archives/2016/page/2/index.html b/archives/2016/page/2/index.html new file mode 100644 index 0000000000..da4103c998 --- /dev/null +++ b/archives/2016/page/2/index.html @@ -0,0 +1,625 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Archive | 贾攀的流水账 + + + + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

贾攀的流水账

+ +
+

Panmax's Blog

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+ + +
+
+
+ + +
+ + + + + +
+
+
+ + Excellent! 357 posts in total. Keep on posting. +
+ + +
+ 2016 +
+ + + + + + + + + + + + + + + + + + + + + + +
+
+ + + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/archives/2017/01/index.html b/archives/2017/01/index.html new file mode 100644 index 0000000000..b182ba15c3 --- /dev/null +++ b/archives/2017/01/index.html @@ -0,0 +1,442 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Archive | 贾攀的流水账 + + + + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

贾攀的流水账

+ +
+

Panmax's Blog

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+ + +
+
+
+ + +
+ + + + + +
+
+
+ + Excellent! 357 posts in total. Keep on posting. +
+ + +
+ 2017 +
+ + + + +
+
+ + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/archives/2017/02/index.html b/archives/2017/02/index.html new file mode 100644 index 0000000000..2cee6f4dfb --- /dev/null +++ b/archives/2017/02/index.html @@ -0,0 +1,442 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Archive | 贾攀的流水账 + + + + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

贾攀的流水账

+ +
+

Panmax's Blog

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+ + +
+
+
+ + +
+ + + + + +
+
+
+ + Excellent! 357 posts in total. Keep on posting. +
+ + +
+ 2017 +
+ + + + +
+
+ + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/archives/2017/04/index.html b/archives/2017/04/index.html new file mode 100644 index 0000000000..9e55a1a15d --- /dev/null +++ b/archives/2017/04/index.html @@ -0,0 +1,462 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Archive | 贾攀的流水账 + + + + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

贾攀的流水账

+ +
+

Panmax's Blog

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+ + +
+
+
+ + +
+ + + + + +
+
+
+ + Excellent! 357 posts in total. Keep on posting. +
+ + +
+ 2017 +
+ + + + + + +
+
+ + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/archives/2017/05/index.html b/archives/2017/05/index.html new file mode 100644 index 0000000000..2766002577 --- /dev/null +++ b/archives/2017/05/index.html @@ -0,0 +1,562 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Archive | 贾攀的流水账 + + + + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

贾攀的流水账

+ +
+

Panmax's Blog

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+ + +
+
+
+ + +
+ + + + + +
+
+
+ + Excellent! 357 posts in total. Keep on posting. +
+ + +
+ 2017 +
+ + + + + + + + + + + + + + + + +
+
+ + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/archives/2017/06/index.html b/archives/2017/06/index.html new file mode 100644 index 0000000000..1642a4e784 --- /dev/null +++ b/archives/2017/06/index.html @@ -0,0 +1,562 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Archive | 贾攀的流水账 + + + + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

贾攀的流水账

+ +
+

Panmax's Blog

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+ + +
+
+
+ + +
+ + + + + +
+
+
+ + Excellent! 357 posts in total. Keep on posting. +
+ + +
+ 2017 +
+ + + + + + + + + + + + + + + + +
+
+ + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/archives/2017/07/index.html b/archives/2017/07/index.html new file mode 100644 index 0000000000..09881a2958 --- /dev/null +++ b/archives/2017/07/index.html @@ -0,0 +1,622 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Archive | 贾攀的流水账 + + + + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

贾攀的流水账

+ +
+

Panmax's Blog

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+ + +
+
+
+ + +
+ + + + + +
+
+
+ + Excellent! 357 posts in total. Keep on posting. +
+ + +
+ 2017 +
+ + + + + + + + + + + + + + + + + + + + + + +
+
+ + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/archives/2017/08/index.html b/archives/2017/08/index.html new file mode 100644 index 0000000000..405e4117c7 --- /dev/null +++ b/archives/2017/08/index.html @@ -0,0 +1,602 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Archive | 贾攀的流水账 + + + + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

贾攀的流水账

+ +
+

Panmax's Blog

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+ + +
+
+
+ + +
+ + + + + +
+
+
+ + Excellent! 357 posts in total. Keep on posting. +
+ + +
+ 2017 +
+ + + + + + + + + + + + + + + + + + + + +
+
+ + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/archives/2017/09/index.html b/archives/2017/09/index.html new file mode 100644 index 0000000000..8f139887ef --- /dev/null +++ b/archives/2017/09/index.html @@ -0,0 +1,502 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Archive | 贾攀的流水账 + + + + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

贾攀的流水账

+ +
+

Panmax's Blog

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+ + +
+
+
+ + +
+ + + + + +
+
+
+ + Excellent! 357 posts in total. Keep on posting. +
+ + +
+ 2017 +
+ + + + + + + + + + +
+
+ + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/archives/2017/12/index.html b/archives/2017/12/index.html new file mode 100644 index 0000000000..16bf2f487f --- /dev/null +++ b/archives/2017/12/index.html @@ -0,0 +1,502 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Archive | 贾攀的流水账 + + + + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

贾攀的流水账

+ +
+

Panmax's Blog

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+ + +
+
+
+ + +
+ + + + + +
+
+
+ + Excellent! 357 posts in total. Keep on posting. +
+ + +
+ 2017 +
+ + + + + + + + + + +
+
+ + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/archives/2017/index.html b/archives/2017/index.html new file mode 100644 index 0000000000..e5645fa779 --- /dev/null +++ b/archives/2017/index.html @@ -0,0 +1,825 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Archive | 贾攀的流水账 + + + + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

贾攀的流水账

+ +
+

Panmax's Blog

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+ + +
+
+
+ + +
+ + + + + +
+
+
+ + Excellent! 357 posts in total. Keep on posting. +
+ + +
+ 2017 +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+ + + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/archives/2017/page/2/index.html b/archives/2017/page/2/index.html new file mode 100644 index 0000000000..30e97ddaae --- /dev/null +++ b/archives/2017/page/2/index.html @@ -0,0 +1,825 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Archive | 贾攀的流水账 + + + + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

贾攀的流水账

+ +
+

Panmax's Blog

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+ + +
+
+
+ + +
+ + + + + +
+
+
+ + Excellent! 357 posts in total. Keep on posting. +
+ + +
+ 2017 +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+ + + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/archives/2017/page/3/index.html b/archives/2017/page/3/index.html new file mode 100644 index 0000000000..bd50b59e9e --- /dev/null +++ b/archives/2017/page/3/index.html @@ -0,0 +1,525 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Archive | 贾攀的流水账 + + + + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

贾攀的流水账

+ +
+

Panmax's Blog

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+ + +
+
+
+ + +
+ + + + + +
+
+
+ + Excellent! 357 posts in total. Keep on posting. +
+ + +
+ 2017 +
+ + + + + + + + + + + + +
+
+ + + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/archives/2018/01/index.html b/archives/2018/01/index.html new file mode 100644 index 0000000000..7f829ad72e --- /dev/null +++ b/archives/2018/01/index.html @@ -0,0 +1,602 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Archive | 贾攀的流水账 + + + + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

贾攀的流水账

+ +
+

Panmax's Blog

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+ + +
+
+
+ + +
+ + + + + +
+
+
+ + Excellent! 357 posts in total. Keep on posting. +
+ + +
+ 2018 +
+ + + + + + + + + + + + + + + + + + + + +
+
+ + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/archives/2018/02/index.html b/archives/2018/02/index.html new file mode 100644 index 0000000000..edd86f5743 --- /dev/null +++ b/archives/2018/02/index.html @@ -0,0 +1,522 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Archive | 贾攀的流水账 + + + + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

贾攀的流水账

+ +
+

Panmax's Blog

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+ + +
+
+
+ + +
+ + + + + +
+
+
+ + Excellent! 357 posts in total. Keep on posting. +
+ + +
+ 2018 +
+ + + + + + + + + + + + +
+
+ + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/archives/2018/03/index.html b/archives/2018/03/index.html new file mode 100644 index 0000000000..de2c58afea --- /dev/null +++ b/archives/2018/03/index.html @@ -0,0 +1,642 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Archive | 贾攀的流水账 + + + + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

贾攀的流水账

+ +
+

Panmax's Blog

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+ + +
+
+
+ + +
+ + + + + +
+
+
+ + Excellent! 357 posts in total. Keep on posting. +
+ + +
+ 2018 +
+ + + + + + + + + + + + + + + + + + + + + + + + +
+
+ + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/archives/2018/04/index.html b/archives/2018/04/index.html new file mode 100644 index 0000000000..2f324a658a --- /dev/null +++ b/archives/2018/04/index.html @@ -0,0 +1,462 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Archive | 贾攀的流水账 + + + + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

贾攀的流水账

+ +
+

Panmax's Blog

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+ + +
+
+
+ + +
+ + + + + +
+
+
+ + Excellent! 357 posts in total. Keep on posting. +
+ + +
+ 2018 +
+ + + + + + +
+
+ + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/archives/2018/05/index.html b/archives/2018/05/index.html new file mode 100644 index 0000000000..978775bbae --- /dev/null +++ b/archives/2018/05/index.html @@ -0,0 +1,462 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Archive | 贾攀的流水账 + + + + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

贾攀的流水账

+ +
+

Panmax's Blog

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+ + +
+
+
+ + +
+ + + + + +
+
+
+ + Excellent! 357 posts in total. Keep on posting. +
+ + +
+ 2018 +
+ + + + + + +
+
+ + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/archives/2018/06/index.html b/archives/2018/06/index.html new file mode 100644 index 0000000000..3202b27f76 --- /dev/null +++ b/archives/2018/06/index.html @@ -0,0 +1,482 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Archive | 贾攀的流水账 + + + + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

贾攀的流水账

+ +
+

Panmax's Blog

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+ + +
+
+
+ + +
+ + + + + +
+
+
+ + Excellent! 357 posts in total. Keep on posting. +
+ + +
+ 2018 +
+ + + + + + + + +
+
+ + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/archives/2018/07/index.html b/archives/2018/07/index.html new file mode 100644 index 0000000000..99a4873c24 --- /dev/null +++ b/archives/2018/07/index.html @@ -0,0 +1,442 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Archive | 贾攀的流水账 + + + + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

贾攀的流水账

+ +
+

Panmax's Blog

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+ + +
+
+
+ + +
+ + + + + +
+
+
+ + Excellent! 357 posts in total. Keep on posting. +
+ + +
+ 2018 +
+ + + + +
+
+ + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/archives/2018/09/index.html b/archives/2018/09/index.html new file mode 100644 index 0000000000..5f2661914f --- /dev/null +++ b/archives/2018/09/index.html @@ -0,0 +1,462 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Archive | 贾攀的流水账 + + + + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

贾攀的流水账

+ +
+

Panmax's Blog

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+ + +
+
+
+ + +
+ + + + + +
+
+
+ + Excellent! 357 posts in total. Keep on posting. +
+ + +
+ 2018 +
+ + + + + + +
+
+ + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/archives/2018/11/index.html b/archives/2018/11/index.html new file mode 100644 index 0000000000..9ca95aace4 --- /dev/null +++ b/archives/2018/11/index.html @@ -0,0 +1,482 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Archive | 贾攀的流水账 + + + + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

贾攀的流水账

+ +
+

Panmax's Blog

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+ + +
+
+
+ + +
+ + + + + +
+
+
+ + Excellent! 357 posts in total. Keep on posting. +
+ + +
+ 2018 +
+ + + + + + + + +
+
+ + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/archives/2018/index.html b/archives/2018/index.html new file mode 100644 index 0000000000..d85af29a22 --- /dev/null +++ b/archives/2018/index.html @@ -0,0 +1,825 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Archive | 贾攀的流水账 + + + + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

贾攀的流水账

+ +
+

Panmax's Blog

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+ + +
+
+
+ + +
+ + + + + +
+
+
+ + Excellent! 357 posts in total. Keep on posting. +
+ + +
+ 2018 +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+ + + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/archives/2018/page/2/index.html b/archives/2018/page/2/index.html new file mode 100644 index 0000000000..648ad893e0 --- /dev/null +++ b/archives/2018/page/2/index.html @@ -0,0 +1,785 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Archive | 贾攀的流水账 + + + + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

贾攀的流水账

+ +
+

Panmax's Blog

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+ + +
+
+
+ + +
+ + + + + +
+
+
+ + Excellent! 357 posts in total. Keep on posting. +
+ + +
+ 2018 +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+ + + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/archives/2019/01/index.html b/archives/2019/01/index.html new file mode 100644 index 0000000000..bf17affe45 --- /dev/null +++ b/archives/2019/01/index.html @@ -0,0 +1,442 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Archive | 贾攀的流水账 + + + + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

贾攀的流水账

+ +
+

Panmax's Blog

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+ + +
+
+
+ + +
+ + + + + +
+
+
+ + Excellent! 357 posts in total. Keep on posting. +
+ + +
+ 2019 +
+ + + + +
+
+ + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/archives/2019/02/index.html b/archives/2019/02/index.html new file mode 100644 index 0000000000..12394f5c63 --- /dev/null +++ b/archives/2019/02/index.html @@ -0,0 +1,442 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Archive | 贾攀的流水账 + + + + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

贾攀的流水账

+ +
+

Panmax's Blog

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+ + +
+
+
+ + +
+ + + + + +
+
+
+ + Excellent! 357 posts in total. Keep on posting. +
+ + +
+ 2019 +
+ + + + +
+
+ + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/archives/2019/03/index.html b/archives/2019/03/index.html new file mode 100644 index 0000000000..288d368c47 --- /dev/null +++ b/archives/2019/03/index.html @@ -0,0 +1,482 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Archive | 贾攀的流水账 + + + + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

贾攀的流水账

+ +
+

Panmax's Blog

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+ + +
+
+
+ + +
+ + + + + +
+
+
+ + Excellent! 357 posts in total. Keep on posting. +
+ + +
+ 2019 +
+ + + + + + + + +
+
+ + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/archives/2019/04/index.html b/archives/2019/04/index.html new file mode 100644 index 0000000000..9f69c2c667 --- /dev/null +++ b/archives/2019/04/index.html @@ -0,0 +1,462 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Archive | 贾攀的流水账 + + + + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

贾攀的流水账

+ +
+

Panmax's Blog

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+ + +
+
+
+ + +
+ + + + + +
+
+
+ + Excellent! 357 posts in total. Keep on posting. +
+ + +
+ 2019 +
+ + + + + + +
+
+ + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/archives/2019/05/index.html b/archives/2019/05/index.html new file mode 100644 index 0000000000..ad67cbbca2 --- /dev/null +++ b/archives/2019/05/index.html @@ -0,0 +1,442 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Archive | 贾攀的流水账 + + + + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

贾攀的流水账

+ +
+

Panmax's Blog

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+ + +
+
+
+ + +
+ + + + + +
+
+
+ + Excellent! 357 posts in total. Keep on posting. +
+ + +
+ 2019 +
+ + + + +
+
+ + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/archives/2019/08/index.html b/archives/2019/08/index.html new file mode 100644 index 0000000000..1b6e7a83c0 --- /dev/null +++ b/archives/2019/08/index.html @@ -0,0 +1,442 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Archive | 贾攀的流水账 + + + + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

贾攀的流水账

+ +
+

Panmax's Blog

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+ + +
+
+
+ + +
+ + + + + +
+
+
+ + Excellent! 357 posts in total. Keep on posting. +
+ + +
+ 2019 +
+ + + + +
+
+ + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/archives/2019/09/index.html b/archives/2019/09/index.html new file mode 100644 index 0000000000..456082e55c --- /dev/null +++ b/archives/2019/09/index.html @@ -0,0 +1,442 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Archive | 贾攀的流水账 + + + + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

贾攀的流水账

+ +
+

Panmax's Blog

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+ + +
+
+
+ + +
+ + + + + +
+
+
+ + Excellent! 357 posts in total. Keep on posting. +
+ + +
+ 2019 +
+ + + + +
+
+ + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/archives/2019/10/index.html b/archives/2019/10/index.html new file mode 100644 index 0000000000..70437683be --- /dev/null +++ b/archives/2019/10/index.html @@ -0,0 +1,462 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Archive | 贾攀的流水账 + + + + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

贾攀的流水账

+ +
+

Panmax's Blog

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+ + +
+
+
+ + +
+ + + + + +
+
+
+ + Excellent! 357 posts in total. Keep on posting. +
+ + +
+ 2019 +
+ + + + + + +
+
+ + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/archives/2019/11/index.html b/archives/2019/11/index.html new file mode 100644 index 0000000000..905a32c12d --- /dev/null +++ b/archives/2019/11/index.html @@ -0,0 +1,662 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Archive | 贾攀的流水账 + + + + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

贾攀的流水账

+ +
+

Panmax's Blog

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+ + +
+
+
+ + +
+ + + + + +
+
+
+ + Excellent! 357 posts in total. Keep on posting. +
+ + +
+ 2019 +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+ + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/archives/2019/12/index.html b/archives/2019/12/index.html new file mode 100644 index 0000000000..fd050ec00f --- /dev/null +++ b/archives/2019/12/index.html @@ -0,0 +1,622 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Archive | 贾攀的流水账 + + + + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

贾攀的流水账

+ +
+

Panmax's Blog

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+ + +
+
+
+ + +
+ + + + + +
+
+
+ + Excellent! 357 posts in total. Keep on posting. +
+ + +
+ 2019 +
+ + + + + + + + + + + + + + + + + + + + + + +
+
+ + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/archives/2019/index.html b/archives/2019/index.html new file mode 100644 index 0000000000..f8bbe7452f --- /dev/null +++ b/archives/2019/index.html @@ -0,0 +1,825 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Archive | 贾攀的流水账 + + + + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

贾攀的流水账

+ +
+

Panmax's Blog

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+ + +
+
+
+ + +
+ + + + + +
+
+
+ + Excellent! 357 posts in total. Keep on posting. +
+ + +
+ 2019 +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+ + + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/archives/2019/page/2/index.html b/archives/2019/page/2/index.html new file mode 100644 index 0000000000..9dd16bff85 --- /dev/null +++ b/archives/2019/page/2/index.html @@ -0,0 +1,705 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Archive | 贾攀的流水账 + + + + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

贾攀的流水账

+ +
+

Panmax's Blog

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+ + +
+
+
+ + +
+ + + + + +
+
+
+ + Excellent! 357 posts in total. Keep on posting. +
+ + +
+ 2019 +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+ + + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/archives/2020/01/index.html b/archives/2020/01/index.html new file mode 100644 index 0000000000..1626a3e65c --- /dev/null +++ b/archives/2020/01/index.html @@ -0,0 +1,442 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Archive | 贾攀的流水账 + + + + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

贾攀的流水账

+ +
+

Panmax's Blog

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+ + +
+
+
+ + +
+ + + + + +
+
+
+ + Excellent! 357 posts in total. Keep on posting. +
+ + +
+ 2020 +
+ + + + +
+
+ + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/archives/2020/02/index.html b/archives/2020/02/index.html new file mode 100644 index 0000000000..688402c02e --- /dev/null +++ b/archives/2020/02/index.html @@ -0,0 +1,502 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Archive | 贾攀的流水账 + + + + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

贾攀的流水账

+ +
+

Panmax's Blog

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+ + +
+
+
+ + +
+ + + + + +
+
+
+ + Excellent! 357 posts in total. Keep on posting. +
+ + +
+ 2020 +
+ + + + + + + + + + +
+
+ + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/archives/2020/03/index.html b/archives/2020/03/index.html new file mode 100644 index 0000000000..95cc78bbba --- /dev/null +++ b/archives/2020/03/index.html @@ -0,0 +1,482 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Archive | 贾攀的流水账 + + + + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

贾攀的流水账

+ +
+

Panmax's Blog

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+ + +
+
+
+ + +
+ + + + + +
+
+
+ + Excellent! 357 posts in total. Keep on posting. +
+ + +
+ 2020 +
+ + + + + + + + +
+
+ + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/archives/2020/04/index.html b/archives/2020/04/index.html new file mode 100644 index 0000000000..26ed60dfdb --- /dev/null +++ b/archives/2020/04/index.html @@ -0,0 +1,462 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Archive | 贾攀的流水账 + + + + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

贾攀的流水账

+ +
+

Panmax's Blog

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+ + +
+
+
+ + +
+ + + + + +
+
+
+ + Excellent! 357 posts in total. Keep on posting. +
+ + +
+ 2020 +
+ + + + + + +
+
+ + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/archives/2020/05/index.html b/archives/2020/05/index.html new file mode 100644 index 0000000000..2d44b41097 --- /dev/null +++ b/archives/2020/05/index.html @@ -0,0 +1,442 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Archive | 贾攀的流水账 + + + + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

贾攀的流水账

+ +
+

Panmax's Blog

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+ + +
+
+
+ + +
+ + + + + +
+
+
+ + Excellent! 357 posts in total. Keep on posting. +
+ + +
+ 2020 +
+ + + + +
+
+ + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/archives/2020/06/index.html b/archives/2020/06/index.html new file mode 100644 index 0000000000..fb0141dbcd --- /dev/null +++ b/archives/2020/06/index.html @@ -0,0 +1,522 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Archive | 贾攀的流水账 + + + + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

贾攀的流水账

+ +
+

Panmax's Blog

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+ + +
+
+
+ + +
+ + + + + +
+
+
+ + Excellent! 357 posts in total. Keep on posting. +
+ + +
+ 2020 +
+ + + + + + + + + + + + +
+
+ + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/archives/2020/07/index.html b/archives/2020/07/index.html new file mode 100644 index 0000000000..abee063c0c --- /dev/null +++ b/archives/2020/07/index.html @@ -0,0 +1,542 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Archive | 贾攀的流水账 + + + + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

贾攀的流水账

+ +
+

Panmax's Blog

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+ + +
+
+
+ + +
+ + + + + +
+
+
+ + Excellent! 357 posts in total. Keep on posting. +
+ + +
+ 2020 +
+ + + + + + + + + + + + + + +
+
+ + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/archives/2020/08/index.html b/archives/2020/08/index.html new file mode 100644 index 0000000000..e116ffdfe4 --- /dev/null +++ b/archives/2020/08/index.html @@ -0,0 +1,442 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Archive | 贾攀的流水账 + + + + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

贾攀的流水账

+ +
+

Panmax's Blog

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+ + +
+
+
+ + +
+ + + + + +
+
+
+ + Excellent! 357 posts in total. Keep on posting. +
+ + +
+ 2020 +
+ + + + +
+
+ + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/archives/2020/09/index.html b/archives/2020/09/index.html new file mode 100644 index 0000000000..c785d2bb19 --- /dev/null +++ b/archives/2020/09/index.html @@ -0,0 +1,462 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Archive | 贾攀的流水账 + + + + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

贾攀的流水账

+ +
+

Panmax's Blog

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+ + +
+
+
+ + +
+ + + + + +
+
+
+ + Excellent! 357 posts in total. Keep on posting. +
+ + +
+ 2020 +
+ + + + + + +
+
+ + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/archives/2020/10/index.html b/archives/2020/10/index.html new file mode 100644 index 0000000000..7f8f32c1e6 --- /dev/null +++ b/archives/2020/10/index.html @@ -0,0 +1,482 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Archive | 贾攀的流水账 + + + + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

贾攀的流水账

+ +
+

Panmax's Blog

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+ + +
+
+
+ + +
+ + + + + +
+
+
+ + Excellent! 357 posts in total. Keep on posting. +
+ + +
+ 2020 +
+ + + + + + + + +
+
+ + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/archives/2020/11/index.html b/archives/2020/11/index.html new file mode 100644 index 0000000000..825f1130ee --- /dev/null +++ b/archives/2020/11/index.html @@ -0,0 +1,562 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Archive | 贾攀的流水账 + + + + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

贾攀的流水账

+ +
+

Panmax's Blog

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+ + +
+
+
+ + +
+ + + + + +
+
+
+ + Excellent! 357 posts in total. Keep on posting. +
+ + +
+ 2020 +
+ + + + + + + + + + + + + + + + +
+
+ + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/archives/2020/index.html b/archives/2020/index.html new file mode 100644 index 0000000000..ce8a375ab3 --- /dev/null +++ b/archives/2020/index.html @@ -0,0 +1,825 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Archive | 贾攀的流水账 + + + + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

贾攀的流水账

+ +
+

Panmax's Blog

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+ + +
+
+
+ + +
+ + + + + +
+
+
+ + Excellent! 357 posts in total. Keep on posting. +
+ + +
+ 2020 +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+ + + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/archives/2020/page/2/index.html b/archives/2020/page/2/index.html new file mode 100644 index 0000000000..dac894a19c --- /dev/null +++ b/archives/2020/page/2/index.html @@ -0,0 +1,725 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Archive | 贾攀的流水账 + + + + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

贾攀的流水账

+ +
+

Panmax's Blog

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+ + +
+
+
+ + +
+ + + + + +
+
+
+ + Excellent! 357 posts in total. Keep on posting. +
+ + +
+ 2020 +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+ + + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/archives/2021/02/index.html b/archives/2021/02/index.html new file mode 100644 index 0000000000..fb37655a39 --- /dev/null +++ b/archives/2021/02/index.html @@ -0,0 +1,522 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Archive | 贾攀的流水账 + + + + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

贾攀的流水账

+ +
+

Panmax's Blog

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+ + +
+
+
+ + +
+ + + + + +
+
+
+ + Excellent! 357 posts in total. Keep on posting. +
+ + +
+ 2021 +
+ + + + + + + + + + + + +
+
+ + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/archives/2021/03/index.html b/archives/2021/03/index.html new file mode 100644 index 0000000000..bb17df424c --- /dev/null +++ b/archives/2021/03/index.html @@ -0,0 +1,442 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Archive | 贾攀的流水账 + + + + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

贾攀的流水账

+ +
+

Panmax's Blog

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+ + +
+
+
+ + +
+ + + + + +
+
+
+ + Excellent! 357 posts in total. Keep on posting. +
+ + +
+ 2021 +
+ + + + +
+
+ + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/archives/2021/04/index.html b/archives/2021/04/index.html new file mode 100644 index 0000000000..bcedfd375b --- /dev/null +++ b/archives/2021/04/index.html @@ -0,0 +1,542 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Archive | 贾攀的流水账 + + + + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

贾攀的流水账

+ +
+

Panmax's Blog

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+ + +
+
+
+ + +
+ + + + + +
+
+
+ + Excellent! 357 posts in total. Keep on posting. +
+ + +
+ 2021 +
+ + + + + + + + + + + + + + +
+
+ + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/archives/2021/05/index.html b/archives/2021/05/index.html new file mode 100644 index 0000000000..d4bbdc2e07 --- /dev/null +++ b/archives/2021/05/index.html @@ -0,0 +1,522 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Archive | 贾攀的流水账 + + + + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

贾攀的流水账

+ +
+

Panmax's Blog

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+ + +
+
+
+ + +
+ + + + + +
+
+
+ + Excellent! 357 posts in total. Keep on posting. +
+ + +
+ 2021 +
+ + + + + + + + + + + + +
+
+ + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/archives/2021/06/index.html b/archives/2021/06/index.html new file mode 100644 index 0000000000..35e5e4791d --- /dev/null +++ b/archives/2021/06/index.html @@ -0,0 +1,502 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Archive | 贾攀的流水账 + + + + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

贾攀的流水账

+ +
+

Panmax's Blog

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+ + +
+
+
+ + +
+ + + + + +
+
+
+ + Excellent! 357 posts in total. Keep on posting. +
+ + +
+ 2021 +
+ + + + + + + + + + +
+
+ + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/archives/2021/07/index.html b/archives/2021/07/index.html new file mode 100644 index 0000000000..188ea4daae --- /dev/null +++ b/archives/2021/07/index.html @@ -0,0 +1,482 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Archive | 贾攀的流水账 + + + + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

贾攀的流水账

+ +
+

Panmax's Blog

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+ + +
+
+
+ + +
+ + + + + +
+
+
+ + Excellent! 357 posts in total. Keep on posting. +
+ + +
+ 2021 +
+ + + + + + + + +
+
+ + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/archives/2021/10/index.html b/archives/2021/10/index.html new file mode 100644 index 0000000000..9bb635db3d --- /dev/null +++ b/archives/2021/10/index.html @@ -0,0 +1,482 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Archive | 贾攀的流水账 + + + + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

贾攀的流水账

+ +
+

Panmax's Blog

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+ + +
+
+
+ + +
+ + + + + +
+
+
+ + Excellent! 357 posts in total. Keep on posting. +
+ + +
+ 2021 +
+ + + + + + + + +
+
+ + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/archives/2021/11/index.html b/archives/2021/11/index.html new file mode 100644 index 0000000000..84e6233978 --- /dev/null +++ b/archives/2021/11/index.html @@ -0,0 +1,482 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Archive | 贾攀的流水账 + + + + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

贾攀的流水账

+ +
+

Panmax's Blog

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+ + +
+
+
+ + +
+ + + + + +
+
+
+ + Excellent! 357 posts in total. Keep on posting. +
+ + +
+ 2021 +
+ + + + + + + + +
+
+ + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/archives/2021/12/index.html b/archives/2021/12/index.html new file mode 100644 index 0000000000..473cd7ea1e --- /dev/null +++ b/archives/2021/12/index.html @@ -0,0 +1,462 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Archive | 贾攀的流水账 + + + + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

贾攀的流水账

+ +
+

Panmax's Blog

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+ + +
+
+
+ + +
+ + + + + +
+
+
+ + Excellent! 357 posts in total. Keep on posting. +
+ + +
+ 2021 +
+ + + + + + +
+
+ + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/archives/2021/index.html b/archives/2021/index.html new file mode 100644 index 0000000000..c41bc5993e --- /dev/null +++ b/archives/2021/index.html @@ -0,0 +1,825 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Archive | 贾攀的流水账 + + + + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

贾攀的流水账

+ +
+

Panmax's Blog

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+ + +
+
+
+ + +
+ + + + + +
+
+
+ + Excellent! 357 posts in total. Keep on posting. +
+ + +
+ 2021 +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+ + + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/archives/2021/page/2/index.html b/archives/2021/page/2/index.html new file mode 100644 index 0000000000..47d54658db --- /dev/null +++ b/archives/2021/page/2/index.html @@ -0,0 +1,665 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Archive | 贾攀的流水账 + + + + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

贾攀的流水账

+ +
+

Panmax's Blog

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+ + +
+
+
+ + +
+ + + + + +
+
+
+ + Excellent! 357 posts in total. Keep on posting. +
+ + +
+ 2021 +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+ + + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/archives/2022/02/index.html b/archives/2022/02/index.html new file mode 100644 index 0000000000..bd60abdefc --- /dev/null +++ b/archives/2022/02/index.html @@ -0,0 +1,482 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Archive | 贾攀的流水账 + + + + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

贾攀的流水账

+ +
+

Panmax's Blog

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+ + +
+
+
+ + +
+ + + + + +
+
+
+ + Excellent! 357 posts in total. Keep on posting. +
+ + +
+ 2022 +
+ + + + + + + + +
+
+ + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/archives/2022/03/index.html b/archives/2022/03/index.html new file mode 100644 index 0000000000..e0bd9464ab --- /dev/null +++ b/archives/2022/03/index.html @@ -0,0 +1,442 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Archive | 贾攀的流水账 + + + + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

贾攀的流水账

+ +
+

Panmax's Blog

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+ + +
+
+
+ + +
+ + + + + +
+
+
+ + Excellent! 357 posts in total. Keep on posting. +
+ + +
+ 2022 +
+ + + + +
+
+ + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/archives/2022/05/index.html b/archives/2022/05/index.html new file mode 100644 index 0000000000..2487467596 --- /dev/null +++ b/archives/2022/05/index.html @@ -0,0 +1,542 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Archive | 贾攀的流水账 + + + + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

贾攀的流水账

+ +
+

Panmax's Blog

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+ + +
+
+
+ + +
+ + + + + +
+
+
+ + Excellent! 357 posts in total. Keep on posting. +
+ + +
+ 2022 +
+ + + + + + + + + + + + + + +
+
+ + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/archives/2022/06/index.html b/archives/2022/06/index.html new file mode 100644 index 0000000000..ca13b11277 --- /dev/null +++ b/archives/2022/06/index.html @@ -0,0 +1,682 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Archive | 贾攀的流水账 + + + + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

贾攀的流水账

+ +
+

Panmax's Blog

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+ + +
+
+
+ + +
+ + + + + +
+
+
+ + Excellent! 357 posts in total. Keep on posting. +
+ + +
+ 2022 +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+ + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/archives/2022/07/index.html b/archives/2022/07/index.html new file mode 100644 index 0000000000..ff093c0444 --- /dev/null +++ b/archives/2022/07/index.html @@ -0,0 +1,642 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Archive | 贾攀的流水账 + + + + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

贾攀的流水账

+ +
+

Panmax's Blog

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+ + +
+
+
+ + +
+ + + + + +
+
+
+ + Excellent! 357 posts in total. Keep on posting. +
+ + +
+ 2022 +
+ + + + + + + + + + + + + + + + + + + + + + + + +
+
+ + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/archives/2022/08/index.html b/archives/2022/08/index.html new file mode 100644 index 0000000000..30113c41a3 --- /dev/null +++ b/archives/2022/08/index.html @@ -0,0 +1,622 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Archive | 贾攀的流水账 + + + + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

贾攀的流水账

+ +
+

Panmax's Blog

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+ + +
+
+
+ + +
+ + + + + +
+
+
+ + Excellent! 357 posts in total. Keep on posting. +
+ + +
+ 2022 +
+ + + + + + + + + + + + + + + + + + + + + + +
+
+ + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/archives/2022/09/index.html b/archives/2022/09/index.html new file mode 100644 index 0000000000..3f197195f4 --- /dev/null +++ b/archives/2022/09/index.html @@ -0,0 +1,582 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Archive | 贾攀的流水账 + + + + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

贾攀的流水账

+ +
+

Panmax's Blog

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+ + +
+
+
+ + +
+ + + + + +
+
+
+ + Excellent! 357 posts in total. Keep on posting. +
+ + +
+ 2022 +
+ + + + + + + + + + + + + + + + + + +
+
+ + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/archives/2022/10/index.html b/archives/2022/10/index.html new file mode 100644 index 0000000000..d38371be13 --- /dev/null +++ b/archives/2022/10/index.html @@ -0,0 +1,482 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Archive | 贾攀的流水账 + + + + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

贾攀的流水账

+ +
+

Panmax's Blog

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+ + +
+
+
+ + +
+ + + + + +
+
+
+ + Excellent! 357 posts in total. Keep on posting. +
+ + +
+ 2022 +
+ + + + + + + + +
+
+ + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/archives/2022/11/index.html b/archives/2022/11/index.html new file mode 100644 index 0000000000..b9aa51b554 --- /dev/null +++ b/archives/2022/11/index.html @@ -0,0 +1,442 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Archive | 贾攀的流水账 + + + + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

贾攀的流水账

+ +
+

Panmax's Blog

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+ + +
+
+
+ + +
+ + + + + +
+
+
+ + Excellent! 357 posts in total. Keep on posting. +
+ + +
+ 2022 +
+ + + + +
+
+ + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/archives/2022/12/index.html b/archives/2022/12/index.html new file mode 100644 index 0000000000..46ee739c5b --- /dev/null +++ b/archives/2022/12/index.html @@ -0,0 +1,442 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Archive | 贾攀的流水账 + + + + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

贾攀的流水账

+ +
+

Panmax's Blog

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+ + +
+
+
+ + +
+ + + + + +
+
+
+ + Excellent! 357 posts in total. Keep on posting. +
+ + +
+ 2022 +
+ + + + +
+
+ + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/archives/2022/index.html b/archives/2022/index.html new file mode 100644 index 0000000000..422ac4ca91 --- /dev/null +++ b/archives/2022/index.html @@ -0,0 +1,825 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Archive | 贾攀的流水账 + + + + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

贾攀的流水账

+ +
+

Panmax's Blog

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+ + +
+
+
+ + +
+ + + + + +
+
+
+ + Excellent! 357 posts in total. Keep on posting. +
+ + +
+ 2022 +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+ + + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/archives/2022/page/2/index.html b/archives/2022/page/2/index.html new file mode 100644 index 0000000000..be05a94b9c --- /dev/null +++ b/archives/2022/page/2/index.html @@ -0,0 +1,825 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Archive | 贾攀的流水账 + + + + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

贾攀的流水账

+ +
+

Panmax's Blog

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+ + +
+
+
+ + +
+ + + + + +
+
+
+ + Excellent! 357 posts in total. Keep on posting. +
+ + +
+ 2022 +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+ + + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/archives/2022/page/3/index.html b/archives/2022/page/3/index.html new file mode 100644 index 0000000000..fe537fd628 --- /dev/null +++ b/archives/2022/page/3/index.html @@ -0,0 +1,765 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Archive | 贾攀的流水账 + + + + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

贾攀的流水账

+ +
+

Panmax's Blog

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+ + +
+
+
+ + +
+ + + + + +
+
+
+ + Excellent! 357 posts in total. Keep on posting. +
+ + +
+ 2022 +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+ + + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/archives/2023/03/index.html b/archives/2023/03/index.html new file mode 100644 index 0000000000..27957000b2 --- /dev/null +++ b/archives/2023/03/index.html @@ -0,0 +1,442 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Archive | 贾攀的流水账 + + + + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

贾攀的流水账

+ +
+

Panmax's Blog

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+ + +
+
+
+ + +
+ + + + + +
+
+
+ + Excellent! 357 posts in total. Keep on posting. +
+ + +
+ 2023 +
+ + + + +
+
+ + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/archives/2023/04/index.html b/archives/2023/04/index.html new file mode 100644 index 0000000000..da870c2268 --- /dev/null +++ b/archives/2023/04/index.html @@ -0,0 +1,502 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Archive | 贾攀的流水账 + + + + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

贾攀的流水账

+ +
+

Panmax's Blog

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+ + +
+
+
+ + +
+ + + + + +
+
+
+ + Excellent! 357 posts in total. Keep on posting. +
+ + +
+ 2023 +
+ + + + + + + + + + +
+
+ + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/archives/2023/06/index.html b/archives/2023/06/index.html new file mode 100644 index 0000000000..e513d96680 --- /dev/null +++ b/archives/2023/06/index.html @@ -0,0 +1,562 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Archive | 贾攀的流水账 + + + + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

贾攀的流水账

+ +
+

Panmax's Blog

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+ + +
+
+
+ + +
+ + + + + +
+
+
+ + Excellent! 357 posts in total. Keep on posting. +
+ + +
+ 2023 +
+ + + + + + + + + + + + + + + + +
+
+ + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/archives/2023/07/index.html b/archives/2023/07/index.html new file mode 100644 index 0000000000..7c910aaff8 --- /dev/null +++ b/archives/2023/07/index.html @@ -0,0 +1,522 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Archive | 贾攀的流水账 + + + + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

贾攀的流水账

+ +
+

Panmax's Blog

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+ + +
+
+
+ + +
+ + + + + +
+
+
+ + Excellent! 357 posts in total. Keep on posting. +
+ + +
+ 2023 +
+ + + + + + + + + + + + +
+
+ + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/archives/2023/08/index.html b/archives/2023/08/index.html new file mode 100644 index 0000000000..3d492f425f --- /dev/null +++ b/archives/2023/08/index.html @@ -0,0 +1,825 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Archive | 贾攀的流水账 + + + + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

贾攀的流水账

+ +
+

Panmax's Blog

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+ + +
+
+
+ + +
+ + + + + +
+
+
+ + Excellent! 357 posts in total. Keep on posting. +
+ + +
+ 2023 +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+ + + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/archives/2023/08/page/2/index.html b/archives/2023/08/page/2/index.html new file mode 100644 index 0000000000..1f6fa728d7 --- /dev/null +++ b/archives/2023/08/page/2/index.html @@ -0,0 +1,625 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Archive | 贾攀的流水账 + + + + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

贾攀的流水账

+ +
+

Panmax's Blog

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+ + +
+
+
+ + +
+ + + + + +
+
+
+ + Excellent! 357 posts in total. Keep on posting. +
+ + +
+ 2023 +
+ + + + + + + + + + + + + + + + + + + + + + +
+
+ + + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/archives/2023/09/index.html b/archives/2023/09/index.html new file mode 100644 index 0000000000..5eeeea2eb4 --- /dev/null +++ b/archives/2023/09/index.html @@ -0,0 +1,782 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Archive | 贾攀的流水账 + + + + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

贾攀的流水账

+ +
+

Panmax's Blog

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+ + +
+
+
+ + +
+ + + + + +
+
+
+ + Excellent! 357 posts in total. Keep on posting. +
+ + +
+ 2023 +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+ + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/archives/2023/10/index.html b/archives/2023/10/index.html new file mode 100644 index 0000000000..5aefb9fc53 --- /dev/null +++ b/archives/2023/10/index.html @@ -0,0 +1,482 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Archive | 贾攀的流水账 + + + + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

贾攀的流水账

+ +
+

Panmax's Blog

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+ + +
+
+
+ + +
+ + + + + +
+
+
+ + Excellent! 357 posts in total. Keep on posting. +
+ + +
+ 2023 +
+ + + + + + + + +
+
+ + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/archives/2023/11/index.html b/archives/2023/11/index.html new file mode 100644 index 0000000000..7213d5f634 --- /dev/null +++ b/archives/2023/11/index.html @@ -0,0 +1,442 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Archive | 贾攀的流水账 + + + + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

贾攀的流水账

+ +
+

Panmax's Blog

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+ + +
+
+
+ + +
+ + + + + +
+
+
+ + Excellent! 357 posts in total. Keep on posting. +
+ + +
+ 2023 +
+ + + + +
+
+ + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/archives/2023/index.html b/archives/2023/index.html new file mode 100644 index 0000000000..5d9c53f0ca --- /dev/null +++ b/archives/2023/index.html @@ -0,0 +1,825 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Archive | 贾攀的流水账 + + + + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

贾攀的流水账

+ +
+

Panmax's Blog

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+ + +
+
+
+ + +
+ + + + + +
+
+
+ + Excellent! 357 posts in total. Keep on posting. +
+ + +
+ 2023 +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+ + + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/archives/2023/page/2/index.html b/archives/2023/page/2/index.html new file mode 100644 index 0000000000..4e8fd3f5cd --- /dev/null +++ b/archives/2023/page/2/index.html @@ -0,0 +1,825 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Archive | 贾攀的流水账 + + + + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

贾攀的流水账

+ +
+

Panmax's Blog

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+ + +
+
+
+ + +
+ + + + + +
+
+
+ + Excellent! 357 posts in total. Keep on posting. +
+ + +
+ 2023 +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+ + + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/archives/2023/page/3/index.html b/archives/2023/page/3/index.html new file mode 100644 index 0000000000..4bd18df5d7 --- /dev/null +++ b/archives/2023/page/3/index.html @@ -0,0 +1,825 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Archive | 贾攀的流水账 + + + + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

贾攀的流水账

+ +
+

Panmax's Blog

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+ + +
+
+
+ + +
+ + + + + +
+
+
+ + Excellent! 357 posts in total. Keep on posting. +
+ + +
+ 2023 +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+ + + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/archives/2023/page/4/index.html b/archives/2023/page/4/index.html new file mode 100644 index 0000000000..0f82701190 --- /dev/null +++ b/archives/2023/page/4/index.html @@ -0,0 +1,605 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Archive | 贾攀的流水账 + + + + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

贾攀的流水账

+ +
+

Panmax's Blog

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+ + +
+
+
+ + +
+ + + + + +
+
+
+ + Excellent! 357 posts in total. Keep on posting. +
+ + +
+ 2023 +
+ + + + + + + + + + + + + + + + + + + + +
+
+ + + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/archives/2024/04/index.html b/archives/2024/04/index.html new file mode 100644 index 0000000000..68103d7547 --- /dev/null +++ b/archives/2024/04/index.html @@ -0,0 +1,442 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Archive | 贾攀的流水账 + + + + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

贾攀的流水账

+ +
+

Panmax's Blog

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+ + +
+
+
+ + +
+ + + + + +
+
+
+ + Excellent! 357 posts in total. Keep on posting. +
+ + +
+ 2024 +
+ + + + +
+
+ + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/archives/2024/06/index.html b/archives/2024/06/index.html new file mode 100644 index 0000000000..117dad6118 --- /dev/null +++ b/archives/2024/06/index.html @@ -0,0 +1,442 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Archive | 贾攀的流水账 + + + + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

贾攀的流水账

+ +
+

Panmax's Blog

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+ + +
+
+
+ + +
+ + + + + +
+
+
+ + Excellent! 357 posts in total. Keep on posting. +
+ + +
+ 2024 +
+ + + + +
+
+ + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/archives/2024/10/index.html b/archives/2024/10/index.html new file mode 100644 index 0000000000..9591896cf6 --- /dev/null +++ b/archives/2024/10/index.html @@ -0,0 +1,442 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Archive | 贾攀的流水账 + + + + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

贾攀的流水账

+ +
+

Panmax's Blog

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+ + +
+
+
+ + +
+ + + + + +
+
+
+ + Excellent! 357 posts in total. Keep on posting. +
+ + +
+ 2024 +
+ + + + +
+
+ + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/archives/2024/11/index.html b/archives/2024/11/index.html new file mode 100644 index 0000000000..f45f868566 --- /dev/null +++ b/archives/2024/11/index.html @@ -0,0 +1,442 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Archive | 贾攀的流水账 + + + + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

贾攀的流水账

+ +
+

Panmax's Blog

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+ + +
+
+
+ + +
+ + + + + +
+
+
+ + Excellent! 357 posts in total. Keep on posting. +
+ + +
+ 2024 +
+ + + + +
+
+ + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/archives/2024/index.html b/archives/2024/index.html new file mode 100644 index 0000000000..2b19ac2d8c --- /dev/null +++ b/archives/2024/index.html @@ -0,0 +1,502 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Archive | 贾攀的流水账 + + + + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

贾攀的流水账

+ +
+

Panmax's Blog

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+ + +
+
+
+ + +
+ + + + + +
+
+
+ + Excellent! 357 posts in total. Keep on posting. +
+ + +
+ 2024 +
+ + + + + + + + + + +
+
+ + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/archives/index.html b/archives/index.html new file mode 100644 index 0000000000..1f4f54bdfc --- /dev/null +++ b/archives/index.html @@ -0,0 +1,828 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Archive | 贾攀的流水账 + + + + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

贾攀的流水账

+ +
+

Panmax's Blog

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+ + +
+
+
+ + +
+ + + + + +
+
+
+ + Excellent! 357 posts in total. Keep on posting. +
+ + +
+ 2024 +
+ + + + + + + + +
+ 2023 +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+ + + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/archives/page/10/index.html b/archives/page/10/index.html new file mode 100644 index 0000000000..003a09fd37 --- /dev/null +++ b/archives/page/10/index.html @@ -0,0 +1,828 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Archive | 贾攀的流水账 + + + + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

贾攀的流水账

+ +
+

Panmax's Blog

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+ + +
+
+
+ + +
+ + + + + +
+
+
+ + Excellent! 357 posts in total. Keep on posting. +
+ + +
+ 2020 +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ 2019 +
+ + + + + + + + +
+
+ + + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/archives/page/11/index.html b/archives/page/11/index.html new file mode 100644 index 0000000000..ccacc8672e --- /dev/null +++ b/archives/page/11/index.html @@ -0,0 +1,825 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Archive | 贾攀的流水账 + + + + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

贾攀的流水账

+ +
+

Panmax's Blog

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+ + +
+
+
+ + +
+ + + + + +
+
+
+ + Excellent! 357 posts in total. Keep on posting. +
+ + +
+ 2019 +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+ + + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/archives/page/12/index.html b/archives/page/12/index.html new file mode 100644 index 0000000000..eb472644ff --- /dev/null +++ b/archives/page/12/index.html @@ -0,0 +1,828 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Archive | 贾攀的流水账 + + + + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

贾攀的流水账

+ +
+

Panmax's Blog

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+ + +
+
+
+ + +
+ + + + + +
+
+
+ + Excellent! 357 posts in total. Keep on posting. +
+ + +
+ 2019 +
+ + + + + + + + + + + + + + + + + + + + + + +
+ 2018 +
+ + + + + + + + + + + + + + + + + + + + +
+
+ + + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/archives/page/13/index.html b/archives/page/13/index.html new file mode 100644 index 0000000000..0a58711953 --- /dev/null +++ b/archives/page/13/index.html @@ -0,0 +1,825 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Archive | 贾攀的流水账 + + + + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

贾攀的流水账

+ +
+

Panmax's Blog

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+ + +
+
+
+ + +
+ + + + + +
+
+
+ + Excellent! 357 posts in total. Keep on posting. +
+ + +
+ 2018 +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+ + + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/archives/page/14/index.html b/archives/page/14/index.html new file mode 100644 index 0000000000..b7a48e6238 --- /dev/null +++ b/archives/page/14/index.html @@ -0,0 +1,828 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Archive | 贾攀的流水账 + + + + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

贾攀的流水账

+ +
+

Panmax's Blog

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+ + +
+
+
+ + +
+ + + + + +
+
+
+ + Excellent! 357 posts in total. Keep on posting. +
+ + +
+ 2018 +
+ + + + + + + + + + + + + + + + + + +
+ 2017 +
+ + + + + + + + + + + + + + + + + + + + + + + + +
+
+ + + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/archives/page/15/index.html b/archives/page/15/index.html new file mode 100644 index 0000000000..b45cdb89f9 --- /dev/null +++ b/archives/page/15/index.html @@ -0,0 +1,825 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Archive | 贾攀的流水账 + + + + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

贾攀的流水账

+ +
+

Panmax's Blog

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+ + +
+
+
+ + +
+ + + + + +
+
+
+ + Excellent! 357 posts in total. Keep on posting. +
+ + +
+ 2017 +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+ + + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/archives/page/16/index.html b/archives/page/16/index.html new file mode 100644 index 0000000000..d751eb158b --- /dev/null +++ b/archives/page/16/index.html @@ -0,0 +1,828 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Archive | 贾攀的流水账 + + + + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

贾攀的流水账

+ +
+

Panmax's Blog

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+ + +
+
+
+ + +
+ + + + + +
+
+
+ + Excellent! 357 posts in total. Keep on posting. +
+ + +
+ 2017 +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ 2016 +
+ + + + + + + + + + + + + + +
+
+ + + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/archives/page/17/index.html b/archives/page/17/index.html new file mode 100644 index 0000000000..4bae488e46 --- /dev/null +++ b/archives/page/17/index.html @@ -0,0 +1,825 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Archive | 贾攀的流水账 + + + + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

贾攀的流水账

+ +
+

Panmax's Blog

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+ + +
+
+
+ + +
+ + + + + +
+
+
+ + Excellent! 357 posts in total. Keep on posting. +
+ + +
+ 2016 +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+ + + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/archives/page/18/index.html b/archives/page/18/index.html new file mode 100644 index 0000000000..6a5fd2731d --- /dev/null +++ b/archives/page/18/index.html @@ -0,0 +1,768 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Archive | 贾攀的流水账 + + + + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

贾攀的流水账

+ +
+

Panmax's Blog

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+ + +
+
+
+ + +
+ + + + + +
+
+
+ + Excellent! 357 posts in total. Keep on posting. +
+ + +
+ 2016 +
+ + + + + + + + +
+ 2015 +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+ + + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/archives/page/2/index.html b/archives/page/2/index.html new file mode 100644 index 0000000000..d6e20c16fa --- /dev/null +++ b/archives/page/2/index.html @@ -0,0 +1,825 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Archive | 贾攀的流水账 + + + + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

贾攀的流水账

+ +
+

Panmax's Blog

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+ + +
+
+
+ + +
+ + + + + +
+
+
+ + Excellent! 357 posts in total. Keep on posting. +
+ + +
+ 2023 +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+ + + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/archives/page/3/index.html b/archives/page/3/index.html new file mode 100644 index 0000000000..452faa3a71 --- /dev/null +++ b/archives/page/3/index.html @@ -0,0 +1,825 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Archive | 贾攀的流水账 + + + + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

贾攀的流水账

+ +
+

Panmax's Blog

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+ + +
+
+
+ + +
+ + + + + +
+
+
+ + Excellent! 357 posts in total. Keep on posting. +
+ + +
+ 2023 +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+ + + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/archives/page/4/index.html b/archives/page/4/index.html new file mode 100644 index 0000000000..68e806f46f --- /dev/null +++ b/archives/page/4/index.html @@ -0,0 +1,828 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Archive | 贾攀的流水账 + + + + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

贾攀的流水账

+ +
+

Panmax's Blog

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+ + +
+
+
+ + +
+ + + + + +
+
+
+ + Excellent! 357 posts in total. Keep on posting. +
+ + +
+ 2023 +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
+ 2022 +
+ + + + + + + + + + + + + + + + +
+
+ + + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/archives/page/5/index.html b/archives/page/5/index.html new file mode 100644 index 0000000000..f209150994 --- /dev/null +++ b/archives/page/5/index.html @@ -0,0 +1,825 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Archive | 贾攀的流水账 + + + + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

贾攀的流水账

+ +
+

Panmax's Blog

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+ + +
+
+
+ + +
+ + + + + +
+
+
+ + Excellent! 357 posts in total. Keep on posting. +
+ + +
+ 2022 +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+ + + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/archives/page/6/index.html b/archives/page/6/index.html new file mode 100644 index 0000000000..8c8d352d5e --- /dev/null +++ b/archives/page/6/index.html @@ -0,0 +1,825 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Archive | 贾攀的流水账 + + + + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

贾攀的流水账

+ +
+

Panmax's Blog

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+ + +
+
+
+ + +
+ + + + + +
+
+
+ + Excellent! 357 posts in total. Keep on posting. +
+ + +
+ 2022 +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+ + + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/archives/page/7/index.html b/archives/page/7/index.html new file mode 100644 index 0000000000..ed9c64335e --- /dev/null +++ b/archives/page/7/index.html @@ -0,0 +1,828 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Archive | 贾攀的流水账 + + + + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

贾攀的流水账

+ +
+

Panmax's Blog

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+ + +
+
+
+ + +
+ + + + + +
+
+
+ + Excellent! 357 posts in total. Keep on posting. +
+ + +
+ 2022 +
+ + + + + + + + + + + + + + + + + + + + +
+ 2021 +
+ + + + + + + + + + + + + + + + + + + + + + +
+
+ + + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/archives/page/8/index.html b/archives/page/8/index.html new file mode 100644 index 0000000000..2c2fd19d14 --- /dev/null +++ b/archives/page/8/index.html @@ -0,0 +1,825 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Archive | 贾攀的流水账 + + + + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

贾攀的流水账

+ +
+

Panmax's Blog

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+ + +
+
+
+ + +
+ + + + + +
+
+
+ + Excellent! 357 posts in total. Keep on posting. +
+ + +
+ 2021 +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+ + + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/archives/page/9/index.html b/archives/page/9/index.html new file mode 100644 index 0000000000..2a236ed200 --- /dev/null +++ b/archives/page/9/index.html @@ -0,0 +1,828 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Archive | 贾攀的流水账 + + + + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

贾攀的流水账

+ +
+

Panmax's Blog

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+ + +
+
+
+ + +
+ + + + + +
+
+
+ + Excellent! 357 posts in total. Keep on posting. +
+ + +
+ 2021 +
+ + + + +
+ 2020 +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+ + + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/atom.xml b/atom.xml new file mode 100644 index 0000000000..b748110f96 --- /dev/null +++ b/atom.xml @@ -0,0 +1,464 @@ + + + 贾攀的流水账 + + Panmax's Blog + + + + 2024-12-16T01:29:11.085Z + https://jiapan.me/ + + + Panmax + + + + Hexo + + + 《论语》公冶长篇中孔子点评过的弟子 + + https://jiapan.me/2024/gong-ye-chang/ + 2024-11-26T10:04:31.000Z + 2024-12-16T01:29:11.085Z + + 公冶长,字子芝

公冶长一生治学,德才兼备,虽然做过牢,可孔子不觉得他有罪,就把女儿嫁给了他;

子谓公冶长:“可妻也,虽在缧绁之中,非其罪也!”以其子妻之。

南宫括,字子容

南宫适知晓分寸,进退有度,好社会能干、坏社会能自保,孔子把侄女嫁给了他;

子谓南容:“邦有道不废;邦无道免于刑戮。”以其兄之子妻之。

宓不齐,字子贱

宓子贱仁德好学,刚正不阿;

子谓子贱:“君子哉若人!鲁无君子者,斯焉取斯?”

端木赐,字子贡

子贡心高气傲,有点喜欢自我标榜,孔子总是苦口婆心地对他旁敲侧击;

子贡问曰:“赐也何如?”子曰:“女,器也。”曰:“何器也?”曰:“瑚琏也。”
子贡曰:“我不欲人之加诸我也,吾亦欲无加诸人。”子曰:“赐也,非尔所及也。”

冉雍,字仲弓

冉雍朴素踏实、勇于实干;

或曰:“雍也仁而不佞。”子曰:“焉用佞?御人以口给,屡憎于人。不知其仁,焉用佞?”

漆雕开,字子开

漆雕开稳重谦虚,志向高远但也沉得住气;

子使漆雕开仕,对曰:“吾斯之未能信。”子说。

仲由,字子路

子路是个急性子,自视甚高,直来直去,有什么说什么;

子曰:“道不行,乘桴浮于海,从我者其由与?”子路闻之喜,子曰:“由也好勇过我,无所取材。”
子路有闻,未之能行,唯恐有闻。

公西赤,字子华

公西赤善于外交,口才一流;

“赤也何如?”子曰:“赤也,束带立于朝,可使与宾客言也。不知其仁也。”

冉求,字子有

冉求多才多艺,特别会管钱,但是因为帮季氏敛财,受到孔子的严厉批评,后来跟孔子学习之后逐渐成为仁德之人;

“求也何如?”子曰:“求也,千室之邑,百乘之家,可使为之宰也,不知其仁也。”、

宰予,字子我

宰我调皮捣蛋,能言善辩,他最出名的事就是白天睡觉被老师骂;

宰予昼寝,子曰:“朽木不可雕也,粪土之墙不可杇也,于予与何诛?”子曰:“始吾于人也,听其言而信其行;今吾于人也,听其言而观其行。于予与改是。”

申枨(申党),字周

申枨精通六艺,但欲望比较强,孔子觉得他还没培养出刚健的气质。

子曰:“吾未见刚者。”或对曰:“申枨。”子曰:“枨也欲,焉得刚。”

参考:

]]>
+ + + + + + <h2 id="公冶长,字子芝"><a href="#公冶长,字子芝" class="headerlink" title="公冶长,字子芝"></a>公冶长,字子芝</h2><p>公冶长一生治学,德才兼备,虽然做过牢,可孔子不觉得他有罪,就把女儿嫁给了他;</p> +<blockq + + + + + +
+ + + 《金钱心理学》摘抄 + + https://jiapan.me/2024/The-Psychology-of-Money/ + 2024-10-31T10:04:31.000Z + 2024-12-16T01:29:10.721Z + + 我所读过的理财类书籍并不多,在国庆后由于人性的贪婪,在股市中损失了(对我来说)一大笔钱,机缘巧合下读了这本名叫《金钱心理学》的理财类书籍。这是我读的为数不多的觉得写的非常好的理财书之一,哪怕不限于理财类,它也是一本用来了解人性和世界观的好书,由于得到了非常好的阅读体验,从另一方面来说这次的投资失利也许对我来说属于因祸得福了。

在读《金钱心理学》时,我脑海中经常飘出一句励志的话:「种一棵树最好的时间是十年前,其次是现在」,刚刚查了一下这句话的来源,出自非洲经济学家丹比萨·莫约的《援助的死亡》一书,巧合的是也是一位经济学家说的。我现在已经开始了超长线的定投计划,用十年时间来定投黄金和标普500,自从开始定投后就出现了两种有冲突的念头:既想让时间过快一点,好让我完成我的定投目标,见证时间和复利带来的强大效果,又想让时间过慢一些,自己还不想那么快的老去,想再多一些时间陪伴孩子们,更不想眼睁睁看着父母一天天的老去。这本书还纠正了我一个错误观念,我之前认为财富跟赚钱多少成非常强的正相关性,这本书告诉我并不是这样,收入当然是一部分,但对大部分人来说更重要的是节俭和储蓄。

这本书中没有教我们认识各种指标,都是一些软技能,下边是我从这本书中摘录下的句子,通过这些句子也能感受到这本书再讲的是什么样的理财观念。最后我会在写一写我准备开启的一段超长线投资计划。

我最喜欢的句子

  • 人们习惯把别人的失败归咎于错误的决策,而把自己的失败归咎于糟糕的运气。
  • 现代资本擅长创造两种东西:财富和嫉妒。
  • 时间自由是财富能带给你的最大红利。
  • 富有的最高级形式是,每天早上起床后你都可以说:“今天我能做我想做的任何事。”
  • 通过用金钱购买昂贵之物获得的尊重和羡慕可能远比你想象中少。
  • 历史是对变化的研究,但具有讽刺性的是,人们却将历史当作预测未来的工具。
  • 杠杆——以负债的方式进行投资——把常规风险扩大到了足以导致毁灭的程度。
  • 只有当你能给一项计划数年或数十年的时间去成长时,复利的效应才能得到最佳体现。
  • 无论在工作生涯的哪个节点,都要定下这样均衡的目标:每年做好适中的储蓄,给自己适度的自由时间,让通勤不超过适当的时长,至少花适量的时间来陪伴家人。
  • 如果你把波动看作要买的入场券,情况就会完全不同。
  • 市场回报永远不会是免费的,现在不是,将来也不会是。你需要支付一定的费用,就像要花钱购买一件产品一样。
  • 在做计划的时候,我们会专注于我们想做的和能做的事情,而忽略了他人的计划和能力,但他人的决策也会对结果产生影响。
  • 用能让你睡踏实的方式来理财。
  • 如果你想提高投资回报,最简单而有效的方法就是拉长时间。时间是投资中最强大的力量。
  • 增长是由复利驱动的,而复利通常需要时间。毁灭却可能由独立的致命因素导致,可以在很短的时间内发生;它也可能由失去信心引发,而信心可以在一瞬间崩塌。

全部摘抄的句子

  • 一个无法控制个人情绪的天才或许会引发财务上的灾难,但反过来看——那些没有接受过专业金融教育的普通人,也可以凭借与智商衡量标准无关的良好行为习惯,最终走向富裕。
  • 财务方面的成功并不是一门硬科学,而是一种软技能——你怎么做,比你掌握多少知识更重要。
  • 有两种事物会影响每一个人,不管你是否对它们感兴趣——健康和金钱。
  • 我认为,这种现象的主要原因是,我们思考和学习理财的方式更像学习物理的(涉及很多法则和定律),而不像学习心理学的(关注情感及其微妙变
  • 关于金钱的知识和经验可以被用于生活中的其他许多问题,比如风险、信心和幸福中。很少有其他事物能像金钱这样,仿佛一面强有力的放大镜,帮助你理解人们为何会做出某些举动。
  • 人类涉及金钱的行为是地球上最伟大的表演之一。
  • 历史从来不会重复,人类却总会重蹈覆辙。
  • 你对金钱的个人经验可能只有0.00000001%符合实际,但它构成了你对世界运作方式的主观判断的80%。
  • 研究股市的历史后,你会觉得自己明白了某些事,但只有亲身经历过,感受过它的巨大影响,你才可能真正改变自己的行为
  • 有些事只有真正经历过才会懂。
  • 人们一生中的投资决策在很大程度上取决于其生活经历——尤其是成年后的早期经历。
  • 每个人对金钱的体验都是不同的,即使是在那些你觉得经历很相似的人之间。
  • 个体的不同经历可能导致他们对那些看似没有争议的话题出现完全不同的看法。
  • 人们做的与金钱相关的每个决定都有其合理的一面,因为这些决定是他们在掌握了当时所能掌握的信息,然后将其纳入自己对世界运作方式的独特认知框架后做出的。
  • 每个关于金钱的决定对当时的他们来说都是合理的,是建立在他们当时具备的条件之上的选择。
  • 我们之所以经常在金钱方面做出看似疯狂的决策,是因为相较之下在这场游戏里我们都是新手,而在你看来不可理喻的行为对我而言却合乎情理。但是,没有谁真的失去了理智——我们都在依靠自己独特的经验做出选择,而这些经验在特定的时间点和情境下都是合理的。
  • 生活中的每一个结果都受到个人努力之外的其他作用的影响。
  • 任何事都没有表面看来那样美好或糟糕。
  • 在生活这场游戏中起作用的除了我们自己,还有其他70亿人,同时还存在着无数的变量。那些在你控制之外的行为产生的意外影响可能比你有意识的行为产生的影响更大。
  • 因为运气难以被量化,把他人的成功归咎于运气又是一种不礼貌的举动,所以我们大多数时候会自动忽略运气在成功中扮演的重要角色。
  • 在评价别人时,将成就归功于运气会显得你很嫉妒和刻薄,哪怕我们知道的确存在运气的成分;而在评价自己时,将成就归功于运气则会令自己感到气馁,难以接受。
  • 人们习惯把别人的失败归咎于错误的决策,而把自己的失败归咎于糟糕的运气
  • 不要太关注具体的个人和案例研究,而要看到具有普适性的模式。
  • 预防失败的诀窍是:做好你的财务规划,使其不至于因为一次糟糕的投资和未能达成的财务目标而全盘崩溃,保证自己能在投资道路上持续前进,一直等到好运降临的那一刻。
  • 风险的存在也意味着在评价自身的失败时,我们应该原谅和理解自己。
  • 为了赚他们并未拥有也不需要的钱,他们拿自己已经拥有并确实需要的东西去冒险了。这是愚蠢至极的做法。冒着失去重要东西的风险去争取并不重要的东西的行为毫无道理可言。
  • 最难的理财技能是让逐利适可而止。
  • 现代资本擅长创造两种东西:财富和嫉妒。
  • 幸福是你拥有的减去你期待的。
  • 攀比就像一场没有人能打赢的战役,取胜的唯一办法是根本不要加入这场战争——用知足的态度接受一切,即使这意味着自己比周围的人逊色
  • 如果你无法拒绝潜在的金钱诱惑,那么欲望最终可能将你吞没。
  • 一个领域里的知识和经验常常可以为其他领域提供重要的借鉴。
  • 冰期形成的主要原因并非极寒的冬季,而是凉爽的夏季。
  • 地球冰川形成的关键并不一定是大量的降雪,而是雪能累积下来,无论量有多少。
  • 成功的投资并不需要你一直做出成功的决定。你只要做到一直不把事情搞砸就够了。
  • 但守富的方式却只有一种:在保持节俭的同时,还需要一些谨小慎微。
  • 致富和守富是两种完全不同的技能。
  • 致富需要的是冒险精神、乐观心态,以及放手一搏的勇气。
  • 守富需要谦逊和敬畏之心,需要清楚财富来得有多快,去得就有多容易。守富需要节俭,并要承认你获得的财富中一部分源自运气,所以不要指望无限复制过去的成功。
  • 生存应该成为你一切策略的基础,无论是关于投资、规划个人职业还是经营生意的
  • 没有任何收益值得你冒失去一切的风险。
  • 你的财务规划要求的具体前提条件越多,你的财务状况就越脆弱。
  • 从长远看结果是积极的,但从短期看过程可能很糟糕”这一点乍看之下不符合直觉,但生活中很多事确实是这样的。
  • 经济、市场和个人职业生涯通常也会遵循一条相似的路径——在不断的损失中持续增长的过程。
  • 对一个投资者来说,为了避免心态膨胀,付出再大的代价都是值得的。
  • 当投资者持有这些藏品的时间足够长,这系列投资组合的整体收益就会趋近其中表现最好的部分的收益
  • 一个投资者在一半的时间里都看走了眼,最后却仍然能致富,这个事实是不符合直觉的。它也意味着我们低估了许多事物失败的频率,所以当失败发生时,我们就会反应过度
  • 任何规模巨大、利润丰厚、声名远播或影响力深远的事物都源自某个尾事件——从几千甚至几百万个事件中脱颖而出的一个。
  • 拿破仑对军事天才的定义是“当身边所有人都进入非理性状态时还能继续正常行事的人”。
  • “当下”其实并没有那么重要。作为投资者,你今天、明天或下周做的决定远不如你一生中个别几天做的决定重要。
  • 一个投资天才也应该是一个当身边所有人都进入非理性状态时还能继续正常行事的人。
  • 如果你是一个优秀的雇员,在经过三番五次的尝试和试验后,你终究会在适合自己的领域找到适合自己的公司。
  • 当我们特别关注某些榜样的成功时,我们就会忽视这样一个事实:他们的成功来自他们全部行为中的一小部分。这种忽视会让我们觉得我们自己的失败、亏损和挫折是因为我们做错了什么。
  • “重要的不是你对了还是错了,”“金融大鳄”乔治·索罗斯(George Soros)曾说,“而是当你对的时候,你能赚到多少,或者当你错的时候,你会损失多少。”你即使有一半的时间都在犯错,到最后依然能赢。
  • 时间自由是财富能带给你的最大红利。
  • 富有的最高级形式是,每天早上起床后你都可以说:“今天我能做我想做的任何事。”
  • 幸福是一个复杂的话题,因为每个人的幸福观都不同,但如果幸福的分数有一个公分母——一种普遍的快乐源泉——那就是对生活的全面掌控。
  • 在自己喜欢的任何时候和自己喜欢的对象做想做的事,而且想做多久就做多久,这样的自由是极其珍贵的,而这就是金钱能带给我们的最大红利
  • 不是工资多少,不是房子大小,也不是工作好坏,而是对自己想做什么、什么时候做、和谁一起做拥有掌控能力。这是生活中决定幸福感的通用变量。
  • 金钱最大的内在价值是它能赋予你掌控自己时间的能力——这句话没有任何夸张的成分。
  • 拥有更多财富则意味着在失业后可以从容地等待更好的职业机会,而不必急于抓住遇到的第一根救命稻草。这种能力可以改变一个人的生活。
  • 拥有更多财富则意味着可以选择一份待遇不高但时间灵活的,或是通勤时间比较短的工作
  • 做一份自己喜欢却无法掌控时间的事和做自己讨厌的事没什么区别。
  • 与前几代人相比,我们对时间的控制力降低了。正因为控制时间是影响幸福感的关键因素,所以我们无须对尽管现在的我们更富有了,但我们没有感到更快乐这一事实感到惊讶。
  • 这里存在一个悖论:我们都想通过财富来告诉其他人,自己应该受到他们的爱慕与敬仰。但事实上,其他人常常会跳过敬仰你这一步。这并不是因为他们觉得你的财富不值得羡慕,而是因为他们会把你的财富当作标尺,转而表达自己渴望被爱慕与敬仰的愿望。
  • 你或许觉得你需要一辆昂贵的车子、一块豪华的手表和一座很大的房子,但我想告诉你的是,你并非真想得到这些东西本身。你真想得到的是来自他人的尊重和羡慕。你觉得拥有昂贵的东西会让别人尊重和羡慕你,但可惜,别人不会——尤其是那些你希望得到其尊重和羡慕的人。
  • 通过用金钱购买昂贵之物获得的尊重和羡慕可能远比你想象中少。
  • 比起豪车,谦虚、善良和同情心等人格特质才能帮你获得更多尊重。
  • 炫富是让财富流失的最快途径。
  • 我们总是喜欢用看到的东西为标准来判断一个人是否富有,因为这些是摆在我们面前、实实在在的东西。
  • 现代资本主义致力于帮助人们通过超前消费的方式来享受原本力不能及的物质生活,并将这种消费观发展为一个备受推崇的产业。
  • 财富并不是我们能看到的外在部分。
  • 财富是由未被转化为实物的金融资产体现的
  • 让自己感到富有的最佳方式莫过于把大笔钱花在那些真正美好的东西上。但想真变得富有,你需要做的是花自己已经有的钱,而不是透支还不属于自己的钱。事情就是这么简单。
  • 想真变得富有,唯一的途径就是别去消耗你拥有的财富。这不仅仅是积累财富的唯一方式,也是富有的真正定义
  • 人们对一次身体锻炼所能燃烧的能量的估值比实际消耗的能量高了4倍,而他们接下来平均摄入的能量大约是运动中消耗的能量的2倍。
  • 我们很容易找到有钱的人做榜样,但想找到富有的人却不容易,因为从性质上讲,他们的成功更隐蔽。
  • 富有的前提其实是克制。
  • 我们擅长通过模仿来学习,但财富看不见的特性让我们很难模仿和学习他人的经验。
  • 这个世界上有很多看起来低调但实际上很富有的人,还有很多看上去很有钱却生活在破产边缘的人。
  • 个人的节俭和储蓄行为——在金融方面的节约和高效——是金钱等式中你具备更强控制力的部分,而且在未来也会像今天一样,是百分百行得通的方法。
  • 财富是对收入扣除开支后剩下的部分进行积累的结果。
  • 即使你收入不高,你依然可以积累财富,但如果你的储蓄率不高,你绝不可能积累财富——两相对比,孰轻孰重显而易见。
  • 如果你学会用更少的钱来获得同样多的幸福感,你的欲望和所得之间就会产生积极的落差。你也可以通过提升收入来造就这种落差,但欲望和所得之间的落差才是你更容易控制的。
  • 但在金钱收支公式的两端,人们在一端投入了大量的精力,在另一端却鲜有作为。这就给了大多数人一个机会
  • 当你把存款定义为虚荣的自我和收入之差时,你就能明白,为什么很多收入不低的人很难存下钱来,因为他们每天都在和自己想要尽情炫耀并与其他炫富者攀比的本能抗争。
  • 在一个智力方面的竞争已经白热化,而很多旧有技术已经被自动化技术取代的世界里,竞争优势开始转向更加细微的软件层面,比如沟通能力、共情能力,以及最重要的一点——一个人的灵活度。
  • 当智力不再是一种持久的优势时,拥有别人没有的灵活度是少数几种能帮你拉开与别人的距离的特质。
  • 在做投资决策时,不要试图保持绝对理性,而要做出对你而言合乎情理,也就是更好接受的选择。
  • 坚持对理财来说才是至关重要的一点。
  • 医生的职责不是简单地治好病,而是使用能让病人接受的人性化手段治好病。
  • 在影响收益表现(包括收益额和在一定时间内有所收益的概率)的诸多金融参数中,相关性最大的莫过于在经济不景气的年份对投资策略的长期坚持。
  • 任何能让你留在投资游戏中的因素都会在时间方面增强你的优势。
  • 如果你一开始就对投资对象很感兴趣——这家企业的使命、产品、团队和技术等方面都非常合你的口味——那么当它因为收益下滑或需要帮助而进入不可避免的低谷期时,你至少会因为感到自己在做一件有意义的事而对损失没有那么在意。
  • 在其他一些涉及金钱的情况下,做个现实主义者也比做个绝对理性主义者强。
  • 大多数对经济和股市走向的预测都极不靠谱,但是做预测这种行为本身是合乎情理的。
  • 人生中很少有理论与现实一致的时候。
  • 历史是对变化的研究,但具有讽刺性的是,人们却将历史当作预测未来的工具。
  • 世界上总在发生过去从来没有发生过的事。
  • 历史研究的主要内容是意料之外的事件,但投资者和经济学家们却经常将其看作对未来不容置疑的指南。
  • 但是投资并非硬科学。投资从本质上说,是规模巨大的一群人根据有限的信息针对将给他们生活幸福度带来巨大影响的事情做出不完美决策的行为,而这会让最聪明的人也变得紧张、贪婪和疑神疑鬼。
  • 和金钱相关的任何事背后最重要的驱动力,是人们对各种现象的合理化解释以及对商品和服务的偏好。
  • 历史上最重要的事件往往是一些重大的、史无前例的意外事件。
  • 人们对刺激物的反应会随着时间前进而趋于稳定。
  • 为错误留出余地的行为的智慧就在于承认不确定性、随机性和概率——“一切未知情况”——的存在
  • 在尽量扩大预测与实际可能发生情况的概率之差的同时,为自己留出即使预测错误也能从头再来的余地。
  • 安全边际的目的在于让预测变得不再必要。
  • 那就是我们不能把眼前的世界看成黑白分明的
  • 在你可以接受可能出现的各种结果的灰色区域展开追求,才是最明智的前进方式。
  • 但在和金钱有关的几乎所有事务中,人们都低估了容错空间的必要性。
  • 我们不愿意留出容错空间的原因有两个。第一,我们认为一定有人知道未来会发生什么,因此承认未来的不可知性会让我们感到不舒服。第二,一旦预测成真,你就错过了充分利用该预测去采取行动的时机,会让自己蒙受损失。
  • 我们很容易低估30%的金融资产损失对自己心理产生的影响。你的信心可能在机会最好的时候严重受挫
  • 理论上的承受力和情感上的承受力之间的差距是人们常常忽略的一种容错空间。
  • 获得幸福的最佳方式是把目标定得低一些。
  • 冒险行为中的乐观偏见”或者“‘俄罗斯轮盘赌在统计学上是可行的’综合征”——当不利结果无论如何都无法接受时,人们一厢情愿地认为出现有利结果的可能性更大的现象。
  • 如果一件事有95%的概率成功,那么剩下的5%的失败概率就意味着在你人生中的某个时间点,你一定会遭遇失败。如果这种失败意味着输得精光,那么即使出现有利局面的概率是95%,这个险也不值得你去冒,无论它看上去多么诱人。
  • 杠杆——以负债的方式进行投资——把常规风险扩大到了足以导致毁灭的程度。
  • 大多数时候的理性乐观主义会让人们忽视极端少数情况下倾家荡产的可能性。
  • 随时可以做自己想做的事而且想做多久就做多久的能力,才是无限投资回报的源泉。
  • 在金钱方面,隐患最大的单点故障便是短期开支全部依靠工资,而没有在计划中的开支和将来可能需要的开支之间用存款来建立缓冲空间。
  • 如果你的理财规划只为已知的风险做准备,那么它会缺乏足够大的安全边际,是无法经受现实世界考验的。
  • 事实上,每个计划中最重要的部分,就是为计划赶不上变化的情况做好预案。
  • 我们很难预料到自己未来的想法。
  • 每个5岁小男孩在成长过程中都有过开拖拉机的梦想。在一个小男孩的眼中,没有什么工作能比每天开着拖拉机,喊着“呜呜呜,嘟嘟嘟,大拖拉机来啦”更美好的事了。
  • 一个30多岁的新手父母对人生目标的规划是18岁时的他或她无法想象的。
  • 在我们生命中的每个阶段,我们都会做出一些决定。这些决定会深刻地影响我们未来的生活。当我们实现了曾经的梦想后,我们并不总会对自己当初的决定感到开心。所以我们看到,青少年花了大价钱文身,在长大后又要花大价钱洗掉;有人年轻时急着和某人结婚,上了年纪后却盼着和同一个人离婚;有人中年时努力想得到的东西,年老后却又拼命想放弃……这样的例子不胜枚举。
  • 只有当你能给一项计划数年或数十年的时间去成长时,复利的效应才能得到最佳体现。
  • 无论在工作生涯的哪个节点,都要定下这样均衡的目标:每年做好适中的储蓄,给自己适度的自由时间,让通勤不超过适当的时长,至少花适量的时间来陪伴家人。
  • 在一个人人都随着时间改变的世界上,沉没成本——过去的决策导致的无法收回的支出——就像一头拦路虎。
  • 投资的成功需要投资者付出相应的代价,但衡量这种代价的不是金钱,而是波动、恐惧、怀疑、不确定感和悔恨——如果你不是那个直接面对它们的人,这些代价都容易被你忽视
  • 财富之神并不青睐那些只求回报却不愿付出的。
  • 投资成功需要付出的代价是我们无法立刻看到的。它不会被直观地写在标签上。所以,当你需要支付这种账单时,你会觉得这笔钱并不是为购买好东西而支付的价钱,反倒更像做错事后必须缴纳的罚款,虽然在人们看来,付账是很正常的事,缴纳罚款却是应该避免的,所以人们觉得应该采取某些明智的预防措施,让自己避免受罚。
  • 把市场波动看作要支付的价钱而不是该缴纳的罚款的视角看似微不足道,却是培养正确理财心态的重要部分
  • 如果你把波动看作要买的入场券,情况就会完全不同。
  • 几乎所有波动都是一种费用,而非一笔罚款。
  • 市场回报永远不会是免费的,现在不是,将来也不会是。你需要支付一定的费用,就像要花钱购买一件产品一样。
  • 人们常常在缺乏足够信息和不讲逻辑的情况下做出一些理财决定,之后又悔不当初。但站在他们当时的角度来看,这些决定是有道理的。
  • 投资者们总是会天真地向那些和自己情况不一样的人学习理财经验。
  • 当投资者的目标和时间规划不同时——在任何一种投资中都会出现这种情况——在一个人看来不合理的价格在另一个人看来也许是可以接受的,因为他们各自关注的因素是不同的
  • 金融领域内的一条铁律是:金钱会追逐回报的最大化。
  • 当交易者推高短期回报,更多的投资者就会开始入场。不久之后——通常时间都不会太久——短线投资者就成了最有权威的股市定价者了。
  • 泡沫与估值上升的关系不大。它体现的其实是另一种情况:随着越来越多的短线投资者入场,交易周期变得越来越短。
  • 泡沫之所以会形成,并不是因为人们在非理性地参与长期投资,而是因为人们在某种程度上堪称理性地转向短线交易,以追逐不断滚雪球式增长的积极动量。
  • 很多金融和投资决策都建立在对他人的观察、模仿或与他人对赌的基础上,但如果你不知道为什么有些人会那样做,你就不知道他们那种行为会持续多久,什么会让他们改变主意,或者他们是否会吸取教训并做出调整。
  • 尽可能努力明确自己玩的是什么游戏。
  • 但在多年前,我曾这样总结我的理财哲学:我是一名被动的投资者,但对这个世界创造货真价实的经济增长的能力持乐观态度。我相信在接下来的30年里,这种增长会让我的财富不断增加。
  • 出于一些我无法理解的原因,人们总喜欢听别人说这个世界要完蛋了。
  • 对绝大多数人来说,保持乐观都是最好的选择,因为这个世界在大多时候对大多数人来说都是越变越好的
  • 乐观主义是一种信念,相信就算过程中充满坎坷,随着时间过去,你心目中好结果出现的概率也比坏结果出现的概率大
  • 如果你告诉人们一切都会变得很好,他们可能会不以为然,或者用怀疑的目光看着你。但如果你说他们正处于危险中,你就会获得他们的全部注意力。
  • 一个在众人心怀绝望时满怀希望的人不会被看重,但一个在众人都心怀希望时满怀绝望的人却会被视为圣人
  • 人类对失去的过度厌恶是在演化过程中形成的一种保护机制。
  • 在进行直接比较或权衡时,失去带给我们的精神影响比得到更大。
  • 相比机遇,对威胁反应更快的生物成功生存和繁殖的可能性才更大
  • 悲观主义者在推测未来趋势时经常没有将市场会如何适应局势纳入考虑。
  • 经济学中有一条铁律:极好和极糟的环境都很难长期维持,因为市场的供需会以很难预测的方式对环境进行适应。
  • 眼前的问题有多糟糕,人们解决问题的动力就有多强——这是经济史中普遍存在的一种现象,却很容易被悲观主义者忽视,
  • 进步发生得太慢,让人难以发觉,但挫折却出现得太快,让人难以忽视。
  • 增长是由复利驱动的,而复利通常需要时间。毁灭却可能由独立的致命因素导致,可以在很短的时间内发生;它也可能由失去信心引发,而信心可以在一瞬间崩塌。
  • 在投资中,你必须认识到成功的代价——在长期增长的背景下出现的波动和损失——并做好为其买单的准备。
  • 故事是现存的对经济影响最大的潜在力量之一。
  • 故事是经济发展中最强大的一股力量。
  • 你越希望某事是真的,你就越容易相信一个高估其成真可能性的故事。
  • 金融领域内的很多投资观点都带有这样的特性:一旦你听从它们,选择了某种策略或方法,你就同时在金钱和心理上进行了双重投资。
  • 每个人对世界的看法都是不完整的,但每个人都会编织完整的故事来弥补其中的空白。
  • 后见之明,即人们解释过去事件的能力,给了我们一种仿佛这个世界可以被理解的错觉,也给了我们一种仿佛这个世界自有其原则的错觉,哪怕在实际上一团混乱的情况下。这是我们在很多领域犯错的重要原因。
  • 对控制力的幻想比充满不确定性的现实更容易让人接受,所以我们死死抓着某些故事不放,骗自己以为结果尽在掌握。
  • 在做计划的时候,我们会专注于我们想做的和能做的事情,而忽略了他人的计划和能力,但他人的决策也会对结果产生影响。
  • 无论是在解释过去还是预测未来时,我们都专注于技能起到的因果性作用,而忽略了运气的重要影响。
  • 我们专注于我们知道的,忽视了我们不知道的,而这让我们对自己的想法过于自信。
  • 当事态朝正确的方向发展时,要保持谦逊;当事态朝错误的方向发展时,要心怀谅解或同情。这是因为任何事都没有表面看来那样美好或糟糕。
  • 虚荣越少,财富越多。你能存下多少钱,要看你彰显自我的需求与你的收入之间的差距,而财富恰恰存在于看不到的地方
  • 用能让你睡踏实的方式来理财。
  • 如果你想提高投资回报,最简单而有效的方法就是拉长时间。时间是投资中最强大的力量。
  • 你应该始终通过衡量自己的整体投资情况,而不是根据某一笔投资的成败来评价自己的表现。
  • 利用财富来获取对时间的掌控,因为对人生的幸福感而言,最严重而普遍的扣分项就是时间上的不自由。在任何时候和喜欢的人去做喜欢的事而且想做多久就做多久的能力,才是财富能带给你的最大红利。
  • 多一些善意,少一些奢侈。
  • 存钱。存就是了。存钱不需要什么特定理由。
  • 明确成功需要付出的代价。然后做好支付的准备,因为没有什么有价值的东西是免费的。
  • 你应该喜欢风险,因为长期看它能带给你回报
  • 这些决策的目的往往不是追求最高的回报,而是尽量降低让伴侣或孩子失望的可能。
  • 我的目的并不是赚大钱。我想要的不过是独立自主而已
  • 最主要的秘诀是控制你的欲望,在能力范围内尽可能节俭地生活。自主性与你的收入水平无关,而是由你的储蓄率决定的。而当你的收入超过一定水平后,你的储蓄率是通过控制自己对生活方式的欲望决定的。
  • 在你负担得起的范围内舒适地生活,不产生过多欲望,你会避免现代西方世界中许多人要承受的巨大社会压力。
  • 退出无谓的激烈竞争,以获得内心平静为目标来调节你的行为,才是真正的成功。
  • 比起让金融资产的长期收益最大化,不用每个月还贷款的选择让我们感觉更好,因为这让我感到独立和自由。
  • 查理·芒格说:“复利的第一条原则是:除非万不得已,永远不要打断这个过程。”
  • 对大多数投资者来说,用平均成本法【一种以定期及定额投资去积累资产(包括股票及基金)的方法,即“定投”。】去投资低成本的指数基金将是长线投资成功率最高的选择。
  • 我始终坚持的投资理念是,在投资领域,努力和结果之间几乎没有关系。这是因为世界是由尾事件驱动的——少数几个变量是大部分回报的来源。
  • 我的投资策略并不依靠选择正确的行业或者把握下一次经济衰退的机会,而是依靠高储蓄率、耐心和认为接下来几十年里全球经济将不断创造价值的乐观态度。
  • 历史不过是糟心事接踵而至的过程。
  • 在第二次世界大战结束后,美国人花了75年的时间,培养出了普通家庭对债务文化的高接受度。
  • 虽然每个群体呐喊的具体细节不同,但他们呐喊的原因——至少部分原因——是在第二次世界大战后形成的对社会本该大体平等的预期落空了。他们没能获得别人获得的利益。
  • 想想这种心态一旦受到社交媒体和有线新闻网强大的传播力量催化后会变成什么样。在这些平台上,人们比以往任何时候都更容易看到别人是怎样生活的——这就像火上浇油一样。
  • 互联网让人们越频繁地接触新观点,人们对这些新观点的存在就越感到愤怒
  • 预期的调整总是晚于实际情况的变化。

长期理财规划

我为自己设计了一个为期至少10年的理财规划,规则很简单:通过蚂蚁财富,每天分别定投100元到「黄金ETF」 和「标普500ETF」,如果行情出现1%的回撤时则当日加仓50元,当盈利超过30%时则赎回30%的仓位,后边再慢慢加仓定投回去。

我们来粗略估算了一下10年下来的投资金额:

1
2
3
4
5
6
7
8
# 每年大约有250个交易日,每年的投资额为:
250天*100元*2只股票=5万元

# 多预备10%的钱来补仓
5万+0.5万=5.5万

# 10年下来就是
5.5万*10=55万

按照10年的投资回报率80%(虽然市场不可预测,但人总要有些盼头,况且我定投的两项在过去十年中回报都超过了200%),这样10年下来大概会有44万的投资回报,加上本金刚好100万。虽然这么长的时间只回报44万看起来不多,但这对我来说这是一种无痛的投资方式,用作者的话是:「用能让你睡踏实的方式来理财」。当然在这个过程中有可能还会根据我的生活水平来调整定投金额,比如5年后我提前把房贷还完了,也许就能出多个2、3倍的闲钱用于定投。另外,本段开头也说了,我这个投资计划短则持续10年,长则持续20年、30年,也许在复利的作用下收益远远不止于我上边计算出来的那么多。

]]>
+ + + + + + <p>我所读过的理财类书籍并不多,在国庆后由于人性的贪婪,在股市中损失了(对我来说)一大笔钱,机缘巧合下读了这本名叫《金钱心理学》的理财类书籍。这是我读的为数不多的觉得写的非常好的理财书之一,哪怕不限于理财类,它也是一本用来了解人性和世界观的好书,由于得到了非常好的阅读体验,从另 + + + + + +
+ + + 解决低版本SpringBoot使用langchain4j Azure 冲突问题 + + https://jiapan.me/2024/springboot-azure-openai/ + 2024-06-26T10:04:31.000Z + 2024-12-16T01:29:12.425Z + + 新公司使用的Java技术栈,我们有部分新业务需要调用 OpenAI 的接口进行交互,之前我找了一个比较轻量的SDK来调用OpenAI的接口,地址是:https://github.com/Lambdua/openai4j ,这个库作为日常使用足够了,但是一些高阶能力无法满足,而这些也是我们未来会用到的,比如:

  • 对接微软 Azure 上部署的 GPT 模型
  • Function Calling
  • RAG

把第一版功能完成后,这几天工作不是那么多,于是我从Github上找到了这个库https://github.com/langchain4j/langchain4j ,从名字就能看出来,这个项目是参考的 Python 的LangChain,Java 库的命名很有意思,很喜欢叫 xxxx4j,4j 的意思是 for Java,比如 log4j。

我大致看了一下介绍,功能还算完备,给出的demo来看使用方式上可读性也很高,更重要的一点是支持古老的Java8。于是我在项目中进行了引入,将已有代码进行了改造,在跑直接调用 OpenAI 的例子时很顺利,当我切换为 Azure 后问题出现了,报错堆栈如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
Exception in thread "main" java.lang.NoClassDefFoundError: reactor/core/Disposable
at java.lang.ClassLoader.defineClass1(Native Method)
at java.lang.ClassLoader.defineClass(ClassLoader.java:756)
at java.security.SecureClassLoader.defineClass(SecureClassLoader.java:142)
at java.net.URLClassLoader.defineClass(URLClassLoader.java:473)
at java.net.URLClassLoader.access$100(URLClassLoader.java:74)
at java.net.URLClassLoader$1.run(URLClassLoader.java:369)
at java.net.URLClassLoader$1.run(URLClassLoader.java:363)
at java.security.AccessController.doPrivileged(Native Method)
at java.net.URLClassLoader.findClass(URLClassLoader.java:362)
at java.lang.ClassLoader.loadClass(ClassLoader.java:418)
at sun.misc.Launcher$AppClassLoader.loadClass(Launcher.java:352)
at java.lang.ClassLoader.loadClass(ClassLoader.java:351)
at com.azure.core.http.netty.NettyAsyncHttpClientProvider.createInstance(NettyAsyncHttpClientProvider.java:81)
at dev.langchain4j.model.azure.InternalAzureOpenAiHelper.setupOpenAIClientBuilder(InternalAzureOpenAiHelper.java:71)
at dev.langchain4j.model.azure.InternalAzureOpenAiHelper.setupSyncClient(InternalAzureOpenAiHelper.java:51)
at dev.langchain4j.model.azure.AzureOpenAiChatModel.<init>(AzureOpenAiChatModel.java:123)
at dev.langchain4j.model.azure.AzureOpenAiChatModel$Builder.build(AzureOpenAiChatModel.java:536)

我按照堆栈的引导,一步一步去看代码,发现是在创建 HttpClient 对象时挂了,我进到 ConnectionProvider 源码中查看,确实找不到上边说的 Disposable 类,这个类来自 reactor-core 包。通过IDE跳转进的路径看到,目前项目中所使用的 reactor-core 版本是 2.0.8.RELEASE,我找到最新 3.6.7 版本的 reactor-core 源码看了下是有Disposable 这个类的。

一开始我认为是 langchain4j 的这个项目有问题,去 Github 的 Issue 中搜了下并没有相关的提问,于是我自己开始尝试动手解决,尝试了以下几种方式都不行:

  1. 直接在项目中引入最新版本的 reactor-core
  2. 排除(exclusions) langchain4j-azure-open-ai 下的 reactor-core 依赖,保证我自己引入的最新版本生效
  3. 引入 reactor-netty-core 的最新版
  4. 引入全部 langchain4j 的依赖
  5. 重启IDE
  6. 重启电脑

在做上边的第2步时,启动调试后可以看到,IDE在进入ConnectionProvider 后确实可以正常跳转进Disposable 了,但最终还是报错。通过依赖分析也没有发现和 reactor 的任何冲突,一直搞到晚上下班也没解决。

今天早上上班后我换了个思路来排查这个项目,创建了一个新项目,只引入 langchain4j 的依赖,可以正常执行,接下来我把我们项目中其他依赖项引进来,发现还是没问题,当我把 parent 引入后问题出现了。虽然 parent 的 pom 文件在远端,但IDEA提供了一个功能,可以修改本地的文件来进行调试,我用二分法删除 parent 中的依赖,最终将问题定位在了:

1
2
3
4
5
6
7
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>${spring.boot.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>

parent 中 spring.boot.version 的值是 1.5.7.RELEASE ,我在上上家公司写Java时就有这个版本了,是个非常老的版本,但升级 SpringBoot 关联的问题会更多。我继续深入进去看,在 spring-boot-dependencies 的 pom 文件中 properties 指定了reactor.version2.0.8.RELEASE,这下破案了。之前我无法通过依赖分析找到冲突,也是因为依赖是在 parent 指定的,且这个依赖版本无法在后续进行修改。

有种覆盖 parent 版本号的方式是在自己项目的父 pom 中的dependencyManagement 下进行声明,我尝试在 dependencyManagement 加上如下片段:

1
2
3
4
5
<dependency>
<groupId>io.projectreactor</groupId>
<artifactId>reactor-core</artifactId>
<version>3.6.7</version>
</dependency>

此时报了另一个错误:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
java.lang.VerifyError: class io.netty.channel.kqueue.AbstractKQueueChannel$AbstractKQueueUnsafe overrides final method close.(Lio/netty/channel/ChannelPromise;)V

at java.lang.ClassLoader.defineClass1(Native Method)
at java.lang.ClassLoader.defineClass(ClassLoader.java:756)
at java.security.SecureClassLoader.defineClass(SecureClassLoader.java:142)
at java.net.URLClassLoader.defineClass(URLClassLoader.java:473)
at java.net.URLClassLoader.access$100(URLClassLoader.java:74)
at java.net.URLClassLoader$1.run(URLClassLoader.java:369)
at java.net.URLClassLoader$1.run(URLClassLoader.java:363)
at java.security.AccessController.doPrivileged(Native Method)
at java.net.URLClassLoader.findClass(URLClassLoader.java:362)
at java.lang.ClassLoader.loadClass(ClassLoader.java:418)
at sun.misc.Launcher$AppClassLoader.loadClass(Launcher.java:352)
at java.lang.ClassLoader.loadClass(ClassLoader.java:351)
at reactor.netty.resources.DefaultLoopKQueue.getChannel(DefaultLoopKQueue.java:50)
at reactor.netty.resources.LoopResources.onChannel(LoopResources.java:243)
at reactor.netty.tcp.TcpResources.onChannel(TcpResources.java:251)
at reactor.netty.transport.TransportConfig.lambda$connectionFactory$1(TransportConfig.java:277)
at reactor.netty.transport.TransportConnector.doInitAndRegister(TransportConnector.java:277)
at reactor.netty.transport.TransportConnector.connect(TransportConnector.java:164)
at reactor.netty.transport.TransportConnector.connect(TransportConnector.java:123)
at reactor.netty.resources.DefaultPooledConnectionProvider$PooledConnectionAllocator.lambda$connectChannel$0(DefaultPooledConnectionProvider.java:519)
at reactor.core.publisher.MonoCreate.subscribe(MonoCreate.java:61)
at reactor.core.publisher.Mono.subscribe(Mono.java:4568)
at reactor.core.publisher.Mono.subscribeWith(Mono.java:4634)
at reactor.core.publisher.Mono.subscribe(Mono.java:4534)
at reactor.core.publisher.Mono.subscribe(Mono.java:4470)
at reactor.netty.internal.shaded.reactor.pool.SimpleDequePool.drainLoop(SimpleDequePool.java:437)
at reactor.netty.internal.shaded.reactor.pool.SimpleDequePool.pendingOffer(SimpleDequePool.java:600)
at reactor.netty.internal.shaded.reactor.pool.SimpleDequePool.doAcquire(SimpleDequePool.java:296)
at reactor.netty.internal.shaded.reactor.pool.AbstractPool$Borrower.request(AbstractPool.java:430)
at reactor.netty.resources.DefaultPooledConnectionProvider$DisposableAcquire.onSubscribe(DefaultPooledConnectionProvider.java:204)
at reactor.netty.internal.shaded.reactor.pool.SimpleDequePool$QueueBorrowerMono.subscribe(SimpleDequePool.java:720)
at reactor.netty.resources.PooledConnectionProvider.lambda$acquire$2(PooledConnectionProvider.java:170)
at reactor.core.publisher.MonoCreate.subscribe(MonoCreate.java:61)
at reactor.netty.http.client.HttpClientConnect$MonoHttpConnect.lambda$subscribe$0(HttpClientConnect.java:273)
at reactor.core.publisher.MonoCreate.subscribe(MonoCreate.java:61)
at reactor.core.publisher.FluxRetryWhen.subscribe(FluxRetryWhen.java:81)
at reactor.core.publisher.MonoRetryWhen.subscribeOrReturn(MonoRetryWhen.java:46)
at reactor.core.publisher.InternalMonoOperator.subscribe(InternalMonoOperator.java:63)
at reactor.netty.http.client.HttpClientConnect$MonoHttpConnect.subscribe(HttpClientConnect.java:276)
at reactor.core.publisher.Mono.subscribe(Mono.java:4568)
at reactor.core.publisher.Mono.block(Mono.java:1778)
at com.azure.core.http.netty.NettyAsyncHttpClient.sendSync(NettyAsyncHttpClient.java:199)
at com.azure.core.http.HttpPipelineNextSyncPolicy.processSync(HttpPipelineNextSyncPolicy.java:51)
at com.azure.core.http.policy.HttpLoggingPolicy.processSync(HttpLoggingPolicy.java:183)
at com.azure.core.http.HttpPipelineNextSyncPolicy.processSync(HttpPipelineNextSyncPolicy.java:53)
at com.azure.core.implementation.http.policy.InstrumentationPolicy.processSync(InstrumentationPolicy.java:101)
at com.azure.core.http.HttpPipelineNextSyncPolicy.processSync(HttpPipelineNextSyncPolicy.java:53)
at com.azure.core.http.policy.KeyCredentialPolicy.processSync(KeyCredentialPolicy.java:115)
at com.azure.core.http.HttpPipelineNextSyncPolicy.processSync(HttpPipelineNextSyncPolicy.java:53)
at com.azure.core.http.policy.CookiePolicy.processSync(CookiePolicy.java:73)
at com.azure.core.http.HttpPipelineNextSyncPolicy.processSync(HttpPipelineNextSyncPolicy.java:53)
at com.azure.core.http.policy.AddDatePolicy.processSync(AddDatePolicy.java:50)
at com.azure.core.http.HttpPipelineNextSyncPolicy.processSync(HttpPipelineNextSyncPolicy.java:53)
at com.azure.core.http.policy.RetryPolicy.attemptSync(RetryPolicy.java:211)
at com.azure.core.http.policy.RetryPolicy.processSync(RetryPolicy.java:161)
at com.azure.core.http.HttpPipelineNextSyncPolicy.processSync(HttpPipelineNextSyncPolicy.java:53)
at com.azure.core.http.policy.AddHeadersPolicy.processSync(AddHeadersPolicy.java:66)
at com.azure.core.http.HttpPipelineNextSyncPolicy.processSync(HttpPipelineNextSyncPolicy.java:53)
at com.azure.core.http.policy.AddHeadersFromContextPolicy.processSync(AddHeadersFromContextPolicy.java:67)
at com.azure.core.http.HttpPipelineNextSyncPolicy.processSync(HttpPipelineNextSyncPolicy.java:53)
at com.azure.core.http.policy.RequestIdPolicy.processSync(RequestIdPolicy.java:77)
at com.azure.core.http.HttpPipelineNextSyncPolicy.processSync(HttpPipelineNextSyncPolicy.java:53)
at com.azure.core.http.policy.HttpPipelineSyncPolicy.processSync(HttpPipelineSyncPolicy.java:51)
at com.azure.core.http.policy.UserAgentPolicy.processSync(UserAgentPolicy.java:174)
at com.azure.core.http.HttpPipelineNextSyncPolicy.processSync(HttpPipelineNextSyncPolicy.java:53)
at com.azure.core.http.HttpPipeline.sendSync(HttpPipeline.java:138)
at com.azure.core.implementation.http.rest.SyncRestProxy.send(SyncRestProxy.java:62)
at com.azure.core.implementation.http.rest.SyncRestProxy.invoke(SyncRestProxy.java:83)
at com.azure.core.implementation.http.rest.RestProxyBase.invoke(RestProxyBase.java:124)
at com.azure.core.http.rest.RestProxy.invoke(RestProxy.java:95)
at com.sun.proxy.$Proxy24.getChatCompletionsSync(Unknown Source)
at com.azure.ai.openai.implementation.OpenAIClientImpl.getChatCompletionsWithResponse(OpenAIClientImpl.java:1444)
at com.azure.ai.openai.OpenAIClient.getChatCompletionsWithResponse(OpenAIClient.java:318)
at com.azure.ai.openai.OpenAIClient.getChatCompletions(OpenAIClient.java:685)
at dev.langchain4j.model.azure.AzureOpenAiChatModel.generate(AzureOpenAiChatModel.java:257)
at dev.langchain4j.model.azure.AzureOpenAiChatModel.generate(AzureOpenAiChatModel.java:215)

回到最开始的问题,报错误的根本原因是,初始化 Azure模型时需要构造一个 HttpClient,默认情况下会使用ConnectionProvider 来构造。看了下 AzureOpenAiChatModel 的 builder 方法,支持自己传入 OpenAIClient,而 OpenAIClient 可以自己构造 HttpClient,通过这个文档看到 https://learn.microsoft.com/en-us/azure/developer/java/sdk/http-client-pipeline HttpClient 有多种实现,其中可以用 OkHttpClient 来实现,于是我进行了以下魔改:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
private static OpenAIClient setupSyncClient(String endpoint, String serviceVersion, Object credential, Duration timeout, Integer maxRetries, ProxyOptions proxyOptions, boolean logRequestsAndResponses) {
OpenAIClientBuilder openAIClientBuilder = setupOpenAIClientBuilder(endpoint, serviceVersion, credential, timeout, maxRetries, proxyOptions, logRequestsAndResponses);
return openAIClientBuilder.buildClient();
}

private static OpenAIClientBuilder setupOpenAIClientBuilder(String endpoint, String serviceVersion, Object credential, Duration timeout, Integer maxRetries, ProxyOptions proxyOptions, boolean logRequestsAndResponses) {
timeout = getOrDefault(timeout, ofSeconds(60));
HttpClientOptions clientOptions = new HttpClientOptions();
clientOptions.setConnectTimeout(timeout);
clientOptions.setResponseTimeout(timeout);
clientOptions.setReadTimeout(timeout);
clientOptions.setWriteTimeout(timeout);
clientOptions.setProxyOptions(proxyOptions);

Header header = new Header("User-Agent", "langchain4j-azure-openai");
clientOptions.setHeaders(Collections.singletonList(header));
// HttpClient httpClient = new NettyAsyncHttpClientProvider().createInstance(clientOptions);
HttpClient httpClient = new OkHttpAsyncClientProvider().createInstance(clientOptions);

HttpLogOptions httpLogOptions = new HttpLogOptions();
if (logRequestsAndResponses) {
httpLogOptions.setLogLevel(HttpLogDetailLevel.BODY_AND_HEADERS);
}

maxRetries = getOrDefault(maxRetries, 3);
ExponentialBackoffOptions exponentialBackoffOptions = new ExponentialBackoffOptions();
exponentialBackoffOptions.setMaxRetries(maxRetries);
RetryOptions retryOptions = new RetryOptions(exponentialBackoffOptions);

OpenAIClientBuilder openAIClientBuilder = new OpenAIClientBuilder()
.endpoint(ensureNotBlank(endpoint, "endpoint"))
.serviceVersion(getOpenAIServiceVersion(serviceVersion))
.httpClient(httpClient)
.clientOptions(clientOptions)
.httpLogOptions(httpLogOptions)
.retryOptions(retryOptions);

if (credential instanceof String) {
openAIClientBuilder.credential(new AzureKeyCredential((String) credential));
} else if (credential instanceof KeyCredential) {
openAIClientBuilder.credential((KeyCredential) credential);
} else if (credential instanceof TokenCredential) {
openAIClientBuilder.credential((TokenCredential) credential);
} else {
throw new IllegalArgumentException("Unsupported credential type: " + credential.getClass());
}

return openAIClientBuilder;
}

private static OpenAIServiceVersion getOpenAIServiceVersion(String serviceVersion) {
for (OpenAIServiceVersion version : OpenAIServiceVersion.values()) {
if (version.getVersion().equals(serviceVersion)) {
return version;
}
}
return OpenAIServiceVersion.getLatest();
}

从开源代码中拷贝出 setupSyncClientsetupOpenAIClientBuilder 方法,并对setupOpenAIClientBuilder 中的HttpClient httpClient 的创建逻辑进行了调整

1
2
3
4
// before
HttpClient httpClient = new NettyAsyncHttpClientProvider().createInstance(clientOptions);
// after
HttpClient httpClient = new OkHttpAsyncClientProvider().createInstance(clientOptions);

初始化Azure模型时传入我自己的 client:

1
2
3
4
5
6
7
8
9
// 默认生成的client使用NettyAsyncHttpClientProvider和SpringBoot所依赖的版本不兼容,改用OkHttpAsyncClientProvider进行重写
OpenAIClient client = setupSyncClient(System.getenv("AZURE_OPENAI_ENDPOINT"), "",
System.getenv("AZURE_OPENAI_API_KEY"), ofSeconds(30), 2, null, true);

model = AzureOpenAiChatModel.builder()
.openAIClient(client)
.deploymentName(modelName)
.temperature(0.0)
.build();

并在工程中引入 azure-core-http-okhttp 的依赖

1
2
3
4
5
<dependency>
<groupId>com.azure</groupId>
<artifactId>azure-core-http-okhttp</artifactId>
<version>1.12.0</version>
</dependency>

再次执行还是报错了,不过这次的错误变为:

1
2
3
4
5
6
7
java.lang.NoClassDefFoundError: reactor/util/context/ContextView

at com.azure.core.http.rest.RestProxy.<init>(RestProxy.java:56)
at com.azure.core.http.rest.RestProxy.create(RestProxy.java:140)
at com.azure.ai.openai.implementation.OpenAIClientImpl.<init>(OpenAIClientImpl.java:144)
at com.azure.ai.openai.OpenAIClientBuilder.buildInnerClient(OpenAIClientBuilder.java:283)
at com.azure.ai.openai.OpenAIClientBuilder.buildClient(OpenAIClientBuilder.java:351)

还是 reactor 的问题,但可以看到,现在已经不再使用 reactor.core.Disposable 了,也许升级一下 reactor-core 可以解决,我再次在项目的 parent 的dependencyManagement 下引入

1
2
3
4
5
<dependency>
<groupId>io.projectreactor</groupId>
<artifactId>reactor-core</artifactId>
<version>3.6.7</version>
</dependency>

再次尝试,问题解决。

]]>
+ + + + + + <p>新公司使用的Java技术栈,我们有部分新业务需要调用 OpenAI 的接口进行交互,之前我找了一个比较轻量的SDK来调用OpenAI的接口,地址是:<a href="https://github.com/Lambdua/openai4j" target="_blank" r + + + + + +
+ + + 为什么好久没更新了 + + https://jiapan.me/2024/Why-not-update-for-a-long-time/ + 2024-04-29T06:23:58.000Z + 2024-12-16T01:29:10.733Z + + 可以看到我在去年8、9月份频繁更新了一批文章,然后在11月就戛然而止了。

昨天早上坐在旁边的同事告诉我,他的女朋友周末把我的博客通过文字转语音的方式边听边做家务,并且想要人肉催更。每次听到有人说读了我的博客,而且希望催更,我都既兴奋又诚惶诚恐。兴奋是因为有人能喜欢读我喜欢写的东西,惶恐是因为居然有人喜欢我写的东西。

实际在看似停更的这小半年来我并没有停,并且再坚持每日一更,只不过内容放在了另一个站点上,域名是 https://diary.jiapan.me/ 。从域名可以看出,这是我写日记的地方,站点标题叫「小小的避难所」,灵感来自毛姆写的《阅读是一座随身携带的避难所》这本书的书名,正如名字写的这样,我把那里作为我的避难所来记录、倾诉我的所感所想。当然那个站点上的内容也不是每天都会更新发布,而是根据我的心情,想起来了就整理一批我在Notion中写的内容发布出去。

进入避难所有一点小小的门槛,需要留下你的邮箱和阅读原因,邮箱只要是常见域名就可以,会收到一个入场验证码,输入验证码后再说明原因就可以进入了,原因我并不会审核,只要大于5个字符就可以了。

设置门槛的原因有两个,首先是我希望让这些内容可控,我需要知道都被谁访问过,其次是我不希望这些内容会被爬虫抓到,或者说可以通过搜索引擎搜到。

为什么我把那些内容单独隔离到了另一个站点内,而没有放在这里,因为那些都是我的日常碎碎念、流水账,每一篇内容都写的很零散,每天晚上我会回顾一下今天值得纪念的事情。拿出几样来记录一下,没有任何主题。

这个博客内大部分内容都是围绕着一个主题来写的,但这种写法很费精力,而且实话实说我并没有那么多干货。当然我也知道写这种结构化的文章相比写流水账,对个人来说会有更好的提升,我想先通过记录流水账的方式把写作这个习惯培养起来,然后再慢慢进阶。

所以,如果想继续读我流水账的朋友可以左转进去我的小小避难所,但我也先在这里做个免责声明(狗头保命),那些内容确实不体系化,没有营养,没有干货,读后可能会让你大失所望。引用曹公的一句话:满纸荒唐言。

顺便说一句,昨天和老板提了离职,准备开启一段新的征程大海,去向暂时保密,等未来有了水花再回来聊一聊这段经历叭。

]]>
+ + + + + + <p>可以看到我在去年8、9月份频繁更新了一批文章,然后在11月就戛然而止了。</p> +<p>昨天早上坐在旁边的同事告诉我,他的女朋友周末把我的博客通过文字转语音的方式边听边做家务,并且想要人肉催更。每次听到有人说读了我的博客,而且希望催更,我都既兴奋又诚惶诚恐。兴奋是因为有人能 + + + + + +
+ + + 回归模型 vs 分类模型 + + https://jiapan.me/2023/Regression-model-vs-Classification-model/ + 2023-11-13T10:33:21.000Z + 2024-12-16T01:29:10.685Z + + 在机器学习中,回归模型和分类模型是两种常见的预测模型,它们的主要区别在于其预测目标和输出类型。

预测目标

  • 回归模型的预测目标是连续数值。
    • 回归模型用于预测输出变量的数值,例如房价预测或股票价格预测。
    • 回归模型试图建立输入特征与输出值之间的数值关系。
  • 分类模型的预测目标是离散类别。
    • 分类模型用于将输入实例分配到预定义的类别中,例如垃圾邮件分类或图像识别。
    • 分类模型试图学习输入特征与类别之间的关系。

输出类型

  • 回归模型的输出是连续的。
    • 回归模型生成一个实数或浮点数作为预测结果,可以是任意精度的数值。
    • 例如,预测某个人的年龄可以是一个实数,如25.6岁。
  • 分类模型的输出是离散的。
    • 分类模型预测样本属于预定义类别的概率或直接预测样本的类别标签。
    • 例如,对于垃圾邮件分类,模型的输出可以是”垃圾邮件”或”非垃圾邮件”。

小孩子都能懂的回归模型解释

回归模型就像是一个预测机器,可以帮助我们猜测事物的未来。假设你喜欢吃冰淇淋,而冰淇淋的价格通常会随着天气变化而变化。现在,我们可以观察天气情况和冰淇淋的价格,然后用这些信息来猜测未来的价格。

比如,如果明天是个炎热的夏天,天气很热,那么冰淇淋的价格可能会比较高,因为很多人想要买冰淇淋来解暑。相反,如果明天是个寒冷的冬天,天气很冷,那么冰淇淋的价格可能会比较低,因为很少人会想要吃冰淇淋。

回归模型就是通过观察过去的天气和冰淇淋价格的关系,来预测将来的价格。它会考虑到很多因素,例如天气、季节和需求,然后给我们一个猜测的价格。虽然它不能百分之百准确地猜测价格,但它可以给我们一个大概的预测,帮助我们做决策。

小孩子都能懂的分类模型解释

分类模型就像是一个分类小助手,可以帮助我们将东西归类。想象一下,你有很多玩具,例如球、娃娃和积木。现在,你想要把它们分类整理,把球放在一起、把娃娃放在一起,积木也放在一起。

分类模型就是帮助我们做这个分类工作的机器。它会观察玩具的特点,比如形状、颜色和材质,然后根据这些特点把它们分成不同的类别。就像是在玩玩具时,你可以根据它们的外观和特点来决定它们应该放在哪个盒子里。

分类模型可以帮助我们在很多不同的情况下进行分类,比如识别动物、区分水果、辨别颜色等。它可以根据事物的特征将它们分成不同的组别,让我们更好地理解和组织世界。

应用场景

回归模型的应用场景:

  1. 房价预测:根据房屋的特征(如面积、卧室数量、地理位置等),预测房屋的价格。
  2. 销售量预测:根据过去的销售数据、广告投入和季节性因素,预测未来某个产品的销售量。
  3. 股票价格预测:根据股票过去的价格数据、市场指标和新闻事件,预测股票的未来走势。
  4. 气候模型:根据历史气象数据、大气压力和温度等因素,预测未来的天气情况。
  5. 医学研究:根据患者的临床特征和生物标记物,预测患者的疾病风险或治疗效果。

分类模型的应用场景:

  1. 垃圾邮件分类:根据电子邮件的内容、发件人和其他特征,将电子邮件分为垃圾邮件和非垃圾邮件。
  2. 图像识别:根据图像的特征和内容,将图像分类为不同的对象或场景,如猫、狗、汽车或风景。
  3. 疾病诊断:根据患者的症状、体征和医学测试结果,将患者的疾病分类为不同的类别,如心脏病、癌症或糖尿病。
  4. 情感分析:根据文本的情感特征,将文本分类为积极、消极或中性的情感。
  5. 客户细分:根据客户的行为、偏好和购买历史,将客户分为不同的细分群体,以便进行个性化营销。
]]>
+ + + + + + <p>在机器学习中,回归模型和分类模型是两种常见的预测模型,它们的主要区别在于其预测目标和输出类型。</p> +<h2 id="预测目标"><a href="#预测目标" class="headerlink" title="预测目标"></a>预测目标</h2><ul> +<li>回 + + + + + +
+ + + 为什么我说宝玉是双子座 + + https://jiapan.me/2023/why-baoyu-is-gemini/ + 2023-10-16T11:34:05.000Z + 2024-12-16T01:29:12.593Z + + 我在上一篇流水账中提到贾宝玉是双子座,虽然宝玉生日在《红楼梦》原文中一直没有明确写明,但通过一些线索可以推断出宝玉生日是在夏天,且是在农历四、五月左右。证据如下:

  • 在六十三回「寿怡红群芳开夜宴」一回中,宝玉晚上想搞个 party,林之孝家过来象征性嘱咐了两句:“还没睡呢?如今天长夜短了,该早些睡,明儿起的方早……”「天长夜短」很明显是夏天的特点。
  • 六十二回「呆香菱情解石榴裙」中,由于香菱和其他姐妹玩斗草游戏才把石榴裙弄脏了,而且也已经提到这一天是宝玉的生日。「斗草」是端午节的游戏,前文中没有描述过斗草,也没提到端午节,所以姐妹们大概率是在端午节前玩的。
  • 还是六十二回「憨湘云醉眠芍药裀」,北京地区芍药的盛花期是阳历四五月间,芍药花飞了湘云一身,说明已经是凋谢期了,按照花期来推的话应该是阳历五月底。
  • 网上还有很多证据说宝玉生日就是农历四月二十六,比如根据张道士说的话、送花神等等

不管哪个证据,宝玉肯定是农历四月底五月初的生日,既阳历(公历)五月底六月初。我按照农历四月二十六这个日期,随机查了几个农历对应的公历(能力有限,只能查到1900年以后的),都是落在双子座的时间范围内。双子座的时间范围是公历5月21日~6月21日。

]]>
+ + + + + + <p>我在上一篇流水账中提到贾宝玉是双子座,虽然宝玉生日在《红楼梦》原文中一直没有明确写明,但通过一些线索可以推断出宝玉生日是在夏天,且是在农历四、五月左右。证据如下:</p> +<ul> +<li>在六十三回「寿怡红群芳开夜宴」一回中,宝玉晚上想搞个 party,林之孝家过来象征性 + + + + + +
+ + + 投资组合理论 && 风险平价模型 + + https://jiapan.me/2023/Portfolio-Theory-and-Risk-Parity-Model/ + 2023-10-09T13:45:39.000Z + 2024-12-16T01:29:10.681Z + + 投资组合理论和风险平价模型是两种与投资组合管理相关的重要概念,是金融领域中用于优化投资组合的方法。

投资组合理论

投资组合理论是由美国经济学家哈里·马科维茨(Harry Markowitz)于20世纪50年代提出的理论框架,也被称为现代投资组合理论(Modern Portfolio Theory,MPT)。该理论旨在帮助投资者在风险和收益之间取得最佳平衡。投资组合理论的核心思想是通过将多种资产组合在一起,以最小化给定预期收益水平下的投资组合风险,或在给定风险水平下最大化预期收益。

辅助理解

想象一下,你有一个盒子,里面装着各种不同的玩具,比如娃娃、小车和积木。每个玩具就像是不同的投资。现在,假设你想保护你的玩具并确保它们的价值随着时间增长。

投资组合理论就像是一种决定你的盒子里应该有多少个不同玩具的方法。你要选择适当的玩具组合,这样你才能获得最好的结果。

但是,这里有个诀窍:不同的玩具有不同的风险和回报。有些玩具可能更有价值,但风险也更大,而其他的可能更安全,但增长速度较慢。所以你需要决定你愿意承担多少风险。

投资组合理论帮助你找到合适的平衡点。它建议你选择一种玩具组合,这样你就可以把风险分散开来。这意味着如果一个玩具表现不好,其他的玩具仍然可以让你获得回报。

简而言之,投资组合理论就是帮助你选择合适的玩具组合,以平衡风险,并让你的盒子里的玩具保持增值。

如何工作

假设你有1000美元,你有三种不同的投资选项:股票、债券和黄金。

现代投资组合理论认为,投资者可以通过合理地分配资金来平衡风险和回报。

首先,你需要了解每种投资的预期回报和风险。假设股票的预期回报是10%,债券是5%,黄金是3%。同时,股票的风险最高,债券次之,黄金的风险最低。

现代投资组合理论建议你根据你的风险承受能力和目标来分配资金。假设你对风险比较保守,你可以将60%的资金分配给债券,30%分配给股票,剩下的10%分配给黄金。

通过这样的分配,你在投资组合中平衡了风险和回报。债券的较高配比可以提供稳定的回报,股票的适度配置可以获得更高的回报,黄金的配置可以提供一定的保值功能。

现代投资组合理论的关键思想是通过将资金分配到不同的资产上,以实现风险的分散化。这样,即使某个资产表现不佳,其他资产仍可以为你的投资组合提供回报。

优点

  1. 风险分散:投资组合理论强调通过将不同资产以适当的权重组合在一起,实现风险的分散化,从而降低整体投资组合的风险。
  2. 预期回报最大化:投资组合理论帮助投资者在给定风险水平下,寻找最优的资产配置方式,以最大化预期回报。
  3. 考虑相关性:投资组合理论考虑资产之间的相关性,通过选择不同相关性的资产组合,可以实现更有效的投资组合。

局限性

  1. 基于历史数据:投资组合理论通常基于历史数据来估计资产的预期回报和风险,但历史表现不一定能准确预测未来。
  2. 忽略非系统风险:投资组合理论主要关注系统性风险,即与整个市场相关的风险,而忽略了非系统性风险,即与特定公司或行业相关的风险。
  3. 需要大量数据和计算:实施投资组合理论需要大量的数据和计算,包括资产的历史表现、相关性矩阵等,这可能对个体投资者或资源有限的投资者来说是一个挑战。

风险平价模型

风险平价模型(Risk Parity Model)是一种投资组合管理方法,旨在通过平等分配投资组合中不同资产的风险,实现更平衡的风险暴露。与传统的投资组合管理方法相比,风险平价模型更加关注风险分散和资产间的相关性

风险平价起源自一个目标收益率为10%、波动率为10%~12%的投资组合,是美国桥水创始人瑞·达利欧在1996年创立的一个投资原则,既全天候资产配置原则。

辅助理解

现在,想象一下你有一张画纸,上面有很多不同的颜色。每种颜色就像是投资中的不同资产,比如红色代表股票,蓝色代表债券,黄色代表房地产等等。

风险平价模型就是一种方法,让你在画纸上均匀涂上不同的颜色。这样,每种颜色(也就是每种资产)都有相同的风险,就像画纸上每个区域的颜色一样多。

为什么要这样做呢?因为不同的颜色(或资产)有不同的风险和回报。有些颜色可能非常亮,表示它们的风险更高,但潜在回报也更大。而有些颜色可能相对较暗,表示它们的风险较低,但潜在回报也较小。

风险平价模型帮助你确保你的画纸上每个颜色(或资产)的风险都是一样的。这样,如果一个颜色表现不好,其他颜色仍然可以给你带来回报。

简而言之,风险平价模型就是让你在画纸上均匀地涂上不同的颜色,以确保不同资产的风险是平衡的,并让你的投资更加稳定。

如何工作

假设你有1000美元,你想将其投资于两种不同的资产:股票和债券。

股票通常风险较高,但潜在回报也更高,而债券被认为更安全,但回报较低。

在风险平价模型中,你不仅仅是平均分配你的资金到股票和债券上(各500美元),而是根据每种资产的风险来分配你的投资。

假设股票的风险更高,你决定将70%的投资分配给债券,30%分配给股票。这种分配是根据每种资产的风险贡献应该相等的理念来确定的。

通过这样做,你在投资组合中平衡了风险。如果股票表现不佳,对债券的较高配置可以帮助抵消损失,并为你的整体投资提供更稳定性。另一方面,如果股票表现出色,较小的配置也不会对整体投资组合的表现产生太大影响。

风险平价模型旨在通过考虑不同资产的风险来实现资产间的平衡。它帮助你进行投资多样化,并更有效地管理风险。

优点

  1. 风险平衡:风险平价模型通过平衡不同资产的风险贡献,实现投资组合的风险均衡。这可以帮助投资者降低对任何单个资产的依赖,从而提高整体投资组合的稳定性。
  2. 简单易懂:风险平价模型相对较简单,容易理解和实施。它不需要大量的数据和计算,适用于个体投资者或资源有限的投资者。

局限性

  1. 忽略预期回报:风险平价模型关注风险的平衡,但忽略了资产的预期回报。这可能导致在追求风险均衡的同时牺牲了潜在的高回报机会。
  2. 对某些资产不适用:风险平价模型在处理某些特殊资产类别(如复杂衍生品)时可能存在困难,因为这些资产的风险无法简单地衡量和比较。

投资组合理论与风险平价模型的主要区别

目标和重点:

  • 投资组合理论的目标是在给定风险水平下,最大化投资组合的预期回报。它关注如何通过资产配置来实现最佳的风险-回报权衡。
  • 风险平价模型的目标是平衡不同资产在整个投资组合中的风险贡献。它强调每个资产对总体风险的贡献应该是相等的。

风险分散方法

  • 投资组合理论通过将不同风险和回报特征的资产组合在一起,以实现风险的分散化。它考虑资产之间的相关性,并通过优化资产权重来达到风险分散的目标。
  • 风险平价模型通过平衡不同资产的风险贡献来实现投资组合的风险分散。它将风险分配给各个资产,以确保它们在整个投资组合中对总体风险的贡献相等。

考虑因素:

  • 投资组合理论考虑了预期回报、风险和资产之间的相关性。它通过优化资产配置来平衡这些因素,以实现最佳的风险-回报组合。
  • 风险平价模型更关注风险方面,特别是资产的风险贡献。它通过平衡不同资产的风险贡献来实现风险均衡,而对预期回报的考虑相对较少。

复杂性:

  • 投资组合理论在实践中通常需要更多的数据和计算,包括资产的历史表现、相关性矩阵等。它可能需要更多的复杂模型和技术分析来确定最佳的资产配置。
  • 风险平价模型相对较简单,不需要大量数据和复杂计算。它可以作为一种直观且易于实施的方法,适用于个体投资者或资源有限的投资者。

关注点:

  • 投资组合理论关注整个投资组合的特征和表现,它试图找到最优的资产配置,以实现预期回报和风险的最佳权衡。
  • 风险平价模型更关注投资组合内部的风险分散,它强调平衡不同资产的风险贡献,以降低整体投资组合的风险。
]]>
+ + + + + + <p>投资组合理论和风险平价模型是两种与投资组合管理相关的重要概念,是金融领域中用于优化投资组合的方法。</p> +<h1 id="投资组合理论"><a href="#投资组合理论" class="headerlink" title="投资组合理论"></a>投资组合理论</h1> + + + + + +
+ + + 4个最重要的企业财务指标 + + https://jiapan.me/2023/The-4-most-important-business-financial-indicators/ + 2023-10-08T13:12:31.000Z + 2024-12-16T01:29:10.689Z + + 当谈到财务指标时,净资产收益率、毛利率、净利率和市盈率是经常被提及的几个指标,这些也是巴菲特最看重的4个指标。

巴菲特是历史上最伟大的价值投资者,他的交易逻辑的核心是:寻找优质企业并长期持有这些企业的股票。如何判断一个企业是否优质?这时就可以依据上边提到的4个指标来进行判断了。

净资产收益率(Return on Equity,ROE)

这个指标可以帮助投资者评估企业的盈利能力和管理效率

净资产收益率是用来衡量企业利润与其净资产之间关系的指标。

它反映了企业利用所有者权益实现的盈利能力。

净资产收益率的计算公式为:净资产收益率 = 净利润 / 平均净资产。

  • 净利润

    指的是企业在一定时期内扣除所有成本和费用后所剩下的利润。

    • 它是企业经营活动的最终利润。
    • 净利润可以通过企业的损益表(或利润表)中的数据来计算得出。
  • 平均净资产

    是指企业在一定期间内的

    资产净值的平均值

    • 资产净值指的是企业的资产减去负债,也可以理解为企业的所有者权益或净资产
    • 平均净资产则是将期初净资产和期末净资产相加后除以2,表示在该期间内的平均资产净值。

举例

假设有一家公司,在某一年度的净利润为500万元,期初净资产为2000万元,期末净资产为2500万元。

首先,计算平均净资产: 平均净资产 = (期初净资产 + 期末净资产)/ 2 = (2000万元 + 2500万元)/ 2 = 2250万元

接下来,计算净资产收益率: 净资产收益率 = 净利润 / 平均净资产 = 500万元 / 2250万元 ≈ 0.2222 或 22.22%

在这个例子中,公司的净利润为500万元,期初净资产为2000万元,期末净资产为2500万元。

通过计算,得到平均净资产为2250万元。

最后,通过将净利润除以平均净资产,得到净资产收益率为22.22%。

这个例子中的数据说明了净资产收益率的计算方法。净资产收益率衡量了企业在一定期间内每单位净资产所创造的净利润水平。在这种情况下,净资产收益率为22.22%,表示该公司在该年度内每单位净资产创造了22.22%的净利润。

为什么净资产收益率可以评估企业的盈利能力和管理效率?

  1. 盈利能力评估:净资产收益率反映了企业在一定期间内每单位净资产所创造的净利润水平。较高的净资产收益率意味着企业能够有效地利用其资产创造盈利,表明企业在经营活动中取得了较高的利润回报。相反,较低的净资产收益率可能意味着企业的盈利能力较弱,资产利用效率不高。
  2. 管理效率评估:净资产收益率反映了企业管理层对资产的运营和配置能力。较高的净资产收益率表明企业管理层能够有效地管理和运营资产,实现更高的利润水平。这可能反映了企业在生产、销售、成本控制等方面的优秀管理能力。相反,较低的净资产收益率可能暗示企业的管理效率较低,资产配置和运营方面存在问题。

毛利率(Gross Profit Margin)

这个指标可以帮助投资者了解企业的盈利能力和生产成本控制情况

毛利率是用来衡量企业销售产品或提供服务后的毛利润与销售收入之间的关系的指标。

它反映了企业在销售过程中所保留的利润比例。

毛利率的计算公式为:毛利率 = 毛利润 / 销售收入。

  • 毛利润

    是指企业在销售产品或提供服务后剩余的销售收入减去与销售直接相关的成本。

    • 它表示企业在核心业务活动中所保留的利润。
    • 毛利润 = 销售收入 - 与销售直接相关的成本
  • 销售收入

    是指企业在一定时期内通过销售产品或提供服务所获得的总收入。

    • 它代表了企业主要经营活动的收入来源,可以在企业的损益表中找到。

如果一个行业的毛利率低于20%,那么几乎可以断定这个行业存在着过度竞争。

净利率(Net Profit Margin)

这个指标可以帮助投资者评估企业的盈利能力和经营效率

净利率是用来衡量企业净利润与销售收入之间关系的指标。

它反映了企业在销售过程中实现的净利润比例。

净利率的计算公式为:净利率 = 净利润 / 销售收入。

毛利润和净利润的区别举例?

假设有一家制造公司,它生产并销售手机。在某一年度,公司的销售收入为1000万元。与销售直接相关的成本包括原材料、直接劳动和制造费用,总计为600万元。此外,公司还有其他间接费用和费用,如销售费用、管理费用和利息费用等,总计为200万元。

根据上述数据,我们可以计算毛利润和净利润:

毛利润 = 销售收入 - 与销售直接相关的成本 = 1000万元 - 600万元 = 400万元

净利润 = 毛利润 - 其他间接费用和费用 = 400万元 - 200万元 = 200万元

在这个例子中,公司的销售收入为1000万元,与销售直接相关的成本为600万元,其他间接费用和费用为200万元。

毛利润表示在销售过程中,公司通过销售所保留的利润。在这种情况下,公司的毛利润为400万元,即销售收入减去与销售直接相关的成本。

净利润则是在扣除所有成本和费用后所得到的最终利润。在这个例子中,公司的净利润为200万元,即毛利润减去其他间接费用和费用。

毛利润关注销售过程中所保留的利润,而净利润则考虑了所有与经营活动相关的费用和收入。

毛利率与净利率的区别?

毛利率和净利率是两个常用的财务指标,用于衡量企业的盈利能力。它们之间的区别在于考虑的成本因素不同。

  1. 毛利率是企业销售产品或提供服务后的毛利润与销售收入之间的比例关系。毛利润是指销售收入减去直接与销售相关的成本,例如生产成本、原材料成本和直接人工成本等。毛利率的计算公式为:毛利率 = (销售收入 - 销售成本)/ 销售收入。毛利率衡量了企业从核心业务活动中获得的利润比例,它反映了企业在销售过程中所保留的利润比例。
  2. 净利率是企业净利润与销售收入之间的比例关系。净利润是指销售收入减去所有成本和费用,包括销售成本、管理费用、利息费用和税费等。净利润是企业最终实现的利润。净利率的计算公式为:净利率 = 净利润 / 销售收入。净利率衡量了企业在销售过程中实现的净利润比例,它反映了企业在营运活动中的盈利能力。

总结起来,毛利率关注的是销售收入和与销售直接相关的成本之间的关系,它衡量了企业从核心业务中获得的利润比例。而净利率则考虑了所有成本和费用,包括销售成本以外的费用,衡量了企业在所有经营活动中实现的净利润比例。净利率相对于毛利率更全面地反映了企业的盈利能力和经营效率。


市盈率(Price-to-Earnings Ratio,P/E Ratio)

市盈率是衡量股票相对于每股盈利的价格的指标。

它是投资者评估一家公司的股票是否被低估或高估的重要指标。

市盈率的计算公式为:市盈率 = 股票市场价格 / 每股税后收益。

  • 股票市场价格指的是股票在市场上的交易价格,也就是投资者购买或出售股票所需支付或获得的价格。

  • 每股税后收益

    是指企业每股普通股的税后净利润,也可以理解为每一股票所对应的盈利。

    • 计算公式为:企业的净利润 / 总发行的普通股数量

较高的市盈率可能意味着市场对该股票有较高的期望和溢价,而较低的市盈率可能意味着市场对该股票的期望较低。

举例

假设有一家公司,它在某一年度的每股收益为10元,而股票的市场价格为100元。

首先,计算市盈率: 市盈率 = 市场价格 / 每股收益 = 100元 / 10元 = 10倍

在这个例子中,每股收益为10元,市场价格为100元。

通过计算,得到市盈率为10倍。


优秀企业四个指标的参考值

  • ROE > 20%
  • 毛利率 > 40%
  • 净利率 > 5%
  • 市盈率 20-40 之间(按照A股标准计算得出)

净资产收益率高和净利率高的公司是否一定是好的投资对象?

ROE高和净利率高通常被视为公司财务状况较好的指标,但并不意味着这些公司一定是好的投资对象。以下是一些需要考虑的因素:

  1. 行业和周期性:不同行业的盈利能力和周期性有所不同。即使一家公司的ROE和净利率很高,如果它所处的行业面临结构性问题或周期性低迷,那么这些指标的表现可能会受到影响。
  2. 可持续性:高ROE和净利率可能是暂时的,而非持续的。投资者需要评估这些指标是否具有持续性,例如公司的竞争优势、市场地位和可持续的盈利模式。
  3. 债务水平:高ROE和净利率的公司可能通过借入资金来实现这些指标,但高负债率可能增加公司的风险。因此,投资者需要关注公司的债务水平和偿债能力。
  4. 估值:高ROE和净利率的公司可能被市场高度看好,导致其股票价格被高估。投资者需要综合考虑股票的估值水平,以确定是否存在投资机会。
  5. 其他因素:除了财务指标,投资者还应考虑公司的管理团队、战略规划、产品竞争力、创新能力以及行业趋势等因素,这些因素对于评估公司的长期投资价值也是至关重要的。

因此,高ROE和净利率只是投资决策的起点,而不是唯一的决策依据。投资者应该进行全面的研究和分析,以综合考虑多种因素,并结合自己的投资目标和风险承受能力做出决策。


ROE 和 ROI 的区别

  1. ROE是用来衡量企业利用所有者权益(净资产)创造利润的能力。它的计算公式是净利润除以平均净资产。ROE衡量了股东权益的回报率,反映了企业在投入资本的同时,通过运营活动创造的盈利能力。ROE通常用于评估企业的盈利能力和资产利用效率。
  2. ROI(Return on Investment投资回报率)是用来衡量特定投资项目或资产的回报率。它的计算公式是净利润除以投资成本,并通常以百分比表示。ROI可以用于评估特定投资项目的经济效益,衡量投资的回报程度。ROI可以用于比较不同投资项目之间的收益率,帮助投资者做出投资决策。

虽然ROE和ROI都涉及利润和投资,但ROE主要关注企业的盈利能力和资产利用效率,而ROI主要关注特定投资项目或资产的回报率,它们评估的对象和应用范围不同。

在评估企业绩效时,ROE和ROI通常会结合使用,以提供更全面的分析。ROE可以衡量企业整体的盈利能力和管理效率,而ROI可以帮助评估具体投资项目的回报情况。

]]>
+ + + + + + <p>当谈到财务指标时,净资产收益率、毛利率、净利率和市盈率是经常被提及的几个指标,这些也是巴菲特最看重的4个指标。</p> +<p>巴菲特是历史上最伟大的价值投资者,他的交易逻辑的核心是:寻找优质企业并长期持有这些企业的股票。如何判断一个企业是否优质?这时就可以依据上边提到的4个 + + + + + +
+ + + 关于早会的思考 + + https://jiapan.me/2023/thoughts-about-morning-meetings/ + 2023-09-18T13:39:44.000Z + 2024-12-16T01:29:12.537Z + + 今天周一,照例我们下午开了全组的周会,我思考了很久决定取消每日晨会,下边是我准备的发言稿。


本月最后一天是我入职 TT 的三周年,我依然向往我刚入职 TT 后近一年左右的时光,那个时候 TT 还有一点点外企文化,不具体展开讲了,用几个词形容就是:包容、信任、自驱、敢于试错。我那时也非常庆幸自己入职一家好公司,当时的 TT 被称为互联网最后一片净土,也确实对得起小而美的称号。

我一年半前主动要求过一次转岗,从直播转到推荐,刚来推荐组的时候,每次晨会听到大家工作那么饱和我都很焦虑,所以我也能体会大家现在的感受。

上周有一天kq因为白天开了一整天的会,但他手里的一个技术驱动项目进度还差一些,晚上下班后我问他走不走,他说得加班把技术驱动搞完,不然第二天早会没得说。我知道他是在开玩笑,不过那句「不然第二天早会没得说」这句话我确实也在心中说过好多次。

我不希望大家每天为了考虑早会上要说什么而有压力,甚至出现为了说点什么而被迫找点琐碎而无意义的事情做,也不希望大家靠堆砌很多工作量来证明自己的能力和重要性。我希望大家的工作可以更专注、聚焦、深入、认真、细致一些,不要东一榔头西一棒槌。我特别喜欢一句话:不要用战术上的勤奋,来掩盖战略的懒惰。

所以我打算尝试取消早会,取消也许是长期的,也许是暂时的,还要看取消后的效果和公司的要求。对于我来说开晨会是正确地做事,现在取消周会是做正确的事(大家可以想想这两句话的区别),结果是否正确现在不得而知。

不开晨会建立在大家自驱的基础上,也建立在我对大家充分了解和信任的基础上,我一直相信信任是促使人们进步的最大动力,因为信任能够让人们表现出自己最好的一面。

我们组内的方向比较多,每个人的工作内容不尽相同,每日同步给所有人的意义不是很大,靠每周周会来做一次相互了解和同步就够了。

我们现在早会最大的益处其实是收集大家日常工作中遇到的问题,我们取消了早会,大家的问题就不要再等到第二天早会上再提了,有了问题随时提,不要因为没了早会的要求就掩盖问题,如果后边发现出现了问题被掩盖的现象,我们还会恢复早会。

在团队划分上,为了便于管理和领域打通,jw 没有再把工程和核心拆成两条线,但大家也能看到kq在推荐工程上的经验比我多的多,而且在核心需求比较多的时候我也确实无法两头都顾及到。再加上由于取消早会后反馈周期的加长,项目的跟进上不可避免会相较之前难度更大,所以我在这里也给kq提个要求,后边我们两个做下分工,所有核心项目我这边都会去了解背景、方案、进度和风险,所有推荐项目kq也要做到这几点,包括内部、产品和对外支持的项目。

再回到大家的工作上,大家在有项目、有工作任务的时候就聚焦于手头的工作,力求完美。如果有几天真的没有那么忙时就适当放松,学习一些感兴趣的东西,工作应该有张有弛,一直紧绷和一直放松都不是正常的状态。大家学习的时候尽量学习和我们业务相关的东西,我们组包含了公司内两大块最重要的业务:推荐和 IM,所以要想学肯定是有的学的。我也非常鼓励大家去发现、解决、优化工作中遇到的业务和技术痛点,这会让大家获取更大收益,包括能力上的和绩效结果上的。如果公司内的业务无法满足自己,也可以学习其他自己感兴趣的东西,比如 Web3或者学一门新的编程语言等等。我推荐作为程序员的大家,有精力的话每年学一门新的语言。编程语言会限制我们的思维模式,如果你长期使用某种语言,你就会慢慢按照这种语言的思维模式进行思考。

除了工作还有大家的工作状态,每个月总有那么几天不想工作,实在不想工作的那一天就让自己松弛一些。我自己很容易焦虑,所以我很羡慕能拥有松驰感的人。根据我的经验,一个正常排期3-5天的项目如果在状态佳而且无打扰的情况下,大概率一天就能把代码写完,这种状态也叫心流,有本叫《心流》的书大家感兴趣也可以看看。

最后,希望大家未来有一天回忆起在 TT 的工作(或实习)经历觉得是有意义的,而不是给大家留下痛苦、无效忙碌的一段经历。

]]>
+ + + + + + <p>今天周一,照例我们下午开了全组的周会,我思考了很久决定取消每日晨会,下边是我准备的发言稿。</p> +<hr> +<p>本月最后一天是我入职 TT 的三周年,我依然向往我刚入职 TT 后近一年左右的时光,那个时候 TT 还有一点点外企文化,不具体展开讲了,用几个词形容就是:包容 + + + + + +
+ + + 念念的房间 + + https://jiapan.me/2023/nian-nian-room/ + 2023-09-17T11:23:42.000Z + 2024-12-16T01:29:12.097Z + + 昨晚又是一整晚没睡,因为一家人来新家开荒,除了我爸,其他人都在这里过夜。由于新家有一个卧室还没有安床,所以我妈和念念就睡在我的床上了,我打的地铺。但因为不太适应,整晚都没睡着。

半夜睡不着时,我想起了白天一件有点内疚的事:

我们有个卧室是专门给念念准备的,墙壁刷成了淡粉色,还买了她喜欢的床。装好床的那天,她高兴极了,在自己的床上蹦了好久,一直想着如何装饰自己的房间。

这次回来,她看到自己的床上放了登登的衣服,地上也有一些其他的杂物。于是,她把那些不属于她的东西全都扔到了其他房间。我当时很严肃地批评了她,告诉她如果不让别人把东西放到她的房间,她以后也就别进其他房间了。她当时一脸惶恐,赶紧把她刚才扔出去的东西一件件搬回来,以讨好我。

深夜静悄悄的时候,我想到念念在这件事上并没有错。既然我已经告诉过她那是她的房间,那么她就有权利让自己的房间保持干净和整洁。再者,还有一个月念念就6岁了,我们之前蜗居在60多平的房子里,她一直没有属于自己的空间。第一次拥有自己的房间肯定是非常想占为己有的,我可以理解她,因为我小时候也有这样的想法。想想自己小时候,如果得到了自己非常喜爱的东西,肯定也不愿意让别人糟蹋。在拥有自己房间这一点上,我觉得非常亏欠她,在北京这个寸土寸金的地方只能委屈一下她了。

我们计划国庆节前带念念去趟上海迪士尼实现她的公主梦,我对自己的唯一要求是对她多一些耐心,不要因为她的一些小孩子的无理要求而对她发脾气。我就她这么一个女儿,不宠着她宠谁呢。去迪士尼的钱用的是我准备买摩托车的钱,之前因为考试失利,摩托车驾照考了两次,第二次考完后摩托车就对我没那么大吸引力了,所以也迟迟没有订车,这笔钱拿出来带念念去玩一趟把。

距上次去远的地方玩刚好过去3年,上一次是离职上家公司入职 TT 之前,到新疆玩了一个星期,一晃三年过去了,时间真快。说到这里,我奉劝各位还没结婚、没生娃的朋友及时行乐,趁着自由能出去玩就多出去玩。也奉劝那些不想结婚、不想生娃的朋友,如果一个人过得开心,请坚持你们的想法。

]]>
+ + + + + + <p>昨晚又是一整晚没睡,因为一家人来新家开荒,除了我爸,其他人都在这里过夜。由于新家有一个卧室还没有安床,所以我妈和念念就睡在我的床上了,我打的地铺。但因为不太适应,整晚都没睡着。</p> +<p>半夜睡不着时,我想起了白天一件有点内疚的事:</p> +<p>我们有个卧室是专门给念 + + + + + +
+ + + 乔迁第一顿饭 + + https://jiapan.me/2023/first-meal-after-moving-into-a-new-house/ + 2023-09-16T12:27:35.000Z + 2024-12-16T01:29:10.961Z + + 今天一家人在新家吃了一顿团员饭,作为我们拥有新家后的第一次正式庆祝。不过还并没有完全搬过来,小登还太小、小念还需要在现在的幼儿园上完大班,所以在之后的很长一段时间内还是只有我一个人在这边住😂

上午和路秘书一起送小念去上陶艺课,她和我一起来的原因是想给那个安排我们进来的老师送两盒月饼,她知道这个活我这种笨嘴笨舌的人肯定完不成,而且我一点都不擅长这些。一开始我也觉得她完不成,认为老师不会轻易收家长东西的。没想到在路秘书的再四推让下,那个老师最后还是接了我们的东西,还主动和我们说下学期可以再给我们推荐一些其他课程。我非常非常佩服路秘书这种有社交牛逼症的人。

距下课还有一个半小时,我和路秘书压了40分钟马路,走到了一个距离上课地点最近的一个瑞幸,中间经过铁路高架桥看到一列高铁经过,路秘书跟我讲了一个当年追她的男生后来进了铁路局工作的一段故事。我们到瑞幸后我点了一杯之前没喝过的咖啡,在那里歇了20分钟,之后一人骑了一个共享单车回到了上课地点。因为平时上下班路程上的需要,我开了哈罗和滴滴两个共享单车平台的月卡,所以今天我用每个平台扫了一个,骑车就没有花钱。

中午回家后路秘书亲自操刀给我剪了个头发,以后又可以在剪头发的开销上省下一笔钱了。过程中我爸作为有8年理发经验的人进行了友情指导。之后去稻香村买了些熟食,我还给自己买了三块在疫情居家办公期间发现的一个好吃的糕点——山楂锅盔,强烈爱吃山楂口味的小伙伴尝一尝。买完熟食回家收拾了一些东西就来新家了,吃饭过程中还喝了两盅酒,现在还晕乎乎的。

小念今天带回了她的第一件陶艺作品,一只啄木马笔筒,里边插了扭扭棒做的花:

]]>
+ + + + + + <p>今天一家人在新家吃了一顿团员饭,作为我们拥有新家后的第一次正式庆祝。不过还并没有完全搬过来,小登还太小、小念还需要在现在的幼儿园上完大班,所以在之后的很长一段时间内还是只有我一个人在这边住😂</p> +<p><img src="0.jpeg" width="600px" s + + + + + +
+ + + Go Struct 不指定 JSON tag 时的默认规则 + + https://jiapan.me/2023/go-struct-with-json-tag/ + 2023-09-15T07:15:57.000Z + 2024-12-16T01:29:11.081Z + + Golang 在序列化和反序列化一个 Struct 时,如果指定了 JSON tag 会严格按照指定的 tag 内容来执行,在没有指定 tag 或 tag 大小写不精准时,会有一些默认规则。

序列化

序列化的情况比较简单:

  • 指定了 tag 的可导出字段,按照 tag 的命名进行序列化
  • 没有指定 tag 的但可以导出的字段(首字母大写)会完全按照变量命名来进行序列化
1
2
3
4
5
6
7
8
9
10
11
12
type A struct {
Case int
casE int
Cas_E int
CaSE int `json:"ok"`
}

func main() {
a := A{1, 2, 3, 4}
s, _ := json.Marshal(&a)
mt.Println(string(s))
}

上边这段代码输出:

1
{"Case":1,"Cas_E":3,"ok":4}
  • casE 这个字段没有输出,原因是因为他是个不可导出的私有字段,即使设置了 tag 也不可序列化。
  • CaSE 序列话后的 key 为 ok 是因为我们给它指定了 tag
  • 其余字段都是按照我们原本的拼写格式进行的输出

反序列化

序列化的情况稍微有点复杂,其整体的优先级为:

  • 先按 tag 匹配,后按字段名匹配
  • 有 tag 的仅匹配 tag,没有tag 的可参与字段名匹配
  • 先精确匹配,后模糊匹配
  • 多个模糊匹配的按照声明在前的匹配

我们看几个例子:

情况1,带 tag 的两个字段都无法匹配上(精准匹配+模糊匹配),不带 tag 的两个字段都可以模糊匹配上,优先赋值给前边声明的字段:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
type B struct {
Case int `json:"a"`
CaSE int `json:"b"`
CasE int
CaSe int
}

func main() {
s := []byte(`{"CAsE":2}`)
var b B
json.Unmarshal(s, &b)
fmt.Printf("%#v\\n", b)
}
// 输出:main.B{Case:0, CaSE:0, CasE:2, CaSe:0}

情况2,带 tag 的其中一个字段可以模糊匹配上:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
type B struct {
Case int `json:"case"`
CaSE int `json:"b"`
CasE int
CaSe int
}

func main() {
s := []byte(`{"CAsE":2}`)
var b B
json.Unmarshal(s, &b)
fmt.Printf("%#v\\n", b)
}
// 输出:main.B{Case:2, CaSE:0, CasE:0, CaSe:0}

情况3,带 tag 的两个字段都可以匹配上,第一个模糊匹配,第二个精准匹配:

]]>
+ + + + + + <p>Golang 在序列化和反序列化一个 Struct 时,如果指定了 JSON tag 会严格按照指定的 tag 内容来执行,在没有指定 tag 或 tag 大小写不精准时,会有一些默认规则。</p> +<h1 id="序列化"><a href="#序列化" class="h + + + + + +
+ + + 为什么用个人博客 + + https://jiapan.me/2023/why-use-personal-blog/ + 2023-09-14T13:51:09.000Z + 2024-12-16T01:29:12.597Z + + 为什么我要用一个关联了个人域名,毫不起眼的个人博客写流水账,而不是在时下更流行的公众号、简书、知乎之类这些平台发布?有以下几个原因:

不想被熟人看到

我有时会写一些不想在日常中表达的内容,这些内容不太想被熟悉的人看到。

但互联网是公开的,既然我把内容发在了网上就应该有被看到的准备,即使未来某一天看到了其实也不要紧。

更自由

使用自己的博客想写点什么就写点什么。

我的这个博客域名 jiapan.me 在国外注册、没有实名、没有备案,所以基本上没有被审查的风险,但我通常也会遵纪守法,所幸我的域名还没有被墙,站点也托管在 Cloudflare,国内大部分地区也是可以正常访问的。

更个性

使用自己的博客想怎么魔改就怎么魔改。

博客实际上是个网站,只要你懂点前端就可以对自己的站点就行修改,如果用的是一个开源的博客生成工具,那么可以直接使用其他人提前写好的主题,那么多主题总有一款符合你的审美。

再搭配上独一无二的域名,更能体现出个性了。

无压力

公众号后台可以看到粉丝数、浏览量这些数据,这无形中给了写作者很大的压力,每次写文章都会考虑这篇文章可以涨几个粉、能带来多少阅读量之类的数据。

我这个站点干脆不统计这些数据,我不在意有多少人读、多少人访问。不用每天绞尽脑汁去想如何打造10万+,如何打造爆款。

无主题限制

使用自己的博客想写什么主题就写什么主题。

在平台上写作还要为垂直领域而困扰,不同领域要迎合不同的读者,在个人网站上就没这个困扰了,我的地盘听我的。

在这里,我心情不好时可以吐槽,有了兴趣可以聊聊技术,郁闷时可以抒发感情,激情时可以干一碗鸡汤。

无广告打扰

在商业平台内发布,如果这个平台需要变现的话,会在你的文章中间或者四周插入一些广告,有些广告会很 low,很影响阅读体验。

不考虑变现

除了平台会插入广告,在一些平台上写作时写作者也可以允许平台插入广告,和平台进行广告收益分成,我目前没有将写流水账当成个营收手段的计划。

有一个词叫「私域流量」或者「私域变现」,我总觉得这种词带一些诈骗性质,我天然反感这种硬生生造出来的词。好多人说做公众号就是做私域,这也致使我反感去运营一个公众号。

发布

使用自己的博客想什么时候写就什么时候写。

在平台上写,发布是一个问题,登录后台困难重重,要经过好几道验证,发布的时候也很麻烦,需要确认一堆内容,而且大部分平台不支持Markdown,但程序员最喜欢的编辑格式就是Markdown。

在公众号发文章,还有发布频率限制,修改起来也很麻烦,何必受这个窝囊气。

符合我的习惯

公众号这类的平台采用的是 push 模式,写完一篇文章后会 push 给你的订阅者,订阅者们会陷入在一个围墙内,只能看到他们订阅的内容,逐渐生成知识壁垒,每次收到 push 来的新内容还会产生焦虑感。

博客类的站点才用的是pull 模式,内容写完后就放在这里,各位看官想什么时候看就什么时候看,在你需要的时候来它就静静的在这里。

这也跟我的性格相符,我喜欢自己做决定,不喜欢被 push 的感觉。

活的时间可能比平台长

微信只是国内的一个聊天工具,虽然国外用的也比较多,但绝大部分还是中国用户。包括知乎、简书这些平台,我不保证它们在50年后还能活着,如果它真的不在了,用户在上边发布的内容是不是也就不在了。

自己搭建一个博客,给域名续上几十年费,页面托管在一个国际主流服务商上,比如 Cloudflare或者 Github,基本就可以永存了。

不被平台限制

在平台上写作需要遵守平台规范、更加谨言慎行,一个不注意触犯了平台上的限制那篇文章可能就没了,更严重一些的整个账号就没了,意味着之前发布的文章跟着受到了牵连,之前经营的成果付之一炬。

大部分平台,尤其是公众号,是很封闭的,这也意味着你写的内容在搜索引擎上检索不到,不仅搜索引擎搜不到,出了微信就很少看到了。

我发现谷歌对独立的博客还是很友好的,我有好几篇流水账通过某个关键字可以排在首页,而且有几篇我随手记问题解决方案帮助过很多人。

证明我来过

Web 和域名都是伟大的发明,就像我前边说的,微信未来有一天会死去,但 Web 和域名服务一定会长期留在这个世界上。

虽然我是个悲观主义者,但我还是非常向往活着,我希望我的这些碎碎念能一直留在这个世界上,证明我存在过。

就像《寻梦环游记》里所说的:死亡不是生命的终点,遗忘才是。

]]>
+ + + + + + <p>为什么我要用一个关联了个人域名,毫不起眼的个人博客写流水账,而不是在时下更流行的公众号、简书、知乎之类这些平台发布?有以下几个原因:</p> +<h1 id="不想被熟人看到"><a href="#不想被熟人看到" class="headerlink" title="不想被熟 + + + + + +
+ + + 记录感激 + + https://jiapan.me/2023/record-gratitude/ + 2023-09-13T05:24:48.000Z + 2024-12-16T01:29:12.269Z + +

“对生活的感激程度其实就是生活的充实程度。当我们对生活麻木,对一切习以为常的时候,其实我们的生活就已经死亡了”

「哈佛幸福课」的第8节,讲得是感激的重要性。作者建议我们把感激培养成一种习惯,当我们感激时,副交感神经系统功能增强,使我们变平静,从而加强免疫系统。

在提到如何培养感激时,作者提了一个行动方式:每天睡前写下5件值得感激的事。

培养一个能力的最佳方式就是实践,通过一次又一次感激来培养感激,我从6月21日开始实施这个行动,不过我稍稍给自己降低了一点点要求,每天记录3条值得感激的事,我同时把这个行动项录入到 Things 中对我进行每日提醒。

我是用 Notion 来记录这些感激内容的,每个月新建一个新的页面,每天一个大标题。使用 Notion 我可以随时随地记录,比如在地铁上、公司里、家里,从第一天开始记录到今天已经将近4个月了。

每天的持续记录使我发现,原来我身边有那么多事值得感激,但我之前已经习以为常,认为这些都理所当然。在写感激过程中,感激最多的肯定是在背后支持我的家人,除此之外我还会感激之前没有意识到的事物,感激的对象也不止有实实在在的人,还有身边给我提供便利的物品。

比如下边这段:

最上边两条我感激了两位同事,一位帮我一起沟通绩效结果,另一位是我现在的 Leader,和我一起梳理一些重点项目;接下来我还感激了「哈罗单车」,那一天是个周五,天气很好,晚上下班早,我骑着单车从公司回家,一路上风景也很好;第二天8月5日是个周六,我早上开车回老家,路上狂风大作电闪雷鸣遇上了大暴雨和大雾,我和我的车经过4小时路程,它安全的把我带回了家;最下边那条,是我回家后带念出去玩,突然感觉她长大了,之前在游乐场玩的时候一定要我陪着,这一次她可以自己玩耍了。

再来随便看两天的:

这两天也是周末,我感激了华为安装师傅、感激了家具安装师傅、感激了木工师傅、感激了北京的交通、感激了念念、感激了游戏厅的抓娃娃机。

在我写这篇流水账翻看这些感激记录的过程中,又能回忆起当时的喜悦,一定程度上起到了日记的作用。每条记录用一句话描述,不会有很大的写作压力,刚开始确实不容易发现那么多值得感激的事,随着自己记录的越来越多,就会越擅长发现生活中值得自己感激的地方。

有时我还会感激自己,比如下边几条:

在记录感激的时候,我不会强迫自己,如果某一天心情实在糟糕,可以允许自己只写一两条,某一天过得充实的时候也写过六七条。

通过记录这几个月的感激,我能很明显感受到自己情绪好了很多,不再那么偏激,能够从积极的方面思考问题了,注意力会放在积极正面的事情上,和其他人打交道时会思考对方有什么优点值得我学习,有时我还会把之前遇到后会非常生气的事换个思路去看。

我们应该心怀感激,而不是等到不幸发生时才意识到之前的自己错过了多么美好的时光。

世界上有很多美好的事物,但我们很快就会适应且不再察觉它们。每天两次花一分钟时间留意周遭的一切,花一分钟的时间,在上班的路上看看美丽的草地、青翠的树、美丽的雪。晚上用一分钟去回忆,回想你度过的一天,写下让你心怀感激的事物。

]]>
+ + + + + + <blockquote> +<p>“对生活的感激程度其实就是生活的充实程度。当我们对生活麻木,对一切习以为常的时候,其实我们的生活就已经死亡了”</p> +</blockquote> +<p>「哈佛幸福课」的第8节,讲得是感激的重要性。作者建议我们把感激培养成一种习惯,当我们感激时,副 + + + + + +
+ + + 又一晚没睡 + + https://jiapan.me/2023/another-night-without-sleep/ + 2023-09-11T21:49:13.000Z + 2024-12-16T01:29:10.733Z + + 现在是早上5:11,昨晚11点半躺下后没有任何睡意,眼睁睁一直躺到现在

中间尝试读书、冥想、听相声都没有缓解

刚刚把小红书、Twitter、脉脉这些会给我制造焦虑或者杀时间的 APP 卸载了

我第一次失眠是在高中时,在这之前我是每天都要午睡的体质

高中时非常喜欢班里一个女生,她也喜欢我

第一次失眠的原因是我们考试考砸了,我向她保证我们一起好好学习

然后那个晚上整晚都在迫切的希望自己早点睡着,早上早点起来开始学习

结果第一个不眠之夜就这么诞生了

到现在十五年了,不要说午睡,晚上很容易整晚无法入睡

运气好的话有时可以靠一片处方安眠药胡乱睡几个小时

高中时就开始了为了治疗失眠的求医之路

我也忘了那时候都吃些什么药了,反正是一把一把吃,也不见效

从失眠第一天开始,就像突然失去了睡眠的这项基本技能

躺在床上很虚无,忘记了该如何入睡

现在我会定期去医院的神经内科,开精神类处方安眠药

为了方便我都是挂周末取药的临时号,好几次医生都劝我挂个普通号或者专家号好好看看

但当我说我这个症状已经十几年了之后,医生也就不再说什么

据说失眠的人会出现在别人的梦里

既然我失眠了,希望梦到我的那个人可以一夜好眠

]]>
+ + + + + + <p>现在是早上5:11,昨晚11点半躺下后没有任何睡意,眼睁睁一直躺到现在</p> +<p>中间尝试读书、冥想、听相声都没有缓解</p> +<p>刚刚把小红书、Twitter、脉脉这些会给我制造焦虑或者杀时间的 APP 卸载了</p> +<p>我第一次失眠是在高中时,在这之前我是每天 + + + + + +
+ + + 注定进不去的大厂 + + https://jiapan.me/2023/not-get-into-big-factory/ + 2023-09-11T05:28:23.000Z + 2024-12-16T01:29:12.097Z + + 前几天,从我当前所在公司离职不久去了程序员终点站字节跳动的领导联系我,问我考不考虑机会,我考虑几分钟后委婉的拒绝了,这不是我第一次拒绝大厂基本唾手可得的机会,之前也有其他前领导联系过我去小红书负责他下边新开的业务线,也有过百度、快手之类的机会。

这篇流水账我想聊聊我选择不去大厂的几个原因。

换工作是件严肃的事

大学刚毕业时,因为年少轻狂,那时候互联网环境也比较好,两年内跳了3次。因为有过频繁换工作的经历,到后来我就对换工作这件事没那么强的意愿了,再换工作时会认真权衡利弊,而且给自己定下了之后每份工作要做3年以上的目标。

到今年我已经工作8年多了,已经换过不下4份工作,换工作都是一件成本极高的事,不管是对个人还是对前东家或者新东家。尤其是对个人,换工作后要重新熟悉环境,重新结交人脉、重新认识上下游、重新了解新公司的技术栈…

刚换工作后的半年内很多事情对我来说都会是全新的。因为成本极高,所以换工作一定一定要慎重,今年五月份我们组有过一轮人员地震,有三个同学因为出国或者回老家发展,在深思熟虑后选择了离职,还有两个看到突然走了好几个人心里痒,仓促的面了外边的机会,匆匆忙忙跳了槽,前段时间聊起来那些匆忙跳槽的都有些后悔。

工作时长

我现在所在公司,平均工作时间是10:30-19:30,去掉中午2小时休息时间,工作时长为7小时。尽管我中午不午休,拿这个时间来运动、看书、刷题、写流水账,但这也是一大块属于我自己的时间,不管上午的事情有没有完成,午休这段时间都不会有人来找我。

去大厂后,晚上七点半下班基本属于奢望了,至少会再多出2个多小时的工作时长,相比现在的工作时长多出了30%,按照现在的市场行情,我不确定我通过跳槽可以再获得30%以上的涨幅,而且即便是获得了30%涨幅,按照工作时长来算,我也只是平薪跳槽,划不来。

我现在的团队也招了2个从字节跳槽进来的新同学,这边让他们很满意的一点是晚上9点后不可能有人突然拉他们进会。我告诉他们,不仅晚上9点后不会,晚上8点后就不会有人再找你了,除非线上炸了。

不知道是不是自己身体不行,我是真的卷不动,下午7点后没有任何想工作的动力,不知道大厂里每天干到晚上十点多的同行们是怎么坚持下来的。

个人能力

这不是谦虚,我在很多方面都不具备大厂喜欢的能力,比如应试能力。我觉得大厂面试和中国的应试制度有些相似,通过背一些工作中实际用不到的八股问题进行面试,通过多伦面试后进入公司,而不是看一些更实际的能力,我也能理解这种做法,因为找工作的人太多了,这是最高效筛选人才的一种方法。

我在做面试官的时候不喜欢问八股文,我会主要关注对方在工作之余做了些什么、写过什么软件。如果一个人不爱一件事,他就不可能把它做得真正优秀,要是他很热爱编程,就不可避免地会开发自己的项目。

我那个去字节的领导跟我说他们在新员工入职第三个月的时候要做工作汇报,入职这三个月内并不是像我现在公司这样给新员工充分的时间安心学习新东西,而是上来就介入工作,在汇报时不仅要讲自己对这三个月工作的理解,还要讲工作的成果和输出。这种做汇报展示成果的能力也是我欠缺的,我也不擅长公众演讲。

换工作就是换Leader

大厂因为发展快,人员变动也相对较快,我遇到好几个朋友和我说他的 Leader 比他小,另一个说他的 Leader 是95后之类的。

一个好的直属 Leader 对工作体验太重要了,在工作中伴随我们最久对我们的影响最大的人就是直属 Leader。我不太相信一个工作两三年的人有特别好的管理能力。对管理的认识虽然可以靠书本学习一些驭人之术来提升,但更多的是靠人生阅历,前者是 PUA,后者是真正的管理。但要做到后者是需要时间的,就像我们不可能找10个孕妇来一个月内生出一个宝宝一样。

我那个去了字节的领导第三天就要求去参加季度规划会,之前他的话语权很重,大家都会听他的,但他在字节的第三天,就在会上比被自己小的产品经理diss,问他是不是不了解背景,质疑他的能力。这也是我前边说过的温情,一个稍微成熟点的,有点社会阅历的成年人不会对一个刚入职3天的人讲出那种话。我的自尊心很重、心眼很小,承受不了职场PUA…

有人生阅历的 Leader 更加善而坚定,更加有管理上的温情,这样的领导能站在员工的角度理解员工,照顾员工的感受,真正为员工着想。

另外大厂里还会有各种「嫡系」文化,在有裁员指标时,通常裁的不是能力不行的,而是非嫡系的。在有晋升指标时最先安排的也是嫡系里的“自己人”。

鸡头与凤尾

我深知人外有人天外有天,我可以在小公司里混的如鱼得水,但放到大厂的人才荟聚的地方也许就是一颗再普通不过的螺丝钉。

我不想在一个默默无闻的岗位工作,这种地方不会让我感受到成就感,很容易失去工作的动力。而大公司就是这么一个地方,大公司会使得每个员工的贡献平均化,这是一个问题。我觉得,大公司最大的困扰就是无法准确测量每个员工的贡献。

做宽与做专

我也许更擅长把一件事情从0做到80分,但从80做到100甚至120分不是我擅长的,而这是在大厂里需要具备的精益求精的能力。我更喜欢做宽而不是做专,喜欢做个八面手而不是一颗螺丝钉,由此也可以看出小公司更适合我一些。


我不想离开现在的公司的最主要原因还是工作时长方面,虽然现阶段的我需要钱,去大公司确实可以用时间换钱,但综合考虑各种因素,对于这个年龄和家庭情况的我已经不再合适。留给那些还年轻、还有梦想的年轻人们去闯一闯吧,未来属于他们。

]]>
+ + + + + + <p>前几天,从我当前所在公司离职不久去了程序员终点站字节跳动的领导联系我,问我考不考虑机会,我考虑几分钟后委婉的拒绝了,这不是我第一次拒绝大厂基本唾手可得的机会,之前也有其他前领导联系过我去小红书负责他下边新开的业务线,也有过百度、快手之类的机会。</p> +<p>这篇流水账我想 + + + + + +
+ + + 第三者的重要性 + + https://jiapan.me/2023/the-importance-of-third-parties/ + 2023-09-10T14:19:57.000Z + 2024-12-16T01:29:12.537Z + + 看到第三者这个词是不是想歪了?我这里指的是一个事件中的第三方参与者。

我举个例子,你媳妇和你妈吵架,你在他们中间就属于第三者,你起到的作用举足轻重,处理好能家和万事兴,处理不好能鸡飞狗跳。我不知道其他人,我是非常不擅长处理这种事的,我经历的鸡飞狗跳太多了🥲。

我不是一个合格的第三者,但我非常敬佩能把事处理的非常妥当的那些第三者。在我看来合格的第三者应该像袭人那样,大事化小、小事化无。

有一回宝玉去薛姨妈家,伺候宝玉的李奶妈拦着不让他多吃酒,回去后李奶妈还把宝玉给晴雯留的豆腐皮包子吃了,宝玉要喝茶时小丫头们说李奶妈把他泡好的枫露茶喝了,宝玉气的摔了茶碗,嚷嚷着要把李奶妈赶出去。不一会贾母房里的小丫头就来问发生了什么事,袭人站出来说是她不小心喝水时打碎了杯子,她不想大晚上的让贾母担心,没有提任何李奶妈的事。

后边还有一回李奶妈把宝玉留给袭人的酥酪吃了,袭人外出回来后,宝玉让人去把酥酪取来,丫鬟们回李奶妈吃了,宝玉正要发火,袭人说“原来是留的这个,多谢费心。前儿我吃的时候好吃,吃过了好肚子疼,足的吐了才好。他吃了倒好,搁在这里白糟蹋了。”就这样又化解了这一次危机。如果换成其他爱作妖的丫鬟,比如晴雯这种爆炭脾气的,非得把事闹大了不可(一会我说个关于晴雯的事)。

宝玉也有很多大事化小、小事化无的高光时刻,说一个例子,一次藕官在大观园里烧纸钱祭奠已经死去的、她之前的戏搭子菂官,被一个老婆子撞见,老婆子抓着藕官要去找太太。宝玉经过遇到此事,按常理,宝玉也可能会责备烧纸钱的人,但他看到藕官满面泪痕,他心想这个小戏子一定有她的心事,背后有无法言说的委屈,宝玉先把事情的真实原因放在后边,先自己站出来说是他让藕官烧的,就这样救下了藕官。

只要将心比心,你就会对一个人的伤心有所关怀,它既不是法律,也不是道德,而是在法律跟道德之外人内心最柔软的那个部分。

公司里,小领导在不同场合对自己的下属进行评价也能看出是否是一个合格的第三者。大老板们不了解一线员工的状态,需要小领导来反馈一下,如果小领导总抓着其他人的缺点去评判,不能避重就轻、善于发现别人的优点是万万不可的。你随随便便几句话,可能带给对方的就是天差地别的结果。

我们不要做老好人,也不要做煽风点火、唯恐天下不乱的人。如果能预测到一件小事在往恶性的方面发展,而你又是参与其中的一个人,不妨尝试化解一下。

不光宝玉身边的袭人,凤姐的特别助理平儿在这方面做的也非常出色,最著名的一回莫过于「俏平儿情掩虾须镯」,这一回中平儿和晴雯的处理方式形成了极大的反差。平儿在大雪天跟宝玉、湘云一起在野外烧烤,吃鹿肉时把镯子摘下放在了一旁,吃完后发现不见了,经过排查发现是宝玉屋里小丫鬟坠儿偷拿了。

平儿考虑到宝玉对丫鬟们很好,原文是这样写的:“我赶忙接了镯子,想了一想:宝玉是偏在你们身上留心用意、争胜要强的。”,「留心用意」,是说宝玉没有用管理丫头的方法管理她们,他相信人性有一种更高的自觉;「争强要胜」是说他希望自己房里的丫头,没有严格的法的约束也能有人性的自觉。如果平儿把这件事爆出来宝玉肯定会被人议论过于放纵自己的丫鬟,而那个丫鬟也会被赶出去,在那个社会如果一个丫鬟被一个大户人家赶出去基本就等于判了死刑(后边晴雯就是这么死的),所以平儿想把这件事掩盖下来,以后让大家提防着点坠儿就好了。她和麝月商议后打算不把这件事告诉正在生病的爆炭脾气的晴雯,谁成想他们的对话被宝玉听到了,宝玉还是转述给了晴雯,晴雯气的对那个小丫鬟又打又骂,假借宝玉之名把坠儿赶了出去。

思考:坠儿出了这种事,等于是宝玉对人性实验的一次失败,可是最大的为难在于,十次有九次失败,我们还要不要为那一次留下余地。

同样的事件,用不同的方式表达,起到的效果也大不一样,比如一个总打败仗的将军,我们可以说他屡战屡败,也可以说他屡败屡战,两个读起来相近的句子,含义却差了十万八千里,这又涉及到了语言的艺术。

最后讲个有趣的典故吧,大家都听过一个顺口溜「二十三,糖瓜粘」。”糖瓜”是一种用黄米和麦芽熬制成的粘性很大的糖,为什么腊月二十三要做糖瓜呢,因为传说这一天灶王爷要去天上,像玉帝报告每户人家这一年做了好事还是坏事,所以百姓们就把糖黏在炉口来贿赂灶王爷,意思是让灶王爷嘴巴甜一点,上天以后讲这一家人的好话。

民间的有趣就在于,他们会觉得没有什么东西是躲不过去的,就看你用什么方法。这个跟人的生命力有关。所谓生命力,就是灾难不再是灾难,危机不再是危机。我们在生活中,有时候遇到一点小事就觉得过不去了,其实就是生命力弱了。

]]>
+ + + + + + <p>看到第三者这个词是不是想歪了?我这里指的是一个事件中的第三方参与者。</p> +<p>我举个例子,你媳妇和你妈吵架,你在他们中间就属于第三者,你起到的作用举足轻重,处理好能家和万事兴,处理不好能鸡飞狗跳。我不知道其他人,我是非常不擅长处理这种事的,我经历的鸡飞狗跳太多了🥲。 + + + + + +
+ + + 每个男生心中都有自己的女神 + + https://jiapan.me/2023/every-boy-has-his-own-goddess/ + 2023-09-09T04:27:48.000Z + 2024-12-16T01:29:10.957Z + + 我在想一个问题,是不是每个男生心中都有自己的女神?比如刘亦菲、林志玲、新结恒衣,我的女神有点特殊。

初中时和我一个班的有个L姓女生,因为长得好看性格又好非常受欢迎,那个时候她有非常多的追求者。她和班里一个当时身高已经超过一米九的韩国籍男生交往过(我当时上的是一个国际学校,有很多韩国交换生),和体育委员交往过,还和其他班的男生交往过,这几个仅是我知道的,不知道的可能更多。

我和她是同一个县的,每周五会一起坐大巴车回老家,我也鼓起过勇气约她来我家一起写暑假作业,那时候真的只是写作业而已。我知道自己几斤几两,也听到过她在私下里对我的评价,知道自己不可能,而且看到她身边整天有那么多人围绕,很是羡慕,甚至有些自卑,所以不敢有任何逾矩的想法。

好巧不巧,我们两个高中又去了一个学校,但这次没有在一个班里。她凭借着自己的优势又成了学校里的小红人,我们班也有好几个仰慕者。其中有一个W姓的男生看她戴了红框眼镜,在还不知道她名字的情况下就用了小红这个昵称来称呼她,当这个W姓的男生知道我和她是老乡后羡慕不已,整天问我很多问题,比如:你说小红有男朋友了吗?小红喜欢什么样的男生?我作为他的可靠线人乐此不疲的和他一起探讨。

高中时她偶尔遇到糟心事的时候会和我这个不可能的备胎倾诉,可能因为我那时候没什么经历,也不能给她出什么好建议,给她出主意的人很多,能静静听她讲的没几个,她就把我当成了一个特别好的倾诉对象。

高考时她通过艺考去了湖南的一所大学,我留在了河北,她的大学生活非常丰富,我就通过她的朋友圈又看了她四年,我也会有一搭没一搭的在微信问候一下她。那时候流行微博,我还在微博上偷偷关注了她。她和我说她想用Instagram,我就指导她一步步进行科学上网的配置,后来也顺理成章关注了她的Instagram账号,她Ins上的很多照片是没有发在朋友圈和微博的,我就觉得自己发现了她的秘密基地,有些窃喜。

一转眼又4年过去了,大学毕业前我在石家庄实习,本来是打算留在石家庄了,可看同学们都来了北京有些心痒痒。毕业第二天给公司领导提了离职,同一天收到了北京的一个面试通知,我在公司楼道里和对方聊了几句,对方问了我一些问题就给我发了offer,如果是现在这么卷的环境我肯定连一个offer也拿不到。

等我到北京开始上班后,看到L回石家庄了,准备在石家庄创业之类的,而且看起来是单身状态。我有些后悔来北京,幻想如果没有来我是不是也许会有什么机会?但既然已经来了就好好在北京发展吧,我们继续有一搭没一搭偶尔互相发个消息。

又过了半年她可能在石家庄不太顺利,也来了北京,在北京找了份工作,没多久在北京认识了新的男朋友,又没多久和男朋友吵架对方把他赶出去,她当时不敢和家里说,也没钱在外边住,就找我借了几千块钱,我毫不犹豫借给了她,这笔钱过了好久才还回来。

我刚来北京不久有段创业经历,是做一个类似探探的产品,我邀请她来我们APP注册发照片。每天通过后台数据看到她被很多人点赞我内心里替她高兴。

再后来我结婚了,作为同学、老乡的身份会继续每隔几个月问问她怎么样,不知道是不是巧合,有好几次我问她的时候都碰巧她遇上困难,和我聊聊她的遭遇。

实际上我们从高中毕业后就再也没见过面,之后的所有交流都是在微信上,她有时也会突然来找,甚至还和我说她梦到了我。

现在她的生活依然丰富多彩,全国各处旅游打卡吃美食,而且是个滑雪手、摩托车手。工作也是换了一份又一份,很早之前我问时是在做婚礼策划,过一段时间再问时准备开个精酿小酒馆,她就像一个神,让人捉摸不定,我是泯然众生中的一个守望者。

2021年她在朋友圈晒了结婚证,巧的是那个男生也姓贾。去年他们举办了婚礼,她穿婚纱真好看。

]]>
+ + + + + + <p>我在想一个问题,是不是每个男生心中都有自己的女神?比如刘亦菲、林志玲、新结恒衣,我的女神有点特殊。</p> +<p>初中时和我一个班的有个L姓女生,因为长得好看性格又好非常受欢迎,那个时候她有非常多的追求者。她和班里一个当时身高已经超过一米九的韩国籍男生交往过(我当时上的是一 + + + + + +
+ + + 富过三代才懂吃穿 + + https://jiapan.me/2023/rich-three-generations/ + 2023-09-08T06:01:43.000Z + 2024-12-16T01:29:12.289Z + + 读过《红楼梦》的朋友一定对其中的一道菜印象深刻:「茄鲞」。王熙凤讲述这道菜的做法是:把才下来的茄子把皮削了,只要净肉,切成碎钉子,用鸡油炸了,再用鸡脯子肉并香菌、新笋、蘑菇,五香腐干、各色干果子,俱切成钉子,用鸡汤煨了,将香油一收,外加糟油一拌,盛在瓷罐子里封严,要吃时拿出来,用炒的鸡瓜一拌就是。

“鸡瓜子”是什么?就是用手撕出来的鸡小腿部分的腱子肉。因为常常活动,所以那块肉的弹性最好。富贵之家能把一个食之无味的茄子,经过这么复杂的环节来制作,做的这么精细。

还有一次宝玉被他的爸爸暴打后,王夫人问他想吃什么,宝玉回说:“也倒不想什么吃,倒是那一回做的那小荷叶儿莲蓬儿的汤还好”。这个莲蓬汤倒不是什么山珍海味,只是做起来很麻烦,当年元妃省亲时做过一次。因为是给皇帝准备吃的,非同小可,既不能过于奢华,又要十分讲究。莲蓬是用银模子刻出来的,库房的人把模子找出来后,薛姨妈看到后说:“你们府上都想绝了,吃碗汤还有这些样子。若不说出来,我见这个也不认得这是作什么用的”。薛姨妈也是大户人家,就连她都没见过这么精细的模子,可想而知贾家在饮食上有多么讲究了。

相较于富贵过好几代的家族,暴发户是不知道怎么吃的,以为大鱼大肉就叫吃了。富贵人家吃的其实并不是山珍海味,他们讲究的是做工的细腻,到最后就变成了文化。

除了在吃上,贾家在穿戴上也是非常讲究,举几个例子:贾母的软烟罗、平儿的虾须镯、宝玉的雀金裘、湘云的凫魇裘……。

通过上边这些内容,我想引出一个更普世的观点:没有钱的人永远无法想象有钱人过的是什么样的生活,平时会使用什么样的东西,就像段子中皇帝的金锄头一样。


下边用几个我使用过的稍微好一点的物品举例,这些物品价格确实会稍贵一点,但也不是什么奢侈品,限于我目前的人生阅历也只能用这些来说明了。

戴森吹风机

一个戴森吹风机3000多,普通家庭是绝对不会买的。我们家几年前一直在用其他品牌的吹风机,也没感觉有什么问题,后来我们帮一个保险销售介绍客户,完成了很多任务,作为奖励她送了我们一台戴森吹风机,自从用上以后就再也用不惯其他吹风机了。

前一阵子搬家,我把戴森拿到了新家使用,因为我爸妈还要在之前的房子住,那边需要一个新吹风机。我看到这两年一个国产的品牌「徕芬」吹风机很火,外形也和戴森很像,就买了一个给他们用。前两天我回家用了一次徕芬,实话实说,如果我之前没有用过戴森,我一定觉得这个吹风机非常好用,但用过了更好的对比之下才知道还有很大差距。

室内隐形门锁

传统的门锁都会外露一个弹簧的探头,用来在关门时将门卡住。探头上下两个角很尖,不注意时会磕碰到人,家里有小孩的情况下,如果小孩正好跟门锁差不多高,在跑来跑去时会更危险。日常关门时,因为探头要和门框上的凹槽摩擦,还会有很大的噪音。因为探头存在阻力,在关这种门时,通常是用手把门把手转到下边,再去将门关严,或者需要很用力地去关。

装修新房时,才知道现在已经有了无形的锁具,门在开启状态时探头是不会外漏的,只有将门关闭后探头才会弹出,避免了磕碰还更静音了。想把门关严时也不用捉着把手去关了,直接推门就可以。我没有研究它的原理,猜测是用了磁铁之类的。

花洒 && 零冷水

另一个和装修有关的是新家里的淋浴设备和零冷水燃气热水器。在我没有用新的花洒之前,没觉得之前用过的花洒有什么问题,用过之后觉得之前花洒水量太小了。前两天再去用之前的就觉得身上的沫子半天才能冲干净,新的淋浴一瞬间就冲完了。

还有支持恒温的零冷水燃气热水器,如果没有接触过,我真的不知道洗澡水居然可以不需要等待,每次打开直接出热水,温度也是之前设置好的恒定温度,完全不用担心忽冷忽热的问题。

自助餐

上个周末和家人去吃了一次比格自助,79一位。如果家庭条件一般,自助吃的比较少的话,就会觉得比格很不错了,当然比格在这个价位里也确实不错。但如果吃过更好的,就会知道比格的食材还差很多。

其实我也没什么资格评判比格,因为我吃的比较多的也是比格或者比格这个价位的自助,只是在公司团建的时候有幸吃过其他稍微高档一些的,比如第六季、水木锦堂之类的。但次数有限,那些更高级的,上千块的自助还没有体验过。


我现在只开过 20w 以内的车,已经觉得很好了。50w 以上的车还没有开过,更别提百万级别的豪车了。我相信我现在一定无法想象出开豪车的体验和惊喜。

如果我以后有机会能开上,再来更新使用体验😂

]]>
+ + + + + + <p>读过《红楼梦》的朋友一定对其中的一道菜印象深刻:「茄鲞」。王熙凤讲述这道菜的做法是:把才下来的茄子把皮削了,只要净肉,切成碎钉子,用鸡油炸了,再用鸡脯子肉并香菌、新笋、蘑菇,五香腐干、各色干果子,俱切成钉子,用鸡汤煨了,将香油一收,外加糟油一拌,盛在瓷罐子里封严,要吃时拿出 + + + + + +
+ + + 解决由于 AWDL 导致 Mac 的断网问题 + + https://jiapan.me/2023/AWDL-Mac-disconnected/ + 2023-09-07T04:24:35.000Z + 2024-12-16T01:29:10.197Z + + 我的电脑在公司使用无线网络时经常性断网,为了有稳定的网络我在工位时经常接根网线,使用网线连接。之前公司运维给了个叫 WiFriedX 的工具来解决这个问题,最近发现问题又出现了,开会时断网非常耽误事,所以就又着手开始排查。

最后定位到是苹果搞的 AWDL 引起的,AWDL 全称:Apple Wireless Direct Link 苹果无线直连,用于 AirDrop、AirPlay 和其他服务的低延迟高速率 WIFI 点对点传输功能。苹果为它提供了独立的网络接口,可以通过 ifconfig awdl0 看到其状态。

苹果的操作内核为1个 WiFi Broadcom 硬件芯片提供了多个 WiFi 接口:

  • en0:主要 WiFi 接口
  • ap1:用于 WiFi 网络共享的接入点接口
  • awdl0:苹果无线直接链接接口

通过拥有多个接口,我们的电脑就能够在 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
2
3
4
5
6
7
8
9
10
11
#!/usr/bin/env bash

set -euo pipefail

while true; do
if ifconfig awdl0 |grep -q "<UP"; then
(set -x; ifconfig awdl0 down)
fi

sleep 1
done

这段逻辑会每秒钟检测一次 awdl0 网卡状态,如果是开启就进行关闭。

运行这段代码可以达到永久关闭 awdl0 网卡的效果,但是如果是我们每次手动运行它会比较麻烦,每次重启电脑后还要记得再次运行。于是大神们继续封装,将这个代码在系统后代常驻运行,重启时也会自动启动。

永久关闭 AWDL

通过下边这个命令,可以把上边的脚本放在后台服务中一直执行,同时跟随系统启动:

1
curl -sL https://raw.githubusercontent.com/meterup/awdl_wifi_scripts/main/awdl-daemon.sh | bash

恢复 AWDL

关闭后会影响 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
2
alias awdldown='curl -sL https://raw.githubusercontent.com/meterup/awdl_wifi_scripts/main/awdl-daemon.sh | bash'
alias awdlup='curl -s https://raw.githubusercontent.com/meterup/awdl_wifi_scripts/main/cleanup-and-reenable-awdl.sh | bash &> /dev/null'
]]>
+ + + + + + <p>我的电脑在公司使用无线网络时经常性断网,为了有稳定的网络我在工位时经常接根网线,使用网线连接。之前公司运维给了个叫 WiFriedX 的工具来解决这个问题,最近发现问题又出现了,开会时断网非常耽误事,所以就又着手开始排查。</p> +<p>最后定位到是苹果搞的 AWDL 引起 + + + + + +
+ +
diff --git a/avatar.jpeg b/avatar.jpeg new file mode 100644 index 0000000000..8bf421998f Binary files /dev/null and b/avatar.jpeg differ diff --git a/book-list/index.html b/book-list/index.html new file mode 100644 index 0000000000..7cb698f096 --- /dev/null +++ b/book-list/index.html @@ -0,0 +1,933 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 书单 | 贾攀的流水账 + + + + + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

贾攀的流水账

+ +
+

Panmax's Blog

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+ + +
+
+
+ + + + +
+ + + + + +
+
+ +

书单 +

+ + + +
+ + + + +
+
+

这里所记录的不限于实体书,还包括一些网络课程。

+
+

+

想读

    +
  • 杀死一只知更鸟(2023年05月16日加入,文化有限播客推荐)
  • +
  • 人生模式
  • +
  • 苔丝
  • +
  • 夜晚的潜水艇
  • +
  • 大教堂与集市
  • +
  • 穷查理宝典
  • +
  • 我们赖以生存的隐喻
  • +
  • 不一样的国文课
  • +
  • 人生(路遥)
  • +
  • 羊的门
  • +
  • 社会心理学
  • +
  • 心理学与生活
  • +
  • 这才是心理学
  • +
  • 我的第一本思维导图入门书
  • +
  • 中国文化的深层结构
  • +
  • 高效演讲与口才
  • +
  • 清单革命
  • +
  • 习惯的力量
  • +
  • 第三选择
  • +
  • 计算机程序的构造和解释
  • +
  • 自卑与超越(阿德勒)
  • +
  • 一地鸡毛(刘震云)
  • +
  • 月亮的距离(卡尔维诺)
  • +
  • 宇宙奇趣故事集(卡尔维诺)
  • +
  • 怦然心动的人生整理魔法
  • +
  • 正面管教(二刷)

    +
  • +
  • 金钱心理学

    +
  • +
  • 世界上最伟大的推销员
  • +
+

在读📖

    +
  • 红楼梦(终生阅读)

    +
  • +
  • 海贼王·第5部(33-40卷)(11月22日-?)

    +
  • +
  • 读库2406(12月2日-?)
  • +
  • 履单: 无所不有与一无所有(2024年12月13日-?)
  • +
  • 漫长的余生(2024年12月16日-?)
  • +
+

2024

12月

    +
  • 半小时漫画唐诗(10月20日-12月04日)
  • +
  • 中央帝国的财政密码(10月22日-12月12日)
  • +
  • 论语·增广贤文(11月18日-2024年12月15日)
  • +
+

11月

    +
  • 【推荐】它们没有脚,但足迹遍天下(10月23日-11月1日)
  • +
  • 一如既往(10月31日-1月20日)
  • +
  • 海贼王·第4部(25-32卷)(10月19日-2024年11月22日)
  • +
  • 读库2405(11月3日-11月30日)
  • +
+

10月

    +
  • 半小时漫画世界史(9月9日-10月3日)
  • +
  • 【一般】不忙不慌-林桂芝(6月11日-10月15日)
  • +
  • 海贼王·第4部(17-24卷)(9月24日-10月19日)
  • +
  • 半小时漫画中国哲学史(10月3日-10月20日)
  • +
  • 【推荐】盛世的崩塌(9月4日-10月22日)
  • +
  • 论语通读·下(8月2日-10月25日)
  • +
  • 【推荐】金钱心理学(10月16日-10月31日)
  • +
+

9月

    +
  • 【推荐】外婆的道歉信(8月19日-9月6日)
  • +
  • 半小时漫画中国史(番外篇):中国传统节日(8月30日-9月9日)
  • +
  • 【一般】暗时间(7月30日-9月20日)(不推荐,网上评论过于神话)
  • +
  • 读库2404(9月3日-9月23日)
  • +
  • 海贼王·第2部(9-16卷)(8月27日-9月24日)
  • +
+

8月

    +
  • 半小时漫画中国史2(7月13日-8月1日)
  • +
  • 【推荐】焦虑的人(7月23日-8月10日)
  • +
  • 半小时漫画中国史3(8月1日-8月14日)
  • +
  • 【推荐】一个叫欧维的男人决定去死(8月11日-8月18日)
  • +
  • Effective Java 第三版(5月28日-8月22日)
  • +
  • 海贼王·第1部(1-8卷)(8月22日-8月27日)
  • +
  • 半小时漫画中国史5(8月15日-8月30日)
  • +
+

7月

    +
  • 【一般】人体极限(6月9日-7月9日)
  • +
  • 半小时漫画中国史1(6月30日-7月12日)
  • +
  • 读库2403(6月24日-7月23日)
  • +
  • 【不推荐】今天,你更博学了吗?(2023年11月22日-7月23日)
  • +
  • 【一般】我在北京送快递(7月9日-7月30日)
  • +
+

6月

    +
  • 【一般】大宋病人(5月18日-6月10日)
  • +
  • 半小时漫画中国史4(6月10日-6月12日)
  • +
  • 【一般】浅谈“两点论”(5月6日-6月12日)
  • +
  • 🎥AI 大模型企业应用实战(4月23日-?)
  • +
  • 半小时漫画中国史0(6月14日-6月30日)
  • +
+

5月

    +
  • DDIA 逐章精读(2023年09月08日-5月9日)
  • +
  • 【推荐】包法利夫人(4月11日-5月15日)
  • +
  • 读库2303(4月25日-5月18日)
  • +
+

4月

    +
  • 读库2304(3月15日-4月6日)
  • +
  • 【推荐】苏菲的世界(2月26日-4月10日)
  • +
  • 读库2402(4月6日-4月24日)
  • +
  • 论语通读·上(2023年12月5日-4月29日)
  • +
+

3月

    +
  • 【推荐】额尔古纳河右岸(2月21日-3月2日)
  • +
  • 深度学习推荐系统实战(2023年10月26日-3月5日)
  • +
  • 【一般】白夜(3月2日-3月5日)
  • +
  • 【不推荐】地下室手记(3月5日-3月15日)
  • +
+

2月

    +
  • 【一般】罪与罚(1月30日-2月19日)
  • +
  • 读库2306(1月21日-2月24日)
  • +
+

1月

    +
  • 读库2401(12月10日-1月15日)
  • +
  • 【一般】阅读是一座随身携带的避难所(1月1日-1月21日)
  • +
  • 【一般】职场的51个基本(2023年11月25日-1月31日)
  • +
+

2023

12月

    +
  • 经典中医启蒙(2023年12月7日-2023年12月26日)
  • +
  • 能力陷阱(2023年10月25日-2023年12月2日)
  • +
  • 读库2305(2023年10月25日-2023年12月7日)
  • +
+

11月

    +
  • 超级访谈:对话毕玄(2023年7月23日-2023年11月12日)
  • +
  • GPT 时代的量化交易(2023年10月6日-2023年11月16日)
  • +
+

9月

    +
  • 睡眠革命(2023年7月16日-2023年9月19日)
  • +
  • 读库2302(2023年6月15日-2023年9月22日)
  • +
  • 幸福的方法(2023年6月18日-2023年9月25日)
  • +
+

8月

+

7月

    +
  • 海风中失落的血色馈赠(2023年6月9日-2023年7月22日)
  • +
+

5月

    +
  • 即时消息技术剖析与实战(2023年3月15日-2023年05月25日)
  • +
  • 🎥扑克脸(Poker Face)
  • +
  • 读库2301(2023年4月26日-2023年5月30日)
  • +
+

4月

    +
  • 五个光子(2023年3月16日-2023年4月3日)
  • +
  • 有知有行投资第一课(2022年10月17日-2023年4月3日)
  • +
  • 读库2205(2023年3月19日-2023年4月25日)
      +
    • 看完这本书后,我有了考摩托车驾照的计划,然后买一辆本田幼兽🏍️
    • +
    +
  • +
  • System Design Interview(2023年3月11日-2023年4月26日)
  • +
+

3月

    +
  • 我们仨(2023年2月12日-2023年3月1日)
  • +
  • 读库2206(2023年2月1日-2023年3月15日)
  • +
  • 10x 程序员工作法(2023年2月21日-2023年3月21日)
  • +
+

2月

    +
  • 贫困一代:被社会囚禁的年轻人(2023年1月6日-2023年2月7日)
  • +
  • 一句顶一万句(2023年1月14日-2023年2月13日)
  • +
  • 数据分析思维课(2023年1月20日-2023年2月24日)
  • +
+

1月

    +
  • 命运(蔡崇达)(2022年11月24日-1月4日)
  • +
  • 读库2204(2022年10月31日-1月5日)
  • +
  • 长安的荔枝(2023年1月5日-2023年1月12日)
  • +
+

2022

12月

    +
  • 文化苦旅(2022年9月23日-2022年12月)
  • +
+

11月

+

10月

    +
  • 第一人称单数(2022年10月15日-2022年10月28日)
  • +
+

9月

    +
  • 读库2203(2022年8月10日-2022年9月9日)
  • +
  • 福格行为模型(2022年8月23日-2022年9月14日)
  • +
  • 人间值得(2022年9月14日-2022年9月22日)
  • +
  • 写作是门手艺(2022年5月31日-2022年9月27日)豆瓣 9.1
  • +
+

8月

    +
  • 微服务架构设计模式(2022年6月27日-2022年8月23日)【推荐!!!】豆瓣 9.1
  • +
+

7月

    +
  • 叫魂:1768年的中国妖术大恐慌(2022年6月26日-2022年7月31日)豆瓣 9.1
  • +
  • 巨婴国(2022年6月27日-?) 读不下去
  • +
+

6月

    +
  • 沟通的方法(2022年4月16日-2022年6月11日)
  • +
  • 信息检索导论【前5章,后边有点啃不动】(52022年月31日-2022年6月11日)
  • +
  • 文心(2022年5月24日-62022年月20日)豆瓣 9.2
  • +
  • 棋王·树王·孩子王(2022年6月21日-2022年6月28日)豆瓣 9.3
  • +
+

5月

    +
  • 读库2202(2022年4月11日-2022年5月1日)
  • +
  • 暴雨下在病房里(2022年5月3日-2022年5月9日)
  • +
  • 四百年后的真相(2022年5月11日-2022年5月18日)
  • +
  • 职场求生攻略(2022年4月21日-2022年5月21日)
  • +
  • 掌控习惯(2022年5月17-2022年5月29日)
  • +
  • 高效信息管理术(2022年5月5日-2022年6月5日)
  • +
+

4月

    +
  • 二刷《红楼梦》中(2021年10月5日-2022年4月13日)
  • +
  • 万千微尘纷坠心田(2022年3月21日-2022年4月10日)
  • +
  • 高并发架构实战课(2022年2月5日-2022年4月13日)
  • +
  • 夜行货车(2022年3月27日-2022年4月26日)
  • +
+

3月

+

2月

    +
  • 读库2200(1月16日-2月11日)
  • +
  • 读库2201(1月16日-2月16日)
  • +
+

1月

    +
  • 重来3:跳出疯狂的忙碌(21年12月24日-1月3日)
  • +
  • 凤凰架构(21年11月18日-1月16日)
  • +
  • 克拉拉与太阳(1月2日-1月16日)
  • +
+

2021

12月

    +
  • 手把手带你写一个Web框架(9月20日-12月10日)
  • +
  • 乡土中国(11月27日-12月13日)
  • +
  • 人类简史(9月13日-12月17日)
  • +
  • 都柏林人(12月17日-12月31日)
  • +
+

11月

    +
  • Go 专家编程(9月7日-11月12日)
  • +
  • 伯恩斯焦虑自助疗法(10月6日-11月16日)
  • +
  • 蛤蟆先生去看心理医生(11月20日-11月27日)
  • +
+

10月

    +
  • 二刷《红楼梦》上(6月1日-10月5日)
  • +
  • Golang修养之路(8月19-10月27日)
  • +
+

9月

+

8月

+

7月

    +
  • 别让猴子跳回背上(7月11日)
  • +
  • 增长黑客(5月20日-7月15日)
  • +
  • 象与骑象人(6月28日-7月29日)
  • +
  • 一半是海水,一半是火焰(6月25日-7月30日)
  • +
+

6月

    +
  • 正面管教(5月14日-6月1日)
  • +
  • 诡计博物馆(5月28日-6月6日)
  • +
  • 东京奇谭集(6月8日-6月22日)
  • +
  • 技术与商业案例解读(2月23日-6月23日)
  • +
  • 感谢自己的不完美(6月3日-6月28日)
  • +
+

5月

    +
  • Effective Go(5月1日-5月3日)
  • +
  • 一本小小的蓝色逻辑书(4月24日-5月4日)
  • +
  • 幸福散论(4月20日-5月5日)
  • +
  • 格局(5月4日-5月13日)
  • +
  • 四世同堂·第3部·饥荒(4月23日-5月26日)
  • +
  • Unix 编程艺术(20年12月28日-21年5月28日)
  • +
+

4月

+

3月

    +
  • 亲密关系(2月15日-3月6日)
  • +
  • 四世同堂·第1部·惶惑(1月19日-3月11日)
  • +
+

2月

    +
  • 黑天鹅(1月4日-2月14日)
  • +
+

1月

    +
  • 儒林外史(20年12月26日-1月19日)
  • +
+

2020

12 月

    +
  • 被讨厌的勇气(12月13日-12月16日)
  • +
  • 红楼梦·中·人民文学出版社[40-80回](11月25日-12月13日)
  • +
  • 乔新亮的成长复盘(11月1日-12月27日)
  • +
  • 红楼梦·下·人民文学出版社[80-120回](12月13日-12月27日)
  • +
  • 君主论(12月17日-12月28日)
  • +
+

11 月

+

10 月

    +
  • 金字塔原理(二刷)(10月1日-10月22日)
  • +
  • 如何有效阅读一本书(10月11日-10月21日)
  • +
  • Linux 性能优化实战(7月19日-10月11日)
  • +
  • 高效能人士的七个习惯(10月4日-10月7日)
  • +
  • 瓦尔登湖(9月8日-10月1日)
  • +
+

9 月

    +
  • 见识(8月23日-9月6日)
  • +
+

8 月

    +
  • 活出生命的意义(8月11日-8月22日)
  • +
  • 数据密集型应用系统设计(二刷)(7月10日-8月17日)
  • +
  • 奈飞文化手册(7月29日-8月9日)
  • +
+

7 月

    +
  • 互联网人的英语私教课(6月29日-7月31日)
  • +
  • 北野武的小酒馆(7月15日-7月30日)
  • +
  • Java 持续交付(6月21日-7月9日)
  • +
  • 刻意练习(6月25日-7月7日)
  • +
+

6 月

    +
  • 程序员修炼之道(第2版)(5月18日-6月19日)
  • +
  • 计算机网络:自顶向下方法(4月16日-6月14日)
  • +
+

5 月

    +
  • 奇特的一生(5月6日-5月7日)
  • +
  • 毛泽东选集 第二卷(3月9日-5月14日)
  • +
+

4月

+

3月

+

2月

+

1月

    +
  • 原则(19年12月11日-1月16日)
  • +
  • 受戒
  • +
  • 深度学习入门(1月31日-2月7日)
  • +
+

2019

12月

    +
  • 朱赟的技术管理课(12月1日-12月31日)
  • +
  • 数据密集型应用系统设计(11月18日-12月28日)
  • +
  • 项目管理实战 20 讲(10月29日-12月17日)
  • +
  • 简单的逻辑学(12月1日-12月10日)
  • +
+

11月

    +
  • 万历十五年(11月13日-11月29日)
  • +
  • 左耳听风(8月22日-11月17日)
  • +
  • Go 语言核心 36 讲(10月12日-11月15日)
  • +
  • 霍乱时期的爱情(10月21日-11月12日)
  • +
+

10月

    +
  • DDD 实战(10月15日-12月2日)
  • +
  • 说透中台(10月1日-10月3日)
  • +
  • 少有人走的路(10月9日-10月21日)
  • +
+

9月

+

8 月

+

4月

    +
  • 岛上书店(二刷)
  • +
  • The Linux Command Line
  • +
+

5月

    +
  • 挪威的森林(二刷)
  • +
+

2018

1月

    +
  • 软技能 代码之外的生存指南
  • +
+

2月

    +
  • 三体1(二刷)
  • +
+

3月

    +
  • 活着(二刷)
  • +
+

8月

    +
  • 虚无的十字架
  • +
+

9月

    +
  • 皮囊
  • +
  • 月亮和六便士
  • +
  • 程序员修炼之道–从小工到专家
  • +
  • Gradle 实战
  • +
  • 恶意(东野圭吾)
  • +
  • 我不知道该说什么,关于死亡还是爱情
  • +
  • 边城
  • +
+

2017

1月

    +
  • 摆渡人
  • +
  • 了不起的盖茨比
  • +
+

2月

    +
  • 偷影子的人
  • +
  • Python 高手之路 (The Hacker’s Guide to Python)
  • +
  • 富爸爸,穷爸爸
  • +
+

3月

    +
  • 小狗钱钱
  • +
  • 岛上书店
  • +
  • 嫌疑人X的献身
  • +
+

4月

    +
  • 无声告白
  • +
+

8月

    +
  • 浪潮之巅
  • +
+
+

2016

1月

    +
  • Redis 入门指南(第2版)
  • +
+

3月

    +
  • 第七天(余华)
  • +
  • 三体·地球往事
  • +
  • 动物农场
  • +
+

4月

    +
  • 黄金时代
  • +
  • 三体Ⅱ·黑暗森林
  • +
  • React Native 入门与实战
  • +
+

5月

    +
  • 兄弟(余华)
  • +
+

6月

    +
  • 三体Ⅲ·死神永生
  • +
+

7月

    +
  • 代码的未来
  • +
  • 白银时代
  • +
+

8月

    +
  • 解忧杂货店
  • +
+

9月

    +
  • 没有色彩的多崎作和他的巡礼之年
  • +
  • 北京折叠
  • +
+

12月

    +
  • 挪威的森林
  • +
+

2014-2015

    +
  • 小王子
  • +
  • 追风筝的人
  • +
  • 围城
  • +
  • 活着
  • +
  • 1984
  • +
  • 平凡的世界
  • +
  • 看见
  • +
  • 许三观卖血记
  • +
  • 失控
  • +
  • 当我谈跑步时我谈些什么
  • +
  • 白鹿原
  • +
  • 爱你就像爱生命
  • +
  • 一只特立独行的猪
  • +
  • 乖,摸摸头
  • +
  • 滚蛋吧,肿瘤君
  • +
  • 人性的弱点
  • +
  • 白夜
  • +
  • 孤独六讲
  • +
  • 黑客与画家
  • +
+

放弃阅读

    +
  • 爱的艺术(21年8月12日-?),放弃原因:翻译太烂
  • +
  • 枪炮、病菌与钢铁,放弃原因:翻译太烂
  • +
  • Google SRE 工作手册(9月6日-?),放弃原因:读了近一半,感觉收益不大
  • +
  • Rust 编程第一课(1月14日-?)
  • +
  • 情绪障碍跨诊断治疗的统一方案自助手册(2023年09月13日-?)
  • +
+ +
+ + + +
+ + + + + + + +
+ +
+ + + +
+ + + + + + + + +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/book-list/quantity.png b/book-list/quantity.png new file mode 100644 index 0000000000..fa4f2ba8e2 Binary files /dev/null and b/book-list/quantity.png differ diff --git a/categories/Code/index.html b/categories/Code/index.html new file mode 100644 index 0000000000..474a6c7be2 --- /dev/null +++ b/categories/Code/index.html @@ -0,0 +1,628 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Category: Code | 贾攀的流水账 + + + + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

贾攀的流水账

+ +
+

Panmax's Blog

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+ + +
+
+
+ + +
+ + + + + +
+
+
+

Code + Category +

+
+ + +
+ 2017 +
+ + +
+ 2016 +
+ + + + + + + + + + + + + + + + + + + +
+
+ + + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/categories/Code/page/2/index.html b/categories/Code/page/2/index.html new file mode 100644 index 0000000000..28b00af56a --- /dev/null +++ b/categories/Code/page/2/index.html @@ -0,0 +1,548 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Category: Code | 贾攀的流水账 + + + + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

贾攀的流水账

+ +
+

Panmax's Blog

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+ + +
+
+
+ + +
+ + + + + +
+
+
+

Code + Category +

+
+ + +
+ 2016 +
+ + + + + + + + +
+ 2015 +
+ + + + + +
+
+ + + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/categories/GitHub/index.html b/categories/GitHub/index.html new file mode 100644 index 0000000000..30d9d6dfca --- /dev/null +++ b/categories/GitHub/index.html @@ -0,0 +1,462 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Category: GitHub | 贾攀的流水账 + + + + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

贾攀的流水账

+ +
+

Panmax's Blog

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+ + +
+
+
+ + +
+ + + + + +
+
+
+

GitHub + Category +

+
+ + +
+ 2016 +
+ + + + + +
+
+ + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/categories/Linux/index.html b/categories/Linux/index.html new file mode 100644 index 0000000000..3307733f8a --- /dev/null +++ b/categories/Linux/index.html @@ -0,0 +1,442 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Category: Linux | 贾攀的流水账 + + + + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

贾攀的流水账

+ +
+

Panmax's Blog

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+ + +
+
+
+ + +
+ + + + + +
+
+
+

Linux + Category +

+
+ + +
+ 2015 +
+ + + +
+
+ + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/categories/Mac/index.html b/categories/Mac/index.html new file mode 100644 index 0000000000..e85f136fa8 --- /dev/null +++ b/categories/Mac/index.html @@ -0,0 +1,442 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Category: Mac | 贾攀的流水账 + + + + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

贾攀的流水账

+ +
+

Panmax's Blog

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+ + +
+
+
+ + +
+ + + + + +
+
+
+

Mac + Category +

+
+ + +
+ 2016 +
+ + + +
+
+ + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/categories/MySql/index.html b/categories/MySql/index.html new file mode 100644 index 0000000000..9d18b12232 --- /dev/null +++ b/categories/MySql/index.html @@ -0,0 +1,442 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Category: MySql | 贾攀的流水账 + + + + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

贾攀的流水账

+ +
+

Panmax's Blog

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+ + +
+
+
+ + +
+ + + + + +
+
+
+

MySql + Category +

+
+ + +
+ 2016 +
+ + + +
+
+ + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/categories/VIM/index.html b/categories/VIM/index.html new file mode 100644 index 0000000000..ca04ce02dd --- /dev/null +++ b/categories/VIM/index.html @@ -0,0 +1,442 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Category: VIM | 贾攀的流水账 + + + + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

贾攀的流水账

+ +
+

Panmax's Blog

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+ + +
+
+
+ + +
+ + + + + +
+
+
+

VIM + Category +

+
+ + +
+ 2016 +
+ + + +
+
+ + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/categories/exploreflask/index.html b/categories/exploreflask/index.html new file mode 100644 index 0000000000..d5483673bd --- /dev/null +++ b/categories/exploreflask/index.html @@ -0,0 +1,502 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Category: exploreflask | 贾攀的流水账 + + + + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

贾攀的流水账

+ +
+

Panmax's Blog

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+ + +
+
+
+ + +
+ + + + + +
+
+
+

exploreflask + Category +

+
+ + +
+ 2015 +
+ + + + + + + + + +
+
+ + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/categories/flask/index.html b/categories/flask/index.html new file mode 100644 index 0000000000..dbbc4a3154 --- /dev/null +++ b/categories/flask/index.html @@ -0,0 +1,442 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Category: flask | 贾攀的流水账 + + + + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

贾攀的流水账

+ +
+

Panmax's Blog

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+ + +
+
+
+ + +
+ + + + + +
+
+
+

flask + Category +

+
+ + +
+ 2015 +
+ + + +
+
+ + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/categories/index.html b/categories/index.html new file mode 100644 index 0000000000..29d1b93986 --- /dev/null +++ b/categories/index.html @@ -0,0 +1,442 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + categories | 贾攀的流水账 + + + + + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

贾攀的流水账

+ +
+

Panmax's Blog

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+ + +
+
+
+ + + + +
+ + + + + +
+
+ +

categories +

+ + + +
+ + + + +
+ + +
+ + + +
+ + + + + + + +
+ +
+ + + +
+ + + + + + + + +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git "a/categories/\345\260\217\346\224\200\350\257\264/index.html" "b/categories/\345\260\217\346\224\200\350\257\264/index.html" new file mode 100644 index 0000000000..091a5a6b6f --- /dev/null +++ "b/categories/\345\260\217\346\224\200\350\257\264/index.html" @@ -0,0 +1,505 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Category: 小攀说 | 贾攀的流水账 + + + + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

贾攀的流水账

+ +
+

Panmax's Blog

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+ + +
+
+
+ + +
+ + + + + +
+
+
+

小攀说 + Category +

+
+ + +
+ 2016 +
+ + +
+ 2015 +
+ + + + + + + +
+
+ + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git "a/categories/\346\212\230\350\205\276/index.html" "b/categories/\346\212\230\350\205\276/index.html" new file mode 100644 index 0000000000..665ceae85c --- /dev/null +++ "b/categories/\346\212\230\350\205\276/index.html" @@ -0,0 +1,442 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Category: 折腾 | 贾攀的流水账 + + + + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

贾攀的流水账

+ +
+

Panmax's Blog

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+ + +
+
+
+ + +
+ + + + + +
+
+
+

折腾 + Category +

+
+ + +
+ 2016 +
+ + + +
+
+ + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git "a/categories/\346\234\211\350\266\243/index.html" "b/categories/\346\234\211\350\266\243/index.html" new file mode 100644 index 0000000000..29614285f0 --- /dev/null +++ "b/categories/\346\234\211\350\266\243/index.html" @@ -0,0 +1,442 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Category: 有趣 | 贾攀的流水账 + + + + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

贾攀的流水账

+ +
+

Panmax's Blog

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+ + +
+
+
+ + +
+ + + + + +
+
+
+

有趣 + Category +

+
+ + +
+ 2016 +
+ + + +
+
+ + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git "a/categories/\346\272\220\347\240\201/index.html" "b/categories/\346\272\220\347\240\201/index.html" new file mode 100644 index 0000000000..71baad4e17 --- /dev/null +++ "b/categories/\346\272\220\347\240\201/index.html" @@ -0,0 +1,502 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Category: 源码 | 贾攀的流水账 + + + + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

贾攀的流水账

+ +
+

Panmax's Blog

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+ + +
+
+
+ + +
+ + + + + +
+
+
+

源码 + Category +

+
+ + +
+ 2016 +
+ + + + + + + + + +
+
+ + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git "a/categories/\347\254\224\350\256\260/index.html" "b/categories/\347\254\224\350\256\260/index.html" new file mode 100644 index 0000000000..cf3b8dd6fe --- /dev/null +++ "b/categories/\347\254\224\350\256\260/index.html" @@ -0,0 +1,462 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Category: 笔记 | 贾攀的流水账 + + + + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

贾攀的流水账

+ +
+

Panmax's Blog

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+ + +
+
+
+ + +
+ + + + + +
+
+
+

笔记 + Category +

+
+ + +
+ 2016 +
+ + + + + +
+
+ + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git "a/categories/\347\277\273\350\257\221/index.html" "b/categories/\347\277\273\350\257\221/index.html" new file mode 100644 index 0000000000..559e6e52b9 --- /dev/null +++ "b/categories/\347\277\273\350\257\221/index.html" @@ -0,0 +1,442 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Category: 翻译 | 贾攀的流水账 + + + + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

贾攀的流水账

+ +
+

Panmax's Blog

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+ + +
+
+
+ + +
+ + + + + +
+
+
+

翻译 + Category +

+
+ + +
+ 2016 +
+ + + +
+
+ + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git "a/categories/\350\275\254\350\275\275/index.html" "b/categories/\350\275\254\350\275\275/index.html" new file mode 100644 index 0000000000..8face7d3e0 --- /dev/null +++ "b/categories/\350\275\254\350\275\275/index.html" @@ -0,0 +1,505 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Category: 转载 | 贾攀的流水账 + + + + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

贾攀的流水账

+ +
+

Panmax's Blog

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+ + +
+
+
+ + +
+ + + + + +
+
+
+

转载 + Category +

+
+ + +
+ 2016 +
+ + + + +
+ 2015 +
+ + + + + +
+
+ + + + + + + + +
+ + + + +
+ + + + + + + + +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/css/main.css b/css/main.css new file mode 100644 index 0000000000..b4255058fa --- /dev/null +++ b/css/main.css @@ -0,0 +1 @@ +:root{--body-bg-color:#f5f7f9;--content-bg-color:#fff;--card-bg-color:#f5f5f5;--text-color:#555;--link-color:#555;--link-hover-color:#222;--brand-color:#fff;--brand-hover-color:#fff;--table-row-odd-bg-color:#f9f9f9;--table-row-hover-bg-color:#f5f5f5;--menu-item-bg-color:#f5f5f5;--btn-default-bg:#fff;--btn-default-color:#555;--btn-default-border-color:#555;--btn-default-hover-bg:#222;--btn-default-hover-color:#fff;--btn-default-hover-border-color:#222}@media (prefers-color-scheme:dark){:root{--body-bg-color:#282828;--content-bg-color:#333;--card-bg-color:#555;--text-color:#ccc;--link-color:#ccc;--link-hover-color:#eee;--brand-color:#ddd;--brand-hover-color:#ddd;--table-row-odd-bg-color:#282828;--table-row-hover-bg-color:#363636;--menu-item-bg-color:#555;--btn-default-bg:#222;--btn-default-color:#ccc;--btn-default-border-color:#555;--btn-default-hover-bg:#666;--btn-default-hover-color:#ccc;--btn-default-hover-border-color:#666}img{opacity:.75;}img:hover{opacity:.9}}html{line-height:1.15;-webkit-text-size-adjust:100%;}body{margin:0}main{display:block}h1{font-size:2em;margin:.67em 0}hr{box-sizing:content-box;height:0;overflow:visible;}pre{font-family:monospace,monospace;font-size:1em;}a{background:transparent}abbr[title]{border-bottom:none;text-decoration:underline;text-decoration:underline dotted;}b,strong{font-weight:bolder}code,kbd,samp{font-family:monospace,monospace;font-size:1em;}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}img{border-style:none}button,input,optgroup,select,textarea{font-family:inherit;font-size:100%;line-height:1.15;margin:0;}button,input{overflow:visible}button,select{text-transform:none}button,[type='button'],[type='reset'],[type='submit']{-webkit-appearance:button}button::-moz-focus-inner,[type='button']::-moz-focus-inner,[type='reset']::-moz-focus-inner,[type='submit']::-moz-focus-inner{border-style:none;padding:0}button:-moz-focusring,[type='button']:-moz-focusring,[type='reset']:-moz-focusring,[type='submit']:-moz-focusring{outline:1px dotted ButtonText}fieldset{padding:.35em .75em .625em}legend{box-sizing:border-box;color:inherit;display:table;max-width:100%;padding:0;white-space:normal;}progress{vertical-align:baseline}textarea{overflow:auto}[type='checkbox'],[type='radio']{box-sizing:border-box;padding:0;}[type='number']::-webkit-inner-spin-button,[type='number']::-webkit-outer-spin-button{height:auto}[type='search']{outline-offset:-2px;-webkit-appearance:textfield;}[type='search']::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{font:inherit;-webkit-appearance:button;}details{display:block}summary{display:list-item}template{display:none}[hidden]{display:none}::selection{background:#262a30;color:#eee}html,body{height:100%}body{background:var(--body-bg-color);color:var(--text-color);font-family:'Lato',"PingFang SC","Microsoft YaHei",sans-serif;font-size:1em;line-height:2;}@media (max-width:991px){body{padding-left:0 !important;padding-right:0 !important}}h1,h2,h3,h4,h5,h6{font-family:'Lato',"PingFang SC","Microsoft YaHei",sans-serif;font-weight:bold;line-height:1.5;margin:20px 0 15px}h1{font-size:1.5em}h2{font-size:1.375em}h3{font-size:1.25em}h4{font-size:1.125em}h5{font-size:1em}h6{font-size:.875em}p{margin:0 0 20px 0}a,span.exturl{border-bottom:1px solid #999;color:var(--link-color);outline:0;text-decoration:none;overflow-wrap:break-word;word-wrap:break-word;cursor:pointer}a:hover,span.exturl:hover{border-bottom-color:var(--link-hover-color);color:var(--link-hover-color)}iframe,img,video{display:block;margin-left:auto;margin-right:auto;max-width:100%}hr{background-image:repeating-linear-gradient(-45deg,#ddd,#ddd 4px,transparent 4px,transparent 8px);border:0;height:3px;margin:40px 0}blockquote{border-left:4px solid #ddd;color:#666;margin:0;padding:0 15px;}blockquote cite::before{content:'-';padding:0 5px}dt{font-weight:bold}dd{margin:0;padding:0}kbd{background-color:#f5f5f5;background-image:linear-gradient(#eee,#fff,#eee);border:1px solid #ccc;border-radius:.2em;box-shadow:.1em .1em .2em rgba(0,0,0,0.1);color:#555;font-family:inherit;padding:.1em .3em;white-space:nowrap}.table-container{overflow:auto}table{border-collapse:collapse;border-spacing:0;font-size:.875em;margin:0 0 20px 0;width:100%}tbody tr:nth-of-type(odd){background:var(--table-row-odd-bg-color)}tbody tr:hover{background:var(--table-row-hover-bg-color)}caption,th,td{font-weight:normal;padding:8px;text-align:left;vertical-align:middle}th,td{border:1px solid #ddd;border-bottom:3px solid #ddd}th{font-weight:700;padding-bottom:10px}td{border-bottom-width:1px}.btn{background:var(--btn-default-bg);border:2px solid var(--btn-default-border-color);border-radius:2px;color:var(--btn-default-color);display:inline-block;font-size:.875em;line-height:2;padding:0 20px;text-decoration:none;transition-property:background-color;transition-delay:0s;transition-duration:.2s;transition-timing-function:ease-in-out;}.btn:hover{background:var(--btn-default-hover-bg);border-color:var(--btn-default-hover-border-color);color:var(--btn-default-hover-color)}.btn + .btn{margin:0 0 8px 8px}.btn .fa-fw{text-align:left;width:1.285714285714286em}.toggle{line-height:0;}.toggle .toggle-line{background:#fff;display:inline-block;height:2px;left:0;position:relative;top:0;transition:all .4s;vertical-align:top;width:100%;}.toggle .toggle-line:not(:first-child){margin-top:3px}.toggle.toggle-arrow .toggle-line-first{left:50%;top:2px;transform:rotate(45deg);width:50%}.toggle.toggle-arrow .toggle-line-middle{left:2px;width:90%}.toggle.toggle-arrow .toggle-line-last{left:50%;top:-2px;transform:rotate(-45deg);width:50%}.toggle.toggle-close .toggle-line-first{transform:rotate(-45deg);top:5px}.toggle.toggle-close .toggle-line-middle{opacity:0}.toggle.toggle-close .toggle-line-last{transform:rotate(45deg);top:-5px}.highlight,pre{background:#f7f7f7;color:#4d4d4c;line-height:1.6;margin:0 auto 20px}pre,code{font-family:consolas,Menlo,monospace,"PingFang SC","Microsoft YaHei"}code{background:#eee;border-radius:3px;color:#555;padding:2px 4px;overflow-wrap:break-word;word-wrap:break-word}.highlight *::selection{background:#d6d6d6}.highlight pre{border:0;margin:0;padding:10px 0}.highlight table{border:0;margin:0;width:auto}.highlight td{border:0;padding:0}.highlight figcaption{background:#eff2f3;color:#4d4d4c;display:flex;font-size:.875em;justify-content:space-between;line-height:1.2;padding:.5em;}.highlight figcaption a{color:#4d4d4c;}.highlight figcaption a:hover{border-bottom-color:#4d4d4c}.highlight .gutter{-moz-user-select:none;-ms-user-select:none;-webkit-user-select:none;user-select:none;}.highlight .gutter pre{background:#eff2f3;color:#869194;padding-left:10px;padding-right:10px;text-align:right}.highlight .code pre{background:#f7f7f7;padding-left:10px;width:100%}.gist table{width:auto;}.gist table td{border:0}pre{overflow:auto;padding:10px;}pre code{background:none;color:#4d4d4c;font-size:.875em;padding:0;text-shadow:none}pre .deletion{background:#fdd}pre .addition{background:#dfd}pre .meta{color:#eab700;-moz-user-select:none;-ms-user-select:none;-webkit-user-select:none;user-select:none}pre .comment{color:#8e908c}pre .variable,pre .attribute,pre .tag,pre .name,pre .regexp,pre .ruby .constant,pre .xml .tag .title,pre .xml .pi,pre .xml .doctype,pre .html .doctype,pre .css .id,pre .css .class,pre .css .pseudo{color:#c82829}pre .number,pre .preprocessor,pre .built_in,pre .builtin-name,pre .literal,pre .params,pre .constant,pre .command{color:#f5871f}pre .ruby .class .title,pre .css .rules .attribute,pre .string,pre .symbol,pre .value,pre .inheritance,pre .header,pre .ruby .symbol,pre .xml .cdata,pre .special,pre .formula{color:#718c00}pre .title,pre .css .hexcolor{color:#3e999f}pre .function,pre .python .decorator,pre .python .title,pre .ruby .function .title,pre .ruby .title .keyword,pre .perl .sub,pre .javascript .title,pre .coffeescript .title{color:#4271ae}pre .keyword,pre .javascript .function{color:#8959a8}.blockquote-center{border-left:none;margin:40px 0;padding:0;position:relative;text-align:center;}.blockquote-center::before,.blockquote-center::after{background-repeat:no-repeat;background-size:22px 22px;content:' ';display:block;height:24px;opacity:.2;position:absolute;width:100%}.blockquote-center::before{background-image:url("../images/quote-l.svg");background-position:0 -6px;border-top:1px solid #ccc;top:-20px}.blockquote-center::after{background-image:url("../images/quote-r.svg");background-position:100% 8px;border-bottom:1px solid #ccc;bottom:-20px}.blockquote-center p,.blockquote-center div{text-align:center}.post-body .group-picture img{margin:0 auto;padding:0 3px}.group-picture-row{margin-bottom:6px;overflow:hidden}.group-picture-column{float:left;margin-bottom:10px}.post-body .label{color:#555;display:inline;padding:0 2px;}.post-body .label.default{background:#f0f0f0}.post-body .label.primary{background:#efe6f7}.post-body .label.info{background:#e5f2f8}.post-body .label.success{background:#e7f4e9}.post-body .label.warning{background:#fcf6e1}.post-body .label.danger{background:#fae8eb}.post-body .tabs{margin-bottom:20px}.post-body .tabs,.tabs-comment{display:block;padding-top:10px;position:relative;}.post-body .tabs ul.nav-tabs,.tabs-comment ul.nav-tabs{display:flex;flex-wrap:wrap;margin:0;margin-bottom:-1px;padding:0;}@media (max-width:413px){.post-body .tabs ul.nav-tabs,.tabs-comment ul.nav-tabs{display:block;margin-bottom:5px}}.post-body .tabs ul.nav-tabs li.tab,.tabs-comment ul.nav-tabs li.tab{border-bottom:1px solid #ddd;border-left:1px solid transparent;border-right:1px solid transparent;border-top:3px solid transparent;flex-grow:1;list-style-type:none;border-radius:0 0 0 0;}@media (max-width:413px){.post-body .tabs ul.nav-tabs li.tab,.tabs-comment ul.nav-tabs li.tab{border-bottom:1px solid transparent;border-left:3px solid transparent;border-right:1px solid transparent;border-top:1px solid transparent}}@media (max-width:413px){.post-body .tabs ul.nav-tabs li.tab,.tabs-comment ul.nav-tabs li.tab{border-radius:0}}.post-body .tabs ul.nav-tabs li.tab a,.tabs-comment ul.nav-tabs li.tab a{border-bottom:initial;display:block;line-height:1.8;outline:0;padding:.25em .75em;text-align:center;transition-delay:0s;transition-duration:.2s;transition-timing-function:ease-out}.post-body .tabs ul.nav-tabs li.tab a i,.tabs-comment ul.nav-tabs li.tab a i{width:1.285714285714286em}.post-body .tabs ul.nav-tabs li.tab.active,.tabs-comment ul.nav-tabs li.tab.active{border-bottom:1px solid transparent;border-left:1px solid #ddd;border-right:1px solid #ddd;border-top:3px solid #fc6423;}@media (max-width:413px){.post-body .tabs ul.nav-tabs li.tab.active,.tabs-comment ul.nav-tabs li.tab.active{border-bottom:1px solid #ddd;border-left:3px solid #fc6423;border-right:1px solid #ddd;border-top:1px solid #ddd}}.post-body .tabs ul.nav-tabs li.tab.active a,.tabs-comment ul.nav-tabs li.tab.active a{color:var(--link-color);cursor:default}.post-body .tabs .tab-content .tab-pane,.tabs-comment .tab-content .tab-pane{border:1px solid #ddd;border-top:0;padding:20px 20px 0 20px;border-radius:0;}.post-body .tabs .tab-content .tab-pane:not(.active),.tabs-comment .tab-content .tab-pane:not(.active){display:none}.post-body .tabs .tab-content .tab-pane.active,.tabs-comment .tab-content .tab-pane.active{display:block;}.post-body .tabs .tab-content .tab-pane.active:nth-of-type(1),.tabs-comment .tab-content .tab-pane.active:nth-of-type(1){border-radius:0 0 0 0;}@media (max-width:413px){.post-body .tabs .tab-content .tab-pane.active:nth-of-type(1),.tabs-comment .tab-content .tab-pane.active:nth-of-type(1){border-radius:0}}.post-body .note{border-radius:3px;margin-bottom:20px;padding:1em;position:relative;border:1px solid #eee;border-left-width:5px;}.post-body .note h2,.post-body .note h3,.post-body .note h4,.post-body .note h5,.post-body .note h6{margin-top:0;border-bottom:initial;margin-bottom:0;padding-top:0}.post-body .note p:first-child,.post-body .note ul:first-child,.post-body .note ol:first-child,.post-body .note table:first-child,.post-body .note pre:first-child,.post-body .note blockquote:first-child,.post-body .note img:first-child{margin-top:0}.post-body .note p:last-child,.post-body .note ul:last-child,.post-body .note ol:last-child,.post-body .note table:last-child,.post-body .note pre:last-child,.post-body .note blockquote:last-child,.post-body .note img:last-child{margin-bottom:0}.post-body .note.default{border-left-color:#777;}.post-body .note.default h2,.post-body .note.default h3,.post-body .note.default h4,.post-body .note.default h5,.post-body .note.default h6{color:#777}.post-body .note.primary{border-left-color:#6f42c1;}.post-body .note.primary h2,.post-body .note.primary h3,.post-body .note.primary h4,.post-body .note.primary h5,.post-body .note.primary h6{color:#6f42c1}.post-body .note.info{border-left-color:#428bca;}.post-body .note.info h2,.post-body .note.info h3,.post-body .note.info h4,.post-body .note.info h5,.post-body .note.info h6{color:#428bca}.post-body .note.success{border-left-color:#5cb85c;}.post-body .note.success h2,.post-body .note.success h3,.post-body .note.success h4,.post-body .note.success h5,.post-body .note.success h6{color:#5cb85c}.post-body .note.warning{border-left-color:#f0ad4e;}.post-body .note.warning h2,.post-body .note.warning h3,.post-body .note.warning h4,.post-body .note.warning h5,.post-body .note.warning h6{color:#f0ad4e}.post-body .note.danger{border-left-color:#d9534f;}.post-body .note.danger h2,.post-body .note.danger h3,.post-body .note.danger h4,.post-body .note.danger h5,.post-body .note.danger h6{color:#d9534f}.pagination .prev,.pagination .next,.pagination .page-number,.pagination .space{display:inline-block;margin:0 10px;padding:0 11px;position:relative;top:-1px;}@media (max-width:767px){.pagination .prev,.pagination .next,.pagination .page-number,.pagination .space{margin:0 5px}}.pagination{border-top:1px solid #eee;margin:120px 0 0;text-align:center;}.pagination .prev,.pagination .next,.pagination .page-number{border-bottom:0;border-top:1px solid #eee;transition-property:border-color;transition-delay:0s;transition-duration:.2s;transition-timing-function:ease-in-out;}.pagination .prev:hover,.pagination .next:hover,.pagination .page-number:hover{border-top-color:#222}.pagination .space{margin:0;padding:0}.pagination .prev{margin-left:0}.pagination .next{margin-right:0}.pagination .page-number.current{background:#ccc;border-top-color:#ccc;color:#fff}@media (max-width:767px){.pagination{border-top:none}.pagination .prev,.pagination .next,.pagination .page-number{border-bottom:1px solid #eee;border-top:0;margin-bottom:10px;padding:0 10px;}.pagination .prev:hover,.pagination .next:hover,.pagination .page-number:hover{border-bottom-color:#222}}.comments{margin-top:60px;overflow:hidden}.comment-button-group{display:flex;flex-wrap:wrap-reverse;justify-content:center;margin:1em 0;}.comment-button-group .comment-button{margin:.1em .2em;}.comment-button-group .comment-button.active{background:var(--btn-default-hover-bg);border-color:var(--btn-default-hover-border-color);color:var(--btn-default-hover-color)}.comment-position{display:none;}.comment-position.active{display:block}.tabs-comment{background:var(--content-bg-color);margin-top:4em;padding-top:0;}.tabs-comment .comments{border:0;box-shadow:none;margin-top:0;padding-top:0}.container{min-height:100%;position:relative}.main-inner{margin:0 auto;width:calc(100% - 20px);}@media (min-width:1200px){.main-inner{width:1160px}}@media (min-width:1600px){.main-inner{width:73%}}@media (max-width:767px){.content-wrap{padding:0 20px}}.header{background:transparent}.header-inner{margin:0 auto;width:calc(100% - 20px);}@media (min-width:1200px){.header-inner{width:1160px}}@media (min-width:1600px){.header-inner{width:73%}}.site-brand-container{display:flex;flex-shrink:0;padding:0 10px}.headband{background:#222;height:3px}.site-meta{flex-grow:1;text-align:center;}@media (max-width:767px){.site-meta{text-align:center}}.brand{border-bottom:none;color:var(--brand-color);display:inline-block;line-height:1.375em;padding:0 40px;position:relative;}.brand:hover{color:var(--brand-hover-color)}.site-title{font-family:'Lato',"PingFang SC","Microsoft YaHei",sans-serif;font-size:1.375em;font-weight:normal;margin:0}.site-subtitle{color:#ddd;font-size:.8125em;margin:10px 0}.use-motion .brand{opacity:0}.use-motion .site-title,.use-motion .site-subtitle,.use-motion .custom-logo-image{opacity:0;position:relative;top:-10px}.site-nav-toggle,.site-nav-right{display:none;}@media (max-width:767px){.site-nav-toggle,.site-nav-right{display:flex;flex-direction:column;justify-content:center}}.site-nav-toggle .toggle,.site-nav-right .toggle{color:var(--text-color);padding:10px;width:22px;}.site-nav-toggle .toggle .toggle-line,.site-nav-right .toggle .toggle-line{background:var(--text-color);border-radius:1px}.site-nav{display:block;}@media (max-width:767px){.site-nav{clear:both;display:none}}.site-nav.site-nav-on{display:block}.menu{margin-top:20px;padding-left:0;text-align:center}.menu-item{display:inline-block;list-style:none;margin:0 10px;}@media (max-width:767px){.menu-item{display:block;margin-top:10px}.menu-item.menu-item-search{display:none}}.menu-item a,.menu-item span.exturl{border-bottom:0;display:block;font-size:.8125em;transition-property:border-color;transition-delay:0s;transition-duration:.2s;transition-timing-function:ease-in-out;}@media (hover:none){.menu-item a:hover,.menu-item span.exturl:hover{border-bottom-color:transparent !important}}.menu-item .fa{margin-right:8px}.menu-item .badge{display:inline-block;font-weight:bold;line-height:1;margin-left:.35em;margin-top:.35em;text-align:center;white-space:nowrap;}@media (max-width:767px){.menu-item .badge{float:right;margin-left:0}}.menu-item-active a,.menu .menu-item a:hover,.menu .menu-item span.exturl:hover{background:var(--menu-item-bg-color)}.use-motion .menu-item{opacity:0}.sidebar{background:#222;bottom:0;box-shadow:inset 0 2px 6px #000;position:fixed;top:0;}@media (max-width:991px){.sidebar{display:none}}.sidebar-inner{color:#999;padding:18px 10px;text-align:center}.cc-license{margin-top:10px;text-align:center;}.cc-license .cc-opacity{border-bottom:none;opacity:.7;}.cc-license .cc-opacity:hover{opacity:.9}.cc-license img{display:inline-block}.site-author-image{border:1px solid #eee;display:block;margin:0 auto;max-width:120px;padding:2px;}.site-author-image{transition:transform 1s ease-out}.site-author-image:hover{transform:rotateZ(360deg)}.site-author-name{color:var(--text-color);font-weight:600;margin:0;text-align:center}.site-description{color:#999;font-size:.8125em;margin-top:0;text-align:center}.links-of-author{margin-top:15px;}.links-of-author a,.links-of-author span.exturl{border-bottom-color:#555;display:inline-block;font-size:.8125em;margin-bottom:10px;margin-right:10px;vertical-align:middle;}.links-of-author a::before,.links-of-author span.exturl::before{background:#e41ce0;border-radius:50%;content:' ';display:inline-block;height:4px;margin-right:3px;vertical-align:middle;width:4px}.sidebar-button{margin-top:15px;}.sidebar-button a{border:1px solid #fc6423;border-radius:4px;color:#fc6423;display:inline-block;padding:0 15px;}.sidebar-button a .fa{margin-right:5px}.sidebar-button a:hover{background:#fc6423;border:1px solid #fc6423;color:#fff;}.sidebar-button a:hover .fa{color:#fff}.links-of-blogroll{font-size:.8125em;margin-top:10px}.links-of-blogroll-title{font-size:.875em;font-weight:600;margin-top:0}.links-of-blogroll-list{list-style:none;margin:0;padding:0}#sidebar-dimmer{display:none}@media (max-width:767px){#sidebar-dimmer{background:#000;display:block;height:100%;left:100%;opacity:0;position:fixed;top:0;width:100%;z-index:1100}.sidebar-active + #sidebar-dimmer{opacity:.7;transform:translateX(-100%);transition:opacity .5s}}.sidebar-nav{margin:0;padding-bottom:20px;padding-left:0;}.sidebar-nav li{border-bottom:1px solid transparent;color:#555;cursor:pointer;display:inline-block;font-size:.875em;}.sidebar-nav li.sidebar-nav-overview{margin-left:10px}.sidebar-nav li:hover{color:#fc6423}.sidebar-nav .sidebar-nav-active{border-bottom-color:#fc6423;color:#fc6423;}.sidebar-nav .sidebar-nav-active:hover{color:#fc6423}.sidebar-panel{display:none;overflow-x:hidden;overflow-y:auto}.sidebar-panel-active{display:block}.sidebar-toggle{background:#222;bottom:45px;cursor:pointer;height:14px;left:30px;padding:5px;position:fixed;width:14px;z-index:1300;}@media (max-width:991px){.sidebar-toggle{left:20px;opacity:.8;display:none}}.sidebar-toggle:hover .toggle-line{background:#fc6423}.post-toc{font-size:.875em;}.post-toc ol{list-style:none;margin:0;padding:0 2px 5px 10px;text-align:left;}.post-toc ol > ol{padding-left:0}.post-toc ol a{transition-property:all;transition-delay:0s;transition-duration:.2s;transition-timing-function:ease-in-out}.post-toc .nav-item{line-height:1.8;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.post-toc .nav .nav-child{display:none}.post-toc .nav .active > .nav-child{display:block}.post-toc .nav .active-current > .nav-child{display:block;}.post-toc .nav .active-current > .nav-child > .nav-item{display:block}.post-toc .nav .active > a{border-bottom-color:#fc6423;color:#fc6423}.post-toc .nav .active-current > a{color:#fc6423;}.post-toc .nav .active-current > a:hover{color:#fc6423}.site-state{display:flex;justify-content:center;line-height:1.4;margin-top:10px;overflow:hidden;text-align:center;white-space:nowrap}.site-state-item{padding:0 15px;}.site-state-item:not(:first-child){border-left:1px solid #eee}.site-state-item a{border-bottom:none}.site-state-item-count{display:block;font-size:1em;font-weight:600;text-align:center}.site-state-item-name{color:#999;font-size:.8125em}.footer{color:#999;font-size:.875em;padding:20px 0;}.footer.footer-fixed{bottom:0;left:0;position:absolute;right:0}.footer-inner{box-sizing:border-box;margin:0 auto;text-align:center;width:calc(100% - 20px);}@media (min-width:1200px){.footer-inner{width:1160px}}@media (min-width:1600px){.footer-inner{width:73%}}.languages{display:inline-block;font-size:1.125em;position:relative;}.languages .lang-select-label span{margin:0 .5em}.languages .lang-select{height:100%;left:0;opacity:0;position:absolute;top:0;width:100%}.with-love{color:#FF6666;display:inline-block;margin:0 5px;animation:iconAnimate 1.33s ease-in-out infinite}.powered-by,.theme-info{display:inline-block}@-moz-keyframes iconAnimate{0%,100%{transform:scale(1)}10%,30%{transform:scale(.9)}20%,40%,60%,80%{transform:scale(1.1)}50%,70%{transform:scale(1.1)}}@-webkit-keyframes iconAnimate{0%,100%{transform:scale(1)}10%,30%{transform:scale(.9)}20%,40%,60%,80%{transform:scale(1.1)}50%,70%{transform:scale(1.1)}}@-o-keyframes iconAnimate{0%,100%{transform:scale(1)}10%,30%{transform:scale(.9)}20%,40%,60%,80%{transform:scale(1.1)}50%,70%{transform:scale(1.1)}}@keyframes iconAnimate{0%,100%{transform:scale(1)}10%,30%{transform:scale(.9)}20%,40%,60%,80%{transform:scale(1.1)}50%,70%{transform:scale(1.1)}}.back-to-top{font-size:12px;text-align:center;transition-delay:0s;transition-duration:.2s;transition-timing-function:ease-in-out}.back-to-top{background:#222;bottom:-100px;box-sizing:border-box;color:#fff;cursor:pointer;left:30px;opacity:.6;padding:0 6px;position:fixed;transition-property:bottom;z-index:1300;width:24px;}.back-to-top span{display:none}.back-to-top:hover{color:#fc6423}.back-to-top.back-to-top-on{bottom:30px}@media (max-width:991px){.back-to-top{left:20px;opacity:.8}}.post-body{font-family:'Lato',"PingFang SC","Microsoft YaHei",sans-serif;overflow-wrap:break-word;word-wrap:break-word;}@media (min-width:1200px){.post-body{font-size:1.125em}}.post-body .exturl .fa{font-size:.875em;margin-left:4px}.post-body .image-caption,.post-body .figure .caption{color:#999;font-size:.875em;font-weight:bold;line-height:1;margin:-20px auto 15px;text-align:center}.post-sticky-flag{display:inline-block;transform:rotate(30deg)}.post-button{margin-top:40px;text-align:center}.use-motion .post-block,.use-motion .pagination,.use-motion .comments{opacity:0}.use-motion .post-header{opacity:0}.use-motion .post-body{opacity:0}.use-motion .collection-header{opacity:0}.posts-collapse{margin-left:35px;position:relative;}@media (max-width:767px){.posts-collapse{margin-left:0;margin-right:0}}.posts-collapse .collection-title{font-size:1.125em;position:relative;}.posts-collapse .collection-title::before{background:#999;border:1px solid #fff;border-radius:50%;content:' ';height:10px;left:0;margin-left:-6px;margin-top:-4px;position:absolute;top:50%;width:10px}.posts-collapse .collection-year{font-size:1.5em;font-weight:bold;margin:60px 0;position:relative;}.posts-collapse .collection-year::before{background:#bbb;border-radius:50%;content:' ';height:8px;left:0;margin-left:-4px;margin-top:-4px;position:absolute;top:50%;width:8px}.posts-collapse .collection-header{display:block;margin:0 0 0 20px;}.posts-collapse .collection-header small{color:#bbb;margin-left:5px}.posts-collapse .post-header{border-bottom:1px dashed #ccc;margin:30px 0;padding-left:15px;position:relative;transition-property:border;transition-delay:0s;transition-duration:.2s;transition-timing-function:ease-in-out;}.posts-collapse .post-header::before{background:#bbb;border:1px solid #fff;border-radius:50%;content:' ';height:6px;left:0;margin-left:-4px;position:absolute;top:.75em;transition-property:background;width:6px;transition-delay:0s;transition-duration:.2s;transition-timing-function:ease-in-out}.posts-collapse .post-header:hover{border-bottom-color:#666;}.posts-collapse .post-header:hover::before{background:#222}.posts-collapse .post-meta{display:inline;font-size:.75em;margin-right:10px}.posts-collapse .post-title{display:inline;}.posts-collapse .post-title a,.posts-collapse .post-title span.exturl{border-bottom:none;color:var(--link-color)}.posts-collapse::before{background:#f5f5f5;content:' ';height:100%;left:0;margin-left:-2px;position:absolute;top:1.25em;width:4px}.posts-collapse .fa-external-link{font-size:.875em;margin-left:5px}.post-eof{background:#ccc;height:1px;margin:80px auto 60px;text-align:center;width:8%;}.post-block:last-of-type .post-eof{display:none}.content{padding-top:40px}@media (min-width:992px){.post-body{text-align:justify}}@media (max-width:991px){.post-body{text-align:justify}}.post-body h1,.post-body h2,.post-body h3,.post-body h4,.post-body h5,.post-body h6{padding-top:10px;}.post-body h1 .header-anchor,.post-body h2 .header-anchor,.post-body h3 .header-anchor,.post-body h4 .header-anchor,.post-body h5 .header-anchor,.post-body h6 .header-anchor{border-bottom-style:none;color:#ccc;float:right;margin-left:10px;visibility:hidden;}.post-body h1 .header-anchor:hover,.post-body h2 .header-anchor:hover,.post-body h3 .header-anchor:hover,.post-body h4 .header-anchor:hover,.post-body h5 .header-anchor:hover,.post-body h6 .header-anchor:hover{color:inherit}.post-body h1:hover .header-anchor,.post-body h2:hover .header-anchor,.post-body h3:hover .header-anchor,.post-body h4:hover .header-anchor,.post-body h5:hover .header-anchor,.post-body h6:hover .header-anchor{visibility:visible}.post-body iframe,.post-body img,.post-body video{margin-bottom:20px}.post-body .video-container{height:0;margin-bottom:20px;overflow:hidden;padding-top:75%;position:relative;width:100%;}.post-body .video-container iframe,.post-body .video-container object,.post-body .video-container embed{height:100%;left:0;margin:0;position:absolute;top:0;width:100%}.post-gallery{align-items:center;display:grid;grid-gap:10px;grid-template-columns:1fr 1fr 1fr;margin-bottom:20px;}@media (max-width:767px){.post-gallery{grid-template-columns:1fr 1fr}}.post-gallery a{border:0}.post-gallery img{margin:0}.posts-expand .post-header{font-size:1.125em}.posts-expand .post-title{font-size:1.5em;font-weight:normal;margin:initial;text-align:center;overflow-wrap:break-word;word-wrap:break-word;}.posts-expand .post-title-link{border-bottom:none;color:var(--link-color);display:inline-block;position:relative;vertical-align:top;}.posts-expand .post-title-link::before{background:var(--link-color);bottom:0;content:'';height:2px;left:0;position:absolute;transform:scaleX(0);visibility:hidden;width:100%;transition-delay:0s;transition-duration:.2s;transition-timing-function:ease-in-out}.posts-expand .post-title-link:hover::before{transform:scaleX(1);visibility:visible}.posts-expand .post-title-link .fa{font-size:.875em;margin-left:5px}.posts-expand .post-meta{color:#999;font-family:'Lato',"PingFang SC","Microsoft YaHei",sans-serif;font-size:.75em;margin:3px 0 60px 0;text-align:center;}.posts-expand .post-meta .post-description{font-size:.875em;margin-top:2px}.posts-expand .post-meta time{border-bottom:1px dashed #999;cursor:pointer}.post-meta .post-meta-item + .post-meta-item::before{content:'|';margin:0 .5em}.post-meta-divider{margin:0 .5em}.post-meta-item-icon{margin-right:3px;}@media (max-width:991px){.post-meta-item-icon{display:inline-block}}@media (max-width:991px){.post-meta-item-text{display:none}}.post-nav{border-top:1px solid #eee;display:flex;justify-content:space-between;margin-top:15px;padding-top:10px}.post-nav-item{flex:1;}.post-nav-item a{border-bottom:none;display:block;font-size:.875em;line-height:1.6;position:relative;}.post-nav-item a:active{top:2px}.post-nav-item .fa{font-size:.75em}.post-nav-item:first-child{margin-right:15px;}.post-nav-item:first-child a{padding-left:5px}.post-nav-item:first-child .fa{margin-right:5px}.post-nav-item:last-child{margin-left:15px;text-align:right;}.post-nav-item:last-child a{padding-right:5px}.post-nav-item:last-child .fa{margin-left:5px}.rtl.post-body p,.rtl.post-body a,.rtl.post-body h1,.rtl.post-body h2,.rtl.post-body h3,.rtl.post-body h4,.rtl.post-body h5,.rtl.post-body h6,.rtl.post-body li,.rtl.post-body ul,.rtl.post-body ol{direction:rtl;font-family:UKIJ Ekran}.rtl.post-title{font-family:UKIJ Ekran}.post-tags{margin-top:40px;text-align:center;}.post-tags a{display:inline-block;font-size:.8125em;}.post-tags a:not(:last-child){margin-right:10px}.post-widgets{border-top:1px solid #eee;margin-top:15px;text-align:center}.wp_rating{height:20px;line-height:20px;margin-top:10px;padding-top:6px;text-align:center}.social-like{display:flex;font-size:.875em;justify-content:center;text-align:center}.reward-container{margin:20px auto;padding:10px 0;text-align:center;width:90%;}.reward-container button{background:#ff2a2a;border:0;border-radius:5px;color:#fff;cursor:pointer;line-height:2;outline:0;padding:0 15px;vertical-align:text-top;}.reward-container button:hover{background:#f55}#qr{padding-top:20px;}#qr a{border:0}#qr img{display:inline-block;margin:.8em 2em 0 2em;max-width:100%;width:180px}#qr p{text-align:center}.category-all-page .category-all-title{text-align:center}.category-all-page .category-all{margin-top:20px}.category-all-page .category-list{list-style:none;margin:0;padding:0}.category-all-page .category-list-item{margin:5px 10px}.category-all-page .category-list-count{color:#bbb;}.category-all-page .category-list-count::before{content:' (';display:inline}.category-all-page .category-list-count::after{content:') ';display:inline}.category-all-page .category-list-child{padding-left:10px}.event-list{padding:0;}.event-list hr{background:#222;margin:20px 0 45px 0;}.event-list hr::after{background:#222;color:#fff;content:'NOW';display:inline-block;font-weight:bold;padding:0 5px;text-align:right}.event-list .event{background:#222;margin:20px 0;min-height:40px;padding:15px 0 15px 10px;}.event-list .event .event-summary{color:#fff;margin:0;padding-bottom:3px;}.event-list .event .event-summary::before{animation:dot-flash 1s alternate infinite ease-in-out;color:#fff;content:'\f111';display:inline-block;font-family:'FontAwesome';font-size:10px;margin-right:25px;vertical-align:middle}.event-list .event .event-relative-time{color:#bbb;display:inline-block;font-size:12px;font-weight:normal;padding-left:12px}.event-list .event .event-details{color:#fff;display:block;line-height:18px;margin-left:56px;padding-bottom:6px;padding-top:3px;text-indent:-24px;}.event-list .event .event-details::before{color:#fff;display:inline-block;font-family:'FontAwesome';margin-right:9px;text-align:center;text-indent:0;width:14px}.event-list .event .event-details.event-location::before{content:'\f041'}.event-list .event .event-details.event-duration::before{content:'\f017'}.event-list .event-past{background:#f5f5f5;}.event-list .event-past .event-summary,.event-list .event-past .event-details{color:#bbb;opacity:.9;}.event-list .event-past .event-summary::before,.event-list .event-past .event-details::before{animation:none;color:#bbb}@-moz-keyframes dot-flash{from{opacity:1;transform:scale(1)}to{opacity:0;transform:scale(.8)}}@-webkit-keyframes dot-flash{from{opacity:1;transform:scale(1)}to{opacity:0;transform:scale(.8)}}@-o-keyframes dot-flash{from{opacity:1;transform:scale(1)}to{opacity:0;transform:scale(.8)}}@keyframes dot-flash{from{opacity:1;transform:scale(1)}to{opacity:0;transform:scale(.8)}}ul.breadcrumb{font-size:.75em;list-style:none;margin:1em 0;padding:0 2em;text-align:center;}ul.breadcrumb li{display:inline}ul.breadcrumb li + li::before{content:'/\00a0';font-weight:normal;padding:.5em}ul.breadcrumb li + li:last-child{font-weight:bold}.tag-cloud{text-align:center;}.tag-cloud a{display:inline-block;margin:10px;}.tag-cloud a:hover{color:#222 !important}.search-pop-overlay{background:rgba(0,0,0,0);height:100%;left:0;position:fixed;top:0;transition:visibility 0s linear .2s,background .2s;visibility:hidden;width:100%;z-index:1400;}.search-pop-overlay.search-active{background:rgba(0,0,0,0.3);transition:background .2s;visibility:visible}.search-popup{background:var(--card-bg-color);border-radius:5px;height:80%;left:calc(50% - 350px);position:fixed;top:10%;transform:scale(0);transition:transform .2s;width:700px;z-index:1500;}.search-active .search-popup{transform:scale(1)}@media (max-width:767px){.search-popup{border-radius:0;height:100%;left:0;margin:0;top:0;width:100%}}.search-popup .search-icon,.search-popup .popup-btn-close{color:#999;font-size:18px;padding:0 10px}.search-popup .popup-btn-close{cursor:pointer;}.search-popup .popup-btn-close:hover .fa{color:#222}.search-popup .search-header{background:#eee;border-top-left-radius:5px;border-top-right-radius:5px;display:flex;padding:5px}.search-popup input.search-input{background:transparent;border:0;outline:0;width:100%;}.search-popup input.search-input::-webkit-search-cancel-button{display:none}.search-popup .search-input-container{flex-grow:1;padding:2px}.search-popup ul.search-result-list{margin:0 5px;padding:0;width:100%}.search-popup p.search-result{border-bottom:1px dashed #ccc;padding:5px 0}.search-popup a.search-result-title{font-weight:bold}.search-popup .search-keyword{border-bottom:1px dashed #ff2a2a;color:#ff2a2a;font-weight:bold}.search-popup #search-result{display:flex;height:calc(100% - 55px);overflow:auto;padding:5px 25px}.search-popup #no-result{color:#ccc;margin:auto}.header{margin:0 auto;position:relative;width:calc(100% - 20px);}@media (min-width:1200px){.header{width:1160px}}@media (min-width:1600px){.header{width:73%}}@media (max-width:991px){.header{width:auto}}.header-inner{background:var(--content-bg-color);border-radius:initial;box-shadow:initial;overflow:hidden;padding:0;position:absolute;top:0;width:240px;}@media (min-width:1200px){.header-inner{width:240px}}@media (max-width:991px){.header-inner{border-radius:initial;position:relative;width:auto}}.main-inner{align-items:flex-start;display:flex;justify-content:space-between;flex-direction:row-reverse;}@media (max-width:991px){.main-inner{width:auto}}.content-wrap{background:var(--content-bg-color);border-radius:initial;box-shadow:initial;box-sizing:border-box;padding:40px;width:calc(100% - 252px);}@media (max-width:991px){.content-wrap{border-radius:initial;padding:20px;width:100%}}.footer-inner{padding-left:260px}.back-to-top{left:auto;right:30px;}@media (max-width:991px){.back-to-top{right:20px}}@media (max-width:991px){.footer-inner{padding-left:0;padding-right:0;width:auto}}.site-brand-container{background:#222;}@media (max-width:991px){.site-brand-container{box-shadow:0 0 16px rgba(0,0,0,0.5)}}.site-meta{padding:20px 0}.brand{padding:0}.site-subtitle{margin:10px 10px 0}.custom-logo-image{margin-top:20px;}@media (max-width:991px){.custom-logo-image{display:none}}@media (min-width:768px) and (max-width:991px){.site-nav-toggle,.site-nav-right{display:flex;flex-direction:column;justify-content:center}}.site-nav-toggle .toggle,.site-nav-right .toggle{color:#fff;}.site-nav-toggle .toggle .toggle-line,.site-nav-right .toggle .toggle-line{background:#fff}@media (min-width:768px) and (max-width:991px){.site-nav{display:none}}.menu-item-active a::after{background:#bbb;border-radius:50%;content:' ';height:6px;margin-top:-3px;position:absolute;right:15px;top:50%;width:6px}.menu .menu-item{display:block;margin:0;}.menu .menu-item a,.menu .menu-item span.exturl{padding:5px 20px;position:relative;text-align:left;transition-property:background-color;}@media (max-width:991px){.menu .menu-item.menu-item-search{display:none}}.menu .menu-item .badge{background:#ccc;border-radius:10px;color:#fff;float:right;padding:2px 5px;text-shadow:1px 1px 0 rgba(0,0,0,0.1);vertical-align:middle}.sub-menu{background:var(--content-bg-color);border-bottom:1px solid #ddd;margin:0;padding:6px 0;}.sub-menu .menu-item{display:inline-block;}.sub-menu .menu-item a,.sub-menu .menu-item span.exturl{background:transparent;margin:5px 10px;padding:initial;}.sub-menu .menu-item a:hover,.sub-menu .menu-item span.exturl:hover{background:transparent;color:#fc6423}.sub-menu .menu-item a::after,.sub-menu .menu-item span.exturl::after{content:initial !important}.sub-menu .menu-item-active a{border-bottom-color:#fc6423;color:#fc6423;}.sub-menu .menu-item-active a:hover{border-bottom-color:#fc6423}.sidebar{background:var(--body-bg-color);box-shadow:none;margin-top:100%;position:static;width:240px;}@media (max-width:991px){.sidebar{display:none}}.sidebar-toggle{display:none}.sidebar-inner{background:var(--content-bg-color);border-radius:initial;box-shadow:initial;box-sizing:border-box;color:var(--text-color);width:240px;opacity:0;}.sidebar-inner.affix{position:fixed;top:12px}.sidebar-inner.affix-bottom{position:absolute}.site-state-item{padding:0 10px}.sidebar-button{border-bottom:1px dotted #ccc;border-top:1px dotted #ccc;margin-top:10px;text-align:center;}.sidebar-button a{border:0;color:#fc6423;display:block;}.sidebar-button a:hover{background:none;border:0;color:#e34603;}.sidebar-button a:hover .fa{color:#e34603}.links-of-author{display:flex;flex-wrap:wrap;margin-top:10px;justify-content:center}.links-of-author-item{margin:5px 0 0;width:50%;}.links-of-author-item a,.links-of-author-item span.exturl{box-sizing:border-box;display:inline-block;margin-bottom:0;margin-right:0;max-width:216px;overflow:hidden;padding:0 5px;text-overflow:ellipsis;white-space:nowrap}.links-of-author-item a,.links-of-author-item span.exturl{border-bottom:none;display:block;text-decoration:none;}.links-of-author-item a::before,.links-of-author-item span.exturl::before{display:none}.links-of-author-item a:hover,.links-of-author-item span.exturl:hover{background:var(--body-bg-color);border-radius:4px}.links-of-author-item .fa{margin-right:2px}.links-of-blogroll-item{padding:0;} \ No newline at end of file diff --git a/favicon.ico b/favicon.ico new file mode 100644 index 0000000000..6ef546425f Binary files /dev/null and b/favicon.ico differ diff --git a/images/algolia_logo.svg b/images/algolia_logo.svg new file mode 100644 index 0000000000..470242341d --- /dev/null +++ b/images/algolia_logo.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/images/apple-touch-icon-next.png b/images/apple-touch-icon-next.png new file mode 100644 index 0000000000..86a0d1d33b Binary files /dev/null and b/images/apple-touch-icon-next.png differ diff --git a/images/avatar.gif b/images/avatar.gif new file mode 100644 index 0000000000..28411fd0ea Binary files /dev/null and b/images/avatar.gif differ diff --git a/images/cc-by-nc-nd.svg b/images/cc-by-nc-nd.svg new file mode 100644 index 0000000000..79a4f2e0d1 --- /dev/null +++ b/images/cc-by-nc-nd.svg @@ -0,0 +1,121 @@ + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + diff --git a/images/cc-by-nc-sa.svg b/images/cc-by-nc-sa.svg new file mode 100644 index 0000000000..bf6bc26f54 --- /dev/null +++ b/images/cc-by-nc-sa.svg @@ -0,0 +1,121 @@ + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + diff --git a/images/cc-by-nc.svg b/images/cc-by-nc.svg new file mode 100644 index 0000000000..36973490ad --- /dev/null +++ b/images/cc-by-nc.svg @@ -0,0 +1,121 @@ + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + diff --git a/images/cc-by-nd.svg b/images/cc-by-nd.svg new file mode 100644 index 0000000000..934c61e15e --- /dev/null +++ b/images/cc-by-nd.svg @@ -0,0 +1,117 @@ + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + diff --git a/images/cc-by-sa.svg b/images/cc-by-sa.svg new file mode 100644 index 0000000000..463276a8cf --- /dev/null +++ b/images/cc-by-sa.svg @@ -0,0 +1,121 @@ + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + diff --git a/images/cc-by.svg b/images/cc-by.svg new file mode 100644 index 0000000000..4bccd14f6d --- /dev/null +++ b/images/cc-by.svg @@ -0,0 +1,121 @@ + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + diff --git a/images/cc-zero.svg b/images/cc-zero.svg new file mode 100644 index 0000000000..0f866392f1 --- /dev/null +++ b/images/cc-zero.svg @@ -0,0 +1,72 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/images/favicon-16x16-next.png b/images/favicon-16x16-next.png new file mode 100644 index 0000000000..de8c5d3a5f Binary files /dev/null and b/images/favicon-16x16-next.png differ diff --git a/images/favicon-32x32-next.png b/images/favicon-32x32-next.png new file mode 100644 index 0000000000..e02f5f4d5c Binary files /dev/null and b/images/favicon-32x32-next.png differ diff --git a/images/logo.svg b/images/logo.svg new file mode 100644 index 0000000000..cbb3937ecd --- /dev/null +++ b/images/logo.svg @@ -0,0 +1,23 @@ + +image/svg+xml diff --git a/images/quote-l.svg b/images/quote-l.svg new file mode 100644 index 0000000000..6dd94a4a05 --- /dev/null +++ b/images/quote-l.svg @@ -0,0 +1,13 @@ + + + + + + + + diff --git a/images/quote-r.svg b/images/quote-r.svg new file mode 100644 index 0000000000..312b64d71d --- /dev/null +++ b/images/quote-r.svg @@ -0,0 +1,12 @@ + + + + + + + + diff --git a/index.html b/index.html new file mode 100644 index 0000000000..e08957d87a --- /dev/null +++ b/index.html @@ -0,0 +1,415 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 贾攀的流水账 + + + + + + + + + + + + + + + +
+
+ +
+
+ + +
+ + + +

贾攀的流水账

+ +
+

Panmax's Blog

+
+ + +
+ + + + + + + + +
+ +
+ +
+
+ + +
+ + 0% +
+ + +
+
+
+ + +
+ +
+
人会长大三次。
+
-> 第一次是在发现自己不是世界中心的时候。
+
-> 第二次是在发现即使再怎么努力,终究还是有些事令人无能为力的时候。
+
-> 第三次是在,明知道有些事可能会无能为力,但还是会尽力争取的时候。
+
+
if you know me:
+ +
else:
+
return 关于我
+
+
+
-------------------------------------------
+
+
+
# my daughter's blog.
+ +
+
+ +
+ + + + +
+ + + + + + + + +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/js/Valine.min.js b/js/Valine.min.js new file mode 100644 index 0000000000..0ea2076d00 --- /dev/null +++ b/js/Valine.min.js @@ -0,0 +1,17 @@ +/*! + * Valine v1.4.18 + * (c) 2017-2022 xCss + * Released under the GPL-2.0 License. + * Last Update: 2022-3-21 11:52:27 ├F10: AM┤ + */ +!function(e,t){"object"==typeof exports&&"object"==typeof module?module.exports=t():"function"==typeof define&&define.amd?define([],t):"object"==typeof exports?exports.Valine=t():e.Valine=t()}(this,function(){return function(e){function t(r){if(n[r])return n[r].exports;var i=n[r]={i:r,l:!1,exports:{}};return e[r].call(i.exports,i,i.exports,t),i.l=!0,i.exports}var n={};return t.m=e,t.c=n,t.i=function(e){return e},t.d=function(e,n,r){t.o(e,n)||Object.defineProperty(e,n,{configurable:!1,enumerable:!0,get:r})},t.n=function(e){var n=e&&e.__esModule?function(){return e.default}:function(){return e};return t.d(n,"a",n),n},t.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},t.p="",t(t.s=119)}([function(e,t,n){"use strict";var r=SyntaxError,i=Function,o=TypeError,a=function(e){try{return i('"use strict"; return ('+e+").constructor;")()}catch(e){}},u=Object.getOwnPropertyDescriptor;if(u)try{u({},"")}catch(e){u=null}var s=function(){throw new o},l=u?function(){try{return arguments.callee,s}catch(e){try{return u(arguments,"callee").get}catch(e){return s}}}():s,c=n(22)(),f=Object.getPrototypeOf||function(e){return e.__proto__},p={},d="undefined"==typeof Uint8Array?void 0:f(Uint8Array),h={"%AggregateError%":"undefined"==typeof AggregateError?void 0:AggregateError,"%Array%":Array,"%ArrayBuffer%":"undefined"==typeof ArrayBuffer?void 0:ArrayBuffer,"%ArrayIteratorPrototype%":c?f([][Symbol.iterator]()):void 0,"%AsyncFromSyncIteratorPrototype%":void 0,"%AsyncFunction%":p,"%AsyncGenerator%":p,"%AsyncGeneratorFunction%":p,"%AsyncIteratorPrototype%":p,"%Atomics%":"undefined"==typeof Atomics?void 0:Atomics,"%BigInt%":"undefined"==typeof BigInt?void 0:BigInt,"%Boolean%":Boolean,"%DataView%":"undefined"==typeof DataView?void 0:DataView,"%Date%":Date,"%decodeURI%":decodeURI,"%decodeURIComponent%":decodeURIComponent,"%encodeURI%":encodeURI,"%encodeURIComponent%":encodeURIComponent,"%Error%":Error,"%eval%":eval,"%EvalError%":EvalError,"%Float32Array%":"undefined"==typeof Float32Array?void 0:Float32Array,"%Float64Array%":"undefined"==typeof Float64Array?void 0:Float64Array,"%FinalizationRegistry%":"undefined"==typeof FinalizationRegistry?void 0:FinalizationRegistry,"%Function%":i,"%GeneratorFunction%":p,"%Int8Array%":"undefined"==typeof Int8Array?void 0:Int8Array,"%Int16Array%":"undefined"==typeof Int16Array?void 0:Int16Array,"%Int32Array%":"undefined"==typeof Int32Array?void 0:Int32Array,"%isFinite%":isFinite,"%isNaN%":isNaN,"%IteratorPrototype%":c?f(f([][Symbol.iterator]())):void 0,"%JSON%":"object"==typeof JSON?JSON:void 0,"%Map%":"undefined"==typeof Map?void 0:Map,"%MapIteratorPrototype%":"undefined"!=typeof Map&&c?f((new Map)[Symbol.iterator]()):void 0,"%Math%":Math,"%Number%":Number,"%Object%":Object,"%parseFloat%":parseFloat,"%parseInt%":parseInt,"%Promise%":"undefined"==typeof Promise?void 0:Promise,"%Proxy%":"undefined"==typeof Proxy?void 0:Proxy,"%RangeError%":RangeError,"%ReferenceError%":ReferenceError,"%Reflect%":"undefined"==typeof Reflect?void 0:Reflect,"%RegExp%":RegExp,"%Set%":"undefined"==typeof Set?void 0:Set,"%SetIteratorPrototype%":"undefined"!=typeof Set&&c?f((new Set)[Symbol.iterator]()):void 0,"%SharedArrayBuffer%":"undefined"==typeof SharedArrayBuffer?void 0:SharedArrayBuffer,"%String%":String,"%StringIteratorPrototype%":c?f(""[Symbol.iterator]()):void 0,"%Symbol%":c?Symbol:void 0,"%SyntaxError%":r,"%ThrowTypeError%":l,"%TypedArray%":d,"%TypeError%":o,"%Uint8Array%":"undefined"==typeof Uint8Array?void 0:Uint8Array,"%Uint8ClampedArray%":"undefined"==typeof Uint8ClampedArray?void 0:Uint8ClampedArray,"%Uint16Array%":"undefined"==typeof Uint16Array?void 0:Uint16Array,"%Uint32Array%":"undefined"==typeof Uint32Array?void 0:Uint32Array,"%URIError%":URIError,"%WeakMap%":"undefined"==typeof WeakMap?void 0:WeakMap,"%WeakRef%":"undefined"==typeof WeakRef?void 0:WeakRef,"%WeakSet%":"undefined"==typeof WeakSet?void 0:WeakSet},v=function e(t){var n;if("%AsyncFunction%"===t)n=a("async function () {}");else if("%GeneratorFunction%"===t)n=a("function* () {}");else if("%AsyncGeneratorFunction%"===t)n=a("async function* () {}");else if("%AsyncGenerator%"===t){var r=e("%AsyncGeneratorFunction%");r&&(n=r.prototype)}else if("%AsyncIteratorPrototype%"===t){var i=e("%AsyncGenerator%");i&&(n=f(i.prototype))}return h[t]=n,n},g={"%ArrayBufferPrototype%":["ArrayBuffer","prototype"],"%ArrayPrototype%":["Array","prototype"],"%ArrayProto_entries%":["Array","prototype","entries"],"%ArrayProto_forEach%":["Array","prototype","forEach"],"%ArrayProto_keys%":["Array","prototype","keys"],"%ArrayProto_values%":["Array","prototype","values"],"%AsyncFunctionPrototype%":["AsyncFunction","prototype"],"%AsyncGenerator%":["AsyncGeneratorFunction","prototype"],"%AsyncGeneratorPrototype%":["AsyncGeneratorFunction","prototype","prototype"],"%BooleanPrototype%":["Boolean","prototype"],"%DataViewPrototype%":["DataView","prototype"],"%DatePrototype%":["Date","prototype"],"%ErrorPrototype%":["Error","prototype"],"%EvalErrorPrototype%":["EvalError","prototype"],"%Float32ArrayPrototype%":["Float32Array","prototype"],"%Float64ArrayPrototype%":["Float64Array","prototype"],"%FunctionPrototype%":["Function","prototype"],"%Generator%":["GeneratorFunction","prototype"],"%GeneratorPrototype%":["GeneratorFunction","prototype","prototype"],"%Int8ArrayPrototype%":["Int8Array","prototype"],"%Int16ArrayPrototype%":["Int16Array","prototype"],"%Int32ArrayPrototype%":["Int32Array","prototype"],"%JSONParse%":["JSON","parse"],"%JSONStringify%":["JSON","stringify"],"%MapPrototype%":["Map","prototype"],"%NumberPrototype%":["Number","prototype"],"%ObjectPrototype%":["Object","prototype"],"%ObjProto_toString%":["Object","prototype","toString"],"%ObjProto_valueOf%":["Object","prototype","valueOf"],"%PromisePrototype%":["Promise","prototype"],"%PromiseProto_then%":["Promise","prototype","then"],"%Promise_all%":["Promise","all"],"%Promise_reject%":["Promise","reject"],"%Promise_resolve%":["Promise","resolve"],"%RangeErrorPrototype%":["RangeError","prototype"],"%ReferenceErrorPrototype%":["ReferenceError","prototype"],"%RegExpPrototype%":["RegExp","prototype"],"%SetPrototype%":["Set","prototype"],"%SharedArrayBufferPrototype%":["SharedArrayBuffer","prototype"],"%StringPrototype%":["String","prototype"],"%SymbolPrototype%":["Symbol","prototype"],"%SyntaxErrorPrototype%":["SyntaxError","prototype"],"%TypedArrayPrototype%":["TypedArray","prototype"],"%TypeErrorPrototype%":["TypeError","prototype"],"%Uint8ArrayPrototype%":["Uint8Array","prototype"],"%Uint8ClampedArrayPrototype%":["Uint8ClampedArray","prototype"],"%Uint16ArrayPrototype%":["Uint16Array","prototype"],"%Uint32ArrayPrototype%":["Uint32Array","prototype"],"%URIErrorPrototype%":["URIError","prototype"],"%WeakMapPrototype%":["WeakMap","prototype"],"%WeakSetPrototype%":["WeakSet","prototype"]},m=n(9),y=n(25),b=m.call(Function.call,Array.prototype.concat),D=m.call(Function.apply,Array.prototype.splice),x=m.call(Function.call,String.prototype.replace),w=m.call(Function.call,String.prototype.slice),A=/[^%.[\]]+|\[(?:(-?\d+(?:\.\d+)?)|(["'])((?:(?!\2)[^\\]|\\.)*?)\2)\]|(?=(?:\.|\[\])(?:\.|\[\]|%$))/g,k=/\\(\\)?/g,E=function(e){var t=w(e,0,1),n=w(e,-1);if("%"===t&&"%"!==n)throw new r("invalid intrinsic syntax, expected closing `%`");if("%"===n&&"%"!==t)throw new r("invalid intrinsic syntax, expected opening `%`");var i=[];return x(e,A,function(e,t,n,r){i[i.length]=n?x(r,k,"$1"):t||e}),i},F=function(e,t){var n,i=e;if(y(g,i)&&(n=g[i],i="%"+n[0]+"%"),y(h,i)){var a=h[i];if(a===p&&(a=v(i)),void 0===a&&!t)throw new o("intrinsic "+e+" exists, but is not available. Please file an issue!");return{alias:n,name:i,value:a}}throw new r("intrinsic "+e+" does not exist!")};e.exports=function(e,t){if("string"!=typeof e||0===e.length)throw new o("intrinsic name must be a non-empty string");if(arguments.length>1&&"boolean"!=typeof t)throw new o('"allowMissing" argument must be a boolean');var n=E(e),i=n.length>0?n[0]:"",a=F("%"+i+"%",t),s=a.name,l=a.value,c=!1,f=a.alias;f&&(i=f[0],D(n,b([0,1],f)));for(var p=1,d=!0;p=n.length){var x=u(l,v);d=!!x,l=d&&"get"in x&&!("originalValue"in x.get)?x.get:l[v]}else d=y(l,v),l=l[v];d&&!c&&(h[s]=l)}}return l}},function(e,t,n){"use strict";var r=n(0),i=n(4),o=i(r("String.prototype.indexOf"));e.exports=function(e,t){var n=r(e,!!t);return"function"==typeof n&&o(e,".prototype.")>-1?i(n):n}},function(e,t,n){"use strict";var r=n(99),i="function"==typeof Symbol&&"symbol"==typeof Symbol("foo"),o=Object.prototype.toString,a=Array.prototype.concat,u=Object.defineProperty,s=function(e){return"function"==typeof e&&"[object Function]"===o.call(e)},l=u&&function(){var e={};try{u(e,"x",{enumerable:!1,value:e});for(var t in e)return!1;return e.x===e}catch(e){return!1}}(),c=function(e,t,n,r){(!(t in e)||s(r)&&r())&&(l?u(e,t,{configurable:!0,enumerable:!1,value:n,writable:!0}):e[t]=n)},f=function(e,t){var n=arguments.length>2?arguments[2]:{},o=r(t);i&&(o=a.call(o,Object.getOwnPropertySymbols(t)));for(var u=0;u"'`\\]/g,y=RegExp(m.source),b=/&(?:amp|lt|gt|quot|#39|#x60|#x5c);/g,D=RegExp(b.source),x={"&":"&","<":"<",">":">",'"':""","'":"'","`":"`","\\":"\"},w={};for(var A in x)w[x[A]]=A;var k=null;Array.prototype.forEach||(Array.prototype.forEach=function(e,t){var n,r;if(null==this)throw new TypeError(" this is null or not defined");var i=Object(this),o=i.length>>>0;if("function"!=typeof e)throw new TypeError(e+" is not a function");for(arguments.length>1&&(n=t),r=0;r":">",'"':""","'":"'"},s={"&":"&","<":"<",">":">",""":'"',"'":"'"},l=/(&|<|>|"|')/g,c=/[&<>"']/g;o.options=a.options={},e.exports={encode:o,escape:o,decode:a,unescape:a,version:"1.0.0-browser"}},function(e,t,n){"use strict";var r,i,o=Function.prototype.toString,a="object"==typeof Reflect&&null!==Reflect&&Reflect.apply;if("function"==typeof a&&"function"==typeof Object.defineProperty)try{r=Object.defineProperty({},"length",{get:function(){throw i}}),i={},a(function(){throw 42},null,r)}catch(e){e!==i&&(a=null)}else a=null;var u=/^\s*class\b/,s=function(e){try{var t=o.call(e);return u.test(t)}catch(e){return!1}},l=function(e){try{return!s(e)&&(o.call(e),!0)}catch(e){return!1}},c=Object.prototype.toString,f="function"==typeof Symbol&&!!Symbol.toStringTag,p="object"==typeof document&&void 0===document.all&&void 0!==document.all?document.all:{};e.exports=a?function(e){if(e===p)return!0;if(!e)return!1;if("function"!=typeof e&&"object"!=typeof e)return!1;if("function"==typeof e&&!e.prototype)return!0;try{a(e,null,r)}catch(e){if(e!==i)return!1}return!s(e)}:function(e){if(e===p)return!0;if(!e)return!1;if("function"!=typeof e&&"object"!=typeof e)return!1;if("function"==typeof e&&!e.prototype)return!0;if(f)return l(e);if(s(e))return!1;var t=c.call(e);return"[object Function]"===t||"[object GeneratorFunction]"===t}},function(e,t){e.exports={indexOf:function(e,t){var n,r;if(Array.prototype.indexOf)return e.indexOf(t);for(n=0,r=e.length;n';return n.test(o)?a:""}};t.default=i},function(e,t,n){"use strict";var r=n(0),i=n(1),o=r("%TypeError%"),a=n(59),u=n(18),s=n(60),l=n(62),c=n(63),f=n(67),p=n(20),d=n(92),h=i("String.prototype.split"),v=Object("a"),g="a"!==v[0]||!(0 in v);e.exports=function(e){var t=f(this),n=g&&d(this)?h(this,""):t,r=c(n);if(!l(e))throw new o("Array.prototype.forEach callback must be a function");var i;arguments.length>1&&(i=arguments[1]);for(var v=0;v=0&&"[object Function]"===r.call(e.callee)),n}},function(e,t,n){"use strict";var r=n(5),i=n(1),o=i("Object.prototype.propertyIsEnumerable"),a=i("Array.prototype.push");e.exports=function(e){var t=r(e),n=[];for(var i in t)o(t,i)&&a(n,[i,t[i]]);return n}},function(e,t,n){"use strict";var r=n(31);e.exports=function(){return"function"==typeof Object.entries?Object.entries:r}},function(e,t,n){"use strict";var r=n(5),i=n(20),o=n(1),a=o("String.prototype.replace"),u=/^[\x09\x0A\x0B\x0C\x0D\x20\xA0\u1680\u180E\u2000\u2001\u2002\u2003\u2004\u2005\u2006\u2007\u2008\u2009\u200A\u202F\u205F\u3000\u2028\u2029\uFEFF]+/,s=/[\x09\x0A\x0B\x0C\x0D\x20\xA0\u1680\u180E\u2000\u2001\u2002\u2003\u2004\u2005\u2006\u2007\u2008\u2009\u200A\u202F\u205F\u3000\u2028\u2029\uFEFF]+$/;e.exports=function(){var e=i(r(this));return a(a(e,u,""),s,"")}},function(e,t,n){"use strict";var r=n(33),i="​";e.exports=function(){return String.prototype.trim&&i.trim()===i?String.prototype.trim:r}},function(e,t,n){function r(){return{a:["target","href","title"],abbr:["title"],address:[],area:["shape","coords","href","alt"],article:[],aside:[],audio:["autoplay","controls","crossorigin","loop","muted","preload","src"],b:[],bdi:["dir"],bdo:["dir"],big:[],blockquote:["cite"],br:[],caption:[],center:[],cite:[],code:[],col:["align","valign","span","width"],colgroup:["align","valign","span","width"],dd:[],del:["datetime"],details:["open"],div:[],dl:[],dt:[],em:[],figcaption:[],figure:[],font:["color","size","face"],footer:[],h1:[],h2:[],h3:[],h4:[],h5:[],h6:[],header:[],hr:[],i:[],img:["src","alt","title","width","height"],ins:["datetime"],li:[],mark:[],nav:[],ol:[],p:[],pre:[],s:[],section:[],small:[],span:[],sub:[],summary:[],sup:[],strong:[],strike:[],table:["width","border","align","valign"],tbody:["align","valign"],td:["width","rowspan","colspan","align","valign"],tfoot:["align","valign"],th:["width","rowspan","colspan","align","valign"],thead:["align","valign"],tr:["rowspan","align","valign"],tt:[],u:[],ul:[],video:["autoplay","controls","crossorigin","loop","muted","playsinline","poster","preload","src","height","width"]}}function i(e,t,n){}function o(e,t,n){}function a(e,t,n){}function u(e,t,n){}function s(e){return e.replace(E,"<").replace(F,">")}function l(e,t,n,r){if(n=v(n),"href"===t||"src"===t){if("#"===(n=A.trim(n)))return"#";if("http://"!==n.substr(0,7)&&"https://"!==n.substr(0,8)&&"mailto:"!==n.substr(0,7)&&"tel:"!==n.substr(0,4)&&"data:image/"!==n.substr(0,11)&&"ftp://"!==n.substr(0,6)&&"./"!==n.substr(0,2)&&"../"!==n.substr(0,3)&&"#"!==n[0]&&"/"!==n[0])return""}else if("background"===t){if(j.lastIndex=0,j.test(n))return""}else if("style"===t){if($.lastIndex=0,$.test(n))return"";if(T.lastIndex=0,T.test(n)&&(j.lastIndex=0,j.test(n)))return"";!1!==r&&(r=r||k,n=r.process(n))}return n=g(n)}function c(e){return e.replace(C,""")}function f(e){return e.replace(S,'"')}function p(e){return e.replace(_,function(e,t){return"x"===t[0]||"X"===t[0]?String.fromCharCode(parseInt(t.substr(1),16)):String.fromCharCode(parseInt(t,10))})}function d(e){return e.replace(O,":").replace(B," ")}function h(e){for(var t="",n=0,r=e.length;n/g,C=/"/g,S=/"/g,_=/&#([a-zA-Z0-9]*);?/gim,O=/:?/gim,B=/&newline;?/gim,j=/((j\s*a\s*v\s*a|v\s*b|l\s*i\s*v\s*e)\s*s\s*c\s*r\s*i\s*p\s*t\s*|m\s*o\s*c\s*h\s*a)\:/gi,$=/e\s*x\s*p\s*r\s*e\s*s\s*s\s*i\s*o\s*n\s*\(.*/gi,T=/u\s*r\s*l\s*\(.*/gi;t.whiteList=r(),t.getDefaultWhiteList=r,t.onTag=i,t.onIgnoreTag=o,t.onTagAttr=a,t.onIgnoreTagAttr=u,t.safeAttrValue=l,t.escapeHtml=s,t.escapeQuote=c,t.unescapeQuote=f,t.escapeHtmlEntities=p,t.escapeDangerHtml5Entities=d,t.clearNonPrintableCharacter=h,t.friendlyAttrValue=v,t.escapeAttrValue=g,t.onIgnoreTagStripAll=m,t.StripTagBody=y,t.stripCommentTag=b,t.stripBlankChar=D,t.cssFilter=k,t.getDefaultCSSWhiteList=w},function(e,t,n){function r(e){var t=f.spaceIndex(e);if(-1===t)var n=e.slice(1,-1);else var n=e.slice(1,t+1);return n=f.trim(n).toLowerCase(),"/"===n.slice(0,1)&&(n=n.slice(1)),"/"===n.slice(-1)&&(n=n.slice(0,-1)),n}function i(e){return""===d){o+=n(e.slice(a,u)),p=e.slice(u,l+1),f=r(p),o+=t(u,o.length,f,p,i(p)),a=l+1,u=!1;continue}if('"'===d||"'"===d)for(var h=1,v=e.charAt(l-h);""===v.trim()||"="===v;){if("="===v){s=d;continue e}v=e.charAt(l-++h)}}else if(d===s){s=!1;continue}}return a0;t--){var n=e[t];if(" "!==n)return"="===n?t:-1}}function l(e){return'"'===e[0]&&'"'===e[e.length-1]||"'"===e[0]&&"'"===e[e.length-1]}function c(e){return l(e)?e.substr(1,e.length-2):e}var f=n(12),p=/[^a-zA-Z0-9_:\.\-]/gim;t.parseTag=o,t.parseAttr=a},function(e,t,n){var r,i,o;/*! + autosize 4.0.4 + license: MIT + http://www.jacklmoore.com/autosize +*/ +!function(n,a){i=[e,t],r=a,void 0!==(o="function"==typeof r?r.apply(t,i):r)&&(e.exports=o)}(0,function(e,t){"use strict";function n(e){function t(t){var n=e.style.width;e.style.width="0px",e.offsetWidth,e.style.width=n,e.style.overflowY=t}function n(e){for(var t=[];e&&e.parentNode&&e.parentNode instanceof Element;)e.parentNode.scrollTop&&t.push({node:e.parentNode,scrollTop:e.parentNode.scrollTop}),e=e.parentNode;return t}function r(){if(0!==e.scrollHeight){var t=n(e),r=document.documentElement&&document.documentElement.scrollTop;e.style.height="",e.style.height=e.scrollHeight+u+"px",s=e.clientWidth,t.forEach(function(e){e.node.scrollTop=e.scrollTop}),r&&(document.documentElement.scrollTop=r)}}function i(){r();var n=Math.round(parseFloat(e.style.height)),i=window.getComputedStyle(e,null),o="content-box"===i.boxSizing?Math.round(parseFloat(i.height)):e.offsetHeight;if(o-1},get:function(n){return t[e.indexOf(n)]},set:function(n,r){-1===e.indexOf(n)&&(e.push(n),t.push(r))},delete:function(n){var r=e.indexOf(n);r>-1&&(e.splice(r,1),t.splice(r,1))}}}(),a=function(e){return new Event(e,{bubbles:!0})};try{new Event("test")}catch(e){a=function(e){var t=document.createEvent("Event");return t.initEvent(e,!0,!1),t}}var u=null;"undefined"==typeof window||"function"!=typeof window.getComputedStyle?(u=function(e){return e},u.destroy=function(e){return e},u.update=function(e){return e}):(u=function(e,t){return e&&Array.prototype.forEach.call(e.length?e:[e],function(e){return n(e)}),e},u.destroy=function(e){return e&&Array.prototype.forEach.call(e.length?e:[e],r),e},u.update=function(e){return e&&Array.prototype.forEach.call(e.length?e:[e],i),e}),t.default=u,e.exports=t.default})},function(e,t,n){"use strict";function r(e){return e&&e.__esModule?e:{default:e}}function i(e){return!!e&&this.init(e),this}function o(e){return new i(e)}var a=n(47),u=r(a),s=n(37),l=r(s),c=n(41),f=r(c),p=n(13),d=r(p),h=n(6),v=n(45),g=r(v),m=n(40),y=r(m),b=n(44),D=n(42),x=r(D),w=n(3),A=r(w),k=n(43),E=r(k),F=n(46),C=r(F),S=n(39),_=(r(S),{comment:"",nick:"",mail:"",link:"",ua:A.default.ua,url:"",QQAvatar:""}),O="",B={cdn:"https://gravatar.loli.net/avatar/",ds:["mp","identicon","monsterid","wavatar","robohash","retro",""],params:"",hide:!1};i.prototype.init=function(e){if("undefined"==typeof document)throw new Error("Sorry, Valine does not support Server-side rendering.");var t=this;return e&&(e=A.default.extend(h.CONFIG,e),t.i18n=(0,f.default)(e.lang||A.default.lang,e.langMode),t.cfg=e,d.default.maps=!!e.emojiMaps&&e.emojiMaps||d.default.maps,d.default.cdn=!!e.emojiCDN&&e.emojiCDN||d.default.cdn,t._init()),t},i.prototype._init=function(){var e=this;try{var t=e.cfg,n=t.avatar,r=t.avatarForce,i=t.avatar_cdn,o=t.visitor,a=t.path,u=void 0===a?location.pathname:a,s=t.pageSize,l=t.recordIP;e.cfg.path=u.replace(/index\.html?$/,"");var c=B.ds,f=r?"&q="+(0,h.RandomStr)():"";B.params="?d="+(c.indexOf(n)>-1?n:"mp")+"&v="+h.VERSION+f,B.hide="hide"===n,B.cdn=/^https?\:\/\//.test(i)?i:B.cdn,e.cfg.pageSize=isNaN(s)?10:s<1?10:s,l&&(0,b.recordIPFn)(function(e){return _.ip=e});var p=e.cfg.el||null,d=(0,A.default)(p);if(p=p instanceof HTMLElement?p:d[d.length-1]||null){e.$el=(0,A.default)(p),e.$el.addClass("v").attr("data-class","v"),B.hide&&e.$el.addClass("hide-avatar"),e.cfg.meta=(e.cfg.guest_info||e.cfg.meta||h.defaultMeta).filter(function(e){return h.defaultMeta.indexOf(e)>-1}),e.cfg.requiredFields=e.cfg.requiredFields.filter(function(e){return h.defaultMeta.indexOf(e)>-1});var v=(0==e.cfg.meta.length?h.defaultMeta:e.cfg.meta).map(function(t){var n="mail"==t?"email":"text";return h.defaultMeta.indexOf(t)>-1?'':""}),g='
'+v.join("")+'
Powered By Valine
v'+h.VERSION+"
";e.$el.html(g),e.$el.find(".cancel-reply").on("click",function(t){e.reset()});var m=e.$el.find(".vempty");e.$nodata={show:function(t){return m.html(t||e.i18n.t("sofa")).show(),e},hide:function(){return m.hide(),e}};var D=e.$el.find(".vload-bottom"),w=e.$el.find(".vload-top");e.$loading={show:function(t){return t&&w.show()||D.show(),e.$nodata.hide(),e},hide:function(){return w.hide(),D.hide(),0===e.$el.find(".vcard").length&&e.$nodata.show(),e}}}(0,y.default)(e.cfg,function(t){var n=(0,A.default)(".valine-comment-count"),r=0;!function t(n){var i=n[r++];if(i){var o=(0,A.default)(i).attr("data-xid");!!o&&e.Q(o).count().then(function(e){i.innerText=e,t(n)}).catch(function(e){i.innerText=0})}}(n),o&&$.add(AV.Object.extend("Counter"),e.cfg.path),e.$el&&e.bind()})}catch(t){(0,x.default)(e,t,"init")}};var j=function(e,t){var n=new e,r=new AV.ACL;r.setPublicReadAccess(!0),r.setPublicWriteAccess(!0),n.setACL(r),n.set("url",t.url),n.set("xid",t.xid),n.set("title",t.title),n.set("time",1),n.save().then(function(e){(0,A.default)(t.el).find(".leancloud-visitors-count").text(1)}).catch(function(e){})},$={add:function(e,t){var n=this,r=(0,A.default)(".leancloud_visitors,.leancloud-visitors");if(1===r.length){var i=r[0],o=decodeURI((0,A.default)(i).attr("id")),a=(0,A.default)(i).attr("data-flag-title"),u=encodeURI(o),s={el:i,url:o,xid:u,title:a};if(decodeURI(o)===decodeURI(t)){var l=new AV.Query(e);l.equalTo("url",o),l.find().then(function(t){if(t.length>0){var n=t[0];n.increment("time"),n.save().then(function(e){(0,A.default)(i).find(".leancloud-visitors-count").text(e.get("time"))}).catch(function(e){})}else j(e,s)}).catch(function(t){101==t.code?j(e,s):(0,x.default)(n,t)})}else $.show(e,r)}else $.show(e,r)},show:function(e,t){var n=[];if(t.forEach(function(e){var t=(0,A.default)(e).find(".leancloud-visitors-count");t&&t.text("0"),n.push(/\%/.test((0,A.default)(e).attr("id"))?decodeURI((0,A.default)(e).attr("id")):(0,A.default)(e).attr("id"))}),n.length){var r=new AV.Query(e);r.containedIn("url",n),r.find().then(function(e){e.length>0&&t.forEach(function(t){e.forEach(function(e){var n=e.get("xid")||encodeURI(e.get("url")),r=e.get("time"),i=(0,A.default)(t),o=i.attr("id");if((/\%/.test(o)?o:encodeURI(o))==n){var a=i.find(".leancloud-visitors-count");a&&a.text(r)}})})}).catch(function(e){})}}};i.prototype.Q=function(e){var t=this,n=arguments.length,r=t.cfg.clazzName;if(1==n){var i=new AV.Query(r);i.doesNotExist("rid");var o=new AV.Query(r);o.equalTo("rid","");var a=AV.Query.or(i,o);return"*"===e?a.exists("url"):a.equalTo("url",decodeURI(e)),a.addDescending("createdAt"),a.addDescending("insertedAt"),a}var u=JSON.stringify(arguments[1]).replace(/(\[|\])/g,""),s="select * from "+r+" where rid in ("+u+") order by -createdAt,-createdAt";return AV.Query.doCloudQuery(s)},i.prototype.installLocale=function(e,t){var n=this;return n.i18n(e,t),n},i.prototype.setPath=function(e){return this.config.path=e,this},i.prototype.bind=function(){var e=this,t=e.$el.find(".vemojis"),n=e.$el.find(".vpreview"),r=e.$el.find(".vemoji-btn"),i=e.$el.find(".vpreview-btn"),o=e.$el.find(".veditor"),a=d.default.maps,s=!1,c=function(e){var n=[];for(var r in a)a.hasOwnProperty(r)&&!!d.default.build(r)&&n.push(''+d.default.build(r)+"");t.html(n.join("")),s=!0,t.find("i").on("click",function(e){e.preventDefault(),D(o[0]," :"+(0,A.default)(this).attr("title")+":")})};e.$emoji={show:function(){return!s&&c(),e.$preview.hide(),t.show(),r.addClass("actived"),e.$emoji},hide:function(){return r.removeClass("actived"),t.hide(),e.$emoji}},e.$preview={show:function(){return O?(e.$emoji.hide(),i.addClass("actived"),n.html(O).show(),T()):e.$preview.hide(),e.$preview},hide:function(){return i.removeClass("actived"),n.hide().html(""),e.$preview}};var f=function(t){var r=(0,E.default)(t.val()||"");r||e.$preview.hide(),O!=r&&(O=r,i.hasClass("actived")>-1&&O!=n.html()&&n.html(O),(0,l.default)(t[0]),T())};r.on("click",function(t){r.hasClass("actived")?e.$emoji.hide():e.$emoji.show()}),i.on("click",function(t){i.hasClass("actived")?e.$preview.hide():e.$preview.show()});var p=e.cfg.meta,v={},m={veditor:"comment"};p.forEach(function(e){m["v"+e]=e});for(var y in m)m.hasOwnProperty(y)&&function(){var t=m[y],n=e.$el.find("."+y);v[t]=n,n.on("input change blur propertychange",function(r){e.cfg.enableQQ&&"blur"===r.type&&"nick"===t&&(isNaN(n.val())?A.default.store.get(h.QQCacheKey)&&A.default.store.get(h.QQCacheKey).nick!=n.val()&&(A.default.store.remove(h.QQCacheKey),_.nick=n.val(),_.mail="",_.QQAvatar=""):(0,b.fetchQQFn)(n.val(),function(e){var t=e.nick||n.val(),r=e.qq+"@qq.com";(0,A.default)(".vnick").val(t),(0,A.default)(".vmail").val(r),_.nick=t,_.mail=r,_.QQAvatar=e.pic})),"comment"===t?f(n):_[t]=A.default.escape(n.val().replace(/(^\s*)|(\s*$)/g,"")).substring(0,40)})}();var D=function(e,t){if(document.selection){e.focus();document.selection.createRange().text=t,e.focus()}else if(e.selectionStart||"0"==e.selectionStart){var n=e.selectionStart,r=e.selectionEnd,i=e.scrollTop;e.value=e.value.substring(0,n)+t+e.value.substring(r,e.value.length),e.focus(),e.selectionStart=n+t.length,e.selectionEnd=n+t.length,e.scrollTop=i}else e.focus(),e.value+=t;setTimeout(function(t){f((0,A.default)(e))},100)},w={no:1,size:e.cfg.pageSize,skip:e.cfg.pageSize},k=e.$el.find(".vpage");k.on("click",function(e){k.hide(),w.no++,F()});var F=function(){var t=w.size,n=w.no,r=Number(e.$el.find(".vnum").text());e.$loading.show();var i=e.Q(e.cfg.path);i.limit(t),i.skip((n-1)*t),i.find().then(function(i){if(w.skip=w.size,i&&i.length){var o=[];i.forEach(function(t){o.push(t.id),S(t,e.$el.find(".vcards"),!0)}),e.Q(e.cfg.path,o).then(function(e){(e&&e.results||[]).forEach(function(e){S(e,(0,A.default)('.vquote[data-self-id="'+e.get("rid")+'"]'))})}).catch(function(e){}),t*n0?(e.$el.find(".vcount").show().find(".vnum").text(t),F()):e.$loading.hide()}).catch(function(t){(0,x.default)(e,t,"count")});var S=function(t,n,r){var i=(0,A.default)('
'),o=t.get("ua"),a="";o&&!/ja/.test(e.cfg.lang)&&(o=A.default.detect(o),a=o.os?''+o.browser+" "+o.version+' '+o.os+" "+o.osVersion+"":""),"*"===e.cfg.path&&(a=''+t.get("url")+"");var s=t.get("link")?/^https?\:\/\//.test(t.get("link"))?t.get("link"):"http://"+t.get("link"):"",l=A.default.escape(t.get("nick")),c=s?''+l+"":''+l+"",f=B.hide?"":e.cfg.enableQQ&&t.get("QQAvatar")?'':'',p=f+'
'+c+" "+a+'
'+(0,g.default)(t.get("insertedAt"),e.i18n)+''+e.i18n.t("reply")+'
'+(0,C.default)(t.get("comment"))+'
';i.html(p);var d=i.find(".vat");i.find("a:not(.at)").forEach(function(e){(0,A.default)(e).attr({target:"_blank",rel:"noopener"})}),r?n.append(i):n.prepend(i);var h=i.find(".vcontent");h&&I(h),d&&$(d,t)},j={},$=function(t,n){t.on("click",function(r){var i=t.attr("data-vm-id"),o=t.attr("data-self-id"),a=e.$el.find(".vwrap"),u="@"+A.default.escape(n.get("nick"));(0,A.default)('.vreply-wrapper[data-self-id="'+o+'"]').append(a).find(".cancel-reply").show(),j={at:A.default.escape(u)+" ",rid:i,pid:o,rmail:n.get("mail")},v.comment.attr({placeholder:u})[0].focus()})},T=function(){setTimeout(function(){try{e.cfg.mathjax&&"MathJax"in window&&"version"in window.MathJax&&(/^3.*/.test(window.MathJax.version)&&MathJax.typeset()||MathJax.Hub.Queue(["Typeset",MathJax.Hub,document.querySelector(".v")])),"renderMathInElement"in window&&renderMathInElement((0,A.default)(".v")[0],{delimiters:[{left:"$$",right:"$$",display:!0},{left:"$",right:"$",display:!1}]})}catch(e){}},100)},I=function(e){setTimeout(function(){e[0].offsetHeight>200&&(e.addClass("expand"),e.on("click",function(t){e.removeClass("expand")}))})};!function(t){if(t=A.default.store.get(h.MetaCacheKey)||t)for(var n in p)if(p.hasOwnProperty(n)){var r=p[n];e.$el.find(".v"+r).val(A.default.unescape(t[r])),_[r]=t[r]}var i=A.default.store.get(h.QQCacheKey);_.QQAvatar=e.cfg.enableQQ&&!!i&&i.pic||""}(),e.reset=function(){_.comment="",v.comment.val(""),f(v.comment),v.comment.attr("placeholder",e.cfg.placeholder),j={},e.$preview.hide(),e.$el.find(".vpanel").append(e.$el.find(".vwrap")),e.$el.find(".cancel-reply").hide(),O=""};var z=e.$el.find(".vsubmit"),P=function(t){if(e.cfg.requiredFields.indexOf("nick")>-1&&_.nick.length<3)return v.nick[0].focus(),void e.$el.find(".status-bar").text(""+e.i18n.t("nickFail")).empty(3e3);if(e.cfg.requiredFields.indexOf("mail")>-1&&!/[\w-\.]+@([\w-]+\.)+[a-z]{2,3}/.test(_.mail))return v.mail[0].focus(),void e.$el.find(".status-bar").text(""+e.i18n.t("mailFail")).empty(3e3);if(""==O)return void v.comment[0].focus();_.comment=O,_.nick=_.nick||"Anonymous";var n=A.default.store.get("vlx");if(n){if(Date.now()/1e3-n/1e3<20)return void e.$el.find(".status-bar").text(e.i18n.t("busy")).empty(3e3)}M()},R=function(){var e=new AV.ACL;return e.setPublicReadAccess(!0),e.setPublicWriteAccess(!1),e},M=function(){A.default.store.set("vlx",Date.now()),z.attr({disabled:!0}),e.$loading.show(!0);var t=AV.Object.extend(e.cfg.clazzName||"Comment"),n=new t;if(_.url=decodeURI(e.cfg.path),_.insertedAt=new Date,j.rid){var r=j.pid||j.rid;n.set("rid",j.rid),n.set("pid",r),_.comment=O.replace("

",'

'+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('

 '+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'
'+(t&&hljs.getLanguage(t)?hljs.highlight(t,e).value:c.default.escape(e))+"
"},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/gi,"")};var o=function(e,t,n,r){if(/code|pre|span/i.test(e)){if("style"==t){var o=n.match(/color:([#a-z0-9]{3,7}|\s+[#a-z0-9]{3,8})/gi);return o&&o.length?'style="'+o[0]+'"':""}if("class"==t)return t+"='"+i.default.escapeAttrValue(n)+"'"}return"a"===e&&"class"==t&&"at"===n?t+"='"+i.default.escapeAttrValue(n)+"'":"img"===e&&/src|class/i.test(t)?t+"='"+i.default.escapeAttrValue(n)+"' referrerPolicy='no-referrer'":void 0}},function(e,t,n){var r;!function(i){"use strict";function o(e,t){var n=(65535&e)+(65535&t);return(e>>16)+(t>>16)+(n>>16)<<16|65535&n}function a(e,t){return e<>>32-t}function u(e,t,n,r,i,u){return o(a(o(o(t,e),o(r,u)),i),n)}function s(e,t,n,r,i,o,a){return u(t&n|~t&r,e,t,i,o,a)}function l(e,t,n,r,i,o,a){return u(t&r|n&~r,e,t,i,o,a)}function c(e,t,n,r,i,o,a){return u(t^n^r,e,t,i,o,a)}function f(e,t,n,r,i,o,a){return u(n^(t|~r),e,t,i,o,a)}function p(e,t){e[t>>5]|=128<>>9<<4)]=t;var n,r,i,a,u,p=1732584193,d=-271733879,h=-1732584194,v=271733878;for(n=0;n>5]>>>t%32&255);return n}function h(e){var t,n=[];for(n[(e.length>>2)-1]=void 0,t=0;t>5]|=(255&e.charCodeAt(t/8))<16&&(i=p(i,8*e.length)),n=0;n<16;n+=1)o[n]=909522486^i[n],a[n]=1549556828^i[n];return r=p(o.concat(h(t)),512+8*t.length),d(p(a.concat(r),640))}function m(e){var t,n,r="0123456789abcdef",i="";for(n=0;n>>4&15)+r.charAt(15&t);return i}function y(e){return unescape(encodeURIComponent(e))}function b(e){return v(y(e))}function D(e){return m(b(e))}function x(e,t){return g(y(e),y(t))}function w(e,t){return m(x(e,t))}function A(e,t,n){return t?n?x(t,e):w(t,e):n?b(e):D(e)}void 0!==(r=function(){return A}.call(t,n,t,e))&&(e.exports=r)}()},function(e,t,n){"use strict";var r=n(2),i=n(4),o=n(1),a=n(5),u=n(14),s=n(15),l=s(),c=n(49),f=o("Array.prototype.slice"),p=i.apply(l),d=function(e,t){return a(e),p(e,f(arguments,1))};r(d,{getPolyfill:s,implementation:u,shim:c}),e.exports=d},function(e,t,n){"use strict";var r=n(2),i=n(15);e.exports=function(){var e=i();return r(Array.prototype,{forEach:e},{forEach:function(){return Array.prototype.forEach!==e}}),e}},function(e,t,n){"use strict";function r(e){for(var t,n,i=Array.prototype.slice.call(arguments,1);i.length;){t=i.shift();for(n in t)t.hasOwnProperty(n)&&("[object Object]"===Object.prototype.toString.call(e[n])?e[n]=r(e[n],t[n]):e[n]=t[n])}return e}e.exports=r},function(e,t,n){"use strict";t.__esModule=!0;var r=n(53),i=function(e){return e&&e.__esModule?e:{default:e}}(r);t.default=function(e){return e=(0,i.default)({url:"",method:"get",body:{}},e),new Promise(function(t,n){if("jsonp"==e.method){var r="cb_"+(Date.now()+Math.round(1e3*Math.random())).toString(32),i=document,o=i.body,u=i.createElement("script");return e.body.callback=r,e.body.t=Date.now(),u.src=e.url+"?"+a(e.body),window[r]=function(e){window[r]=null,o.removeChild(u),t(e)},void o.appendChild(u)}var s="XMLHttpRequest"in window?new XMLHttpRequest:new ActiveXObject("Microsoft.XMLHTTP"),l=[],c=[],f={},p=function e(){return{ok:2==(s.status/100|0),statusText:s.statusText,status:s.status,url:s.responseURL,text:function(){return Promise.resolve(s.responseText)},json:function(){return Promise.resolve(s.responseText).then(JSON.parse)},blob:function(){return Promise.resolve(new Blob([s.response]))},clone:e,headers:{keys:function(){return l},entries:function(){return c},get:function(e){return f[e.toLowerCase()]},has:function(e){return e.toLowerCase()in f}}}};e.url=e.url+"?"+("get"==e.method?a(e.body):""),s.open(e.method||"get",e.url,!0),s.onload=function(){s.getAllResponseHeaders().replace(/^(.*?):[^\S\n]*([\s\S]*?)$/gm,function(e,t,n){l.push(t=t.toLowerCase()),c.push([t,n]),f[t]=f[t]?f[t]+","+n:n}),t(p())},s.onerror=n,s.withCredentials="include"==e.credentials;for(var d in e.headers)s.setRequestHeader(d,e.headers[d]);s.send("post"==e.method?e.body:"get"==e.method?null:a(e.body))})};var o=encodeURIComponent,a=function(e){var t=[];for(var n in e)e.hasOwnProperty(n)&&t.push(o(n)+"="+o(e[n]));return(t=t.join("&").replace(/%20/g,"+"))||null}},function(e,t,n){"use strict";t.__esModule=!0;var r=function(e){e=e||navigator.userAgent;var t={},n={Trident:e.indexOf("Trident")>-1||e.indexOf("NET CLR")>-1,Presto:e.indexOf("Presto")>-1,WebKit:e.indexOf("AppleWebKit")>-1,Gecko:e.indexOf("Gecko/")>-1,Safari:e.indexOf("Safari")>-1,Edge:e.indexOf("Edge")>-1||e.indexOf("Edg")>-1,Chrome:e.indexOf("Chrome")>-1||e.indexOf("CriOS")>-1,IE:e.indexOf("MSIE")>-1||e.indexOf("Trident")>-1,Firefox:e.indexOf("Firefox")>-1||e.indexOf("FxiOS")>-1,"Firefox Focus":e.indexOf("Focus")>-1,Chromium:e.indexOf("Chromium")>-1,Opera:e.indexOf("Opera")>-1||e.indexOf("OPR")>-1,Vivaldi:e.indexOf("Vivaldi")>-1,Yandex:e.indexOf("YaBrowser")>-1,Kindle:e.indexOf("Kindle")>-1||e.indexOf("Silk/")>-1,360:e.indexOf("360EE")>-1||e.indexOf("360SE")>-1,UC:e.indexOf("UC")>-1||e.indexOf(" UBrowser")>-1,QQBrowser:e.indexOf("QQBrowser")>-1,QQ:e.indexOf("QQ/")>-1,Baidu:e.indexOf("Baidu")>-1||e.indexOf("BIDUBrowser")>-1,Maxthon:e.indexOf("Maxthon")>-1,Sogou:e.indexOf("MetaSr")>-1||e.indexOf("Sogou")>-1,LBBROWSER:e.indexOf("LBBROWSER")>-1,"2345Explorer":e.indexOf("2345Explorer")>-1,TheWorld:e.indexOf("TheWorld")>-1,XiaoMi:e.indexOf("MiuiBrowser")>-1,Quark:e.indexOf("Quark")>-1,Qiyu:e.indexOf("Qiyu")>-1,Wechat:e.indexOf("MicroMessenger")>-1,Taobao:e.indexOf("AliApp(TB")>-1,Alipay:e.indexOf("AliApp(AP")>-1,Weibo:e.indexOf("Weibo")>-1,Douban:e.indexOf("com.douban.frodo")>-1,Suning:e.indexOf("SNEBUY-APP")>-1,iQiYi:e.indexOf("IqiyiApp")>-1,Windows:e.indexOf("Windows")>-1,Linux:e.indexOf("Linux")>-1||e.indexOf("X11")>-1,macOS:e.indexOf("Macintosh")>-1,Android:e.indexOf("Android")>-1||e.indexOf("Adr")>-1,Ubuntu:e.indexOf("Ubuntu")>-1,FreeBSD:e.indexOf("FreeBSD")>-1,Debian:e.indexOf("Debian")>-1,"Windows Phone":e.indexOf("IEMobile")>-1||e.indexOf("Windows Phone")>-1,BlackBerry:e.indexOf("BlackBerry")>-1||e.indexOf("RIM")>-1||e.indexOf("BB10")>-1,MeeGo:e.indexOf("MeeGo")>-1,Symbian:e.indexOf("Symbian")>-1,iOS:e.indexOf("like Mac OS X")>-1,"Chrome OS":e.indexOf("CrOS")>-1,WebOS:e.indexOf("hpwOS")>-1,Mobile:e.indexOf("Mobi")>-1||e.indexOf("iPh")>-1||e.indexOf("480")>-1,Tablet:e.indexOf("Tablet")>-1||e.indexOf("Pad")>-1||e.indexOf("Nexus 7")>-1};n.Mobile&&(n.Mobile=!(e.indexOf("iPad")>-1));var r={browser:["Safari","Chrome","Edge","IE","Firefox","Firefox Focus","Chromium","Opera","Vivaldi","Yandex","Kindle","360","UC","QQBrowser","QQ","Baidu","Maxthon","Sogou","LBBROWSER","2345Explorer","TheWorld","XiaoMi","Quark","Qiyu","Wechat","Taobao","Alipay","Weibo","Douban","Suning","iQiYi"],os:["Windows","Linux","Mac OS","macOS","Android","Ubuntu","FreeBSD","Debian","iOS","Windows Phone","BlackBerry","MeeGo","Symbian","Chrome OS","WebOS"]};for(var i in r)if(r.hasOwnProperty(i))for(var o=0,a=r[i].length;o-1){var n=function(){};e.__proto__={setItem:n,getItem:n,removeItem:n,clear:n}}}finally{"yes"===e.getItem(t)&&e.removeItem(t)}return e}(c),s.prototype={set:function(e,t){if(e&&!r(e))c.setItem(e,a(t));else if(r(e))for(var n in e)this.set(n,e[n]);return this},get:function(e){if(!e){var t={};return this.each(function(e,n){return t[e]=n}),t}if("?"===e.charAt(0))return this.has(e.substr(1));var n=arguments;if(n.length>1){for(var r={},i=0,o=n.length;i-1&&(n[t[r]]=this.get(t[r]));return n}};var f=null;for(var p in s.prototype)l[p]=s.prototype[p];t.default=l},function(e,t,n){var r,i;!function(n,o){var o=function(e,t,n){function r(i,o,a){return a=Object.create(r.fn),i&&a.push.apply(a,i[t]?[i]:""+i===i?/2?arguments[2]:[];if(!a(n))throw new o("Assertion failed: optional `argumentsList`, if provided, must be a List");return u(e,t,n)}},function(e,t,n){"use strict";var r=n(0),i=r("%TypeError%"),o=n(19),a=n(8);e.exports=function(e,t){if("Object"!==a(e))throw new i("Assertion failed: `O` must be an Object");if(!o(t))throw new i("Assertion failed: `P` must be a Property Key");return t in e}},function(e,t,n){"use strict";var r=n(0),i=r("%Array%"),o=!i.isArray&&n(1)("Object.prototype.toString");e.exports=i.isArray||function(e){return"[object Array]"===o(e)}},function(e,t,n){"use strict";e.exports=n(11)},function(e,t,n){"use strict";var r=n(0),i=r("%TypeError%"),o=n(18),a=n(65),u=n(8);e.exports=function(e){if("Object"!==u(e))throw new i("Assertion failed: `obj` must be an Object");return a(o(e,"length"))}},function(e,t,n){"use strict";var r=n(70),i=n(66);e.exports=function(e){var t=i(e);return 0!==t&&(t=r(t)),0===t?0:t}},function(e,t,n){"use strict";var r=n(79),i=n(64);e.exports=function(e){var t=i(e);return t<=0?0:t>r?r:t}},function(e,t,n){"use strict";var r=n(0),i=r("%TypeError%"),o=r("%Number%"),a=r("%RegExp%"),u=r("%parseInt%"),s=n(1),l=n(80),c=n(78),f=s("String.prototype.slice"),p=l(/^0b[01]+$/i),d=l(/^0o[0-7]+$/i),h=l(/^[-+]0x[0-9a-f]+$/i),v=["…","​","￾"].join(""),g=new a("["+v+"]","g"),m=l(g),y=["\t\n\v\f\r   ᠎    ","          \u2028","\u2029\ufeff"].join(""),b=new RegExp("(^["+y+"]+)|(["+y+"]+$)","g"),D=s("String.prototype.replace"),x=function(e){return D(e,b,"")},w=n(68);e.exports=function e(t){var n=c(t)?t:w(t,o);if("symbol"==typeof n)throw new i("Cannot convert a Symbol value to a number");if("bigint"==typeof n)throw new i("Conversion from 'BigInt' to 'number' is not allowed.");if("string"==typeof n){if(p(n))return e(u(f(n,2),2));if(d(n))return e(u(f(n,2),8));if(m(n)||h(n))return NaN;var r=x(n);if(r!==n)return e(r)}return o(n)}},function(e,t,n){"use strict";var r=n(0),i=r("%Object%"),o=n(5);e.exports=function(e){return o(e),i(e)}},function(e,t,n){"use strict";var r=n(83);e.exports=function(e){return arguments.length>1?r(e,arguments[1]):r(e)}},function(e,t,n){"use strict";var r=n(0),i=r("%TypeError%");e.exports=function(e,t){if(null==e)throw new i(t||"Cannot call method on "+e);return e}},function(e,t,n){"use strict";var r=n(74),i=n(75),o=n(71),a=n(77),u=n(76),s=n(81);e.exports=function(e){var t=o(e);return a(t)?0:0!==t&&u(t)?s(t)*i(r(t)):t}},function(e,t,n){"use strict";var r=n(72);e.exports=function(e){var t=r(e,Number);if("string"!=typeof t)return+t;var n=t.replace(/^[ \t\x0b\f\xa0\ufeff\n\r\u2028\u2029\u1680\u180e\u2000\u2001\u2002\u2003\u2004\u2005\u2006\u2007\u2008\u2009\u200a\u202f\u205f\u3000\u0085]+|[ \t\x0b\f\xa0\ufeff\n\r\u2028\u2029\u1680\u180e\u2000\u2001\u2002\u2003\u2004\u2005\u2006\u2007\u2008\u2009\u200a\u202f\u205f\u3000\u0085]+$/g,"");return/^0[ob]|^[+-]0x/.test(n)?NaN:+n}},function(e,t,n){"use strict";e.exports=n(84)},function(e,t,n){"use strict";e.exports=function(e){return null===e?"Null":void 0===e?"Undefined":"function"==typeof e||"object"==typeof e?"Object":"number"==typeof e?"Number":"boolean"==typeof e?"Boolean":"string"==typeof e?"String":void 0}},function(e,t,n){"use strict";var r=n(0),i=r("%Math.abs%");e.exports=function(e){return i(e)}},function(e,t,n){"use strict";var r=Math.floor;e.exports=function(e){return r(e)}},function(e,t,n){"use strict";var r=Number.isNaN||function(e){return e!==e};e.exports=Number.isFinite||function(e){return"number"==typeof e&&!r(e)&&e!==1/0&&e!==-1/0}},function(e,t,n){"use strict";e.exports=Number.isNaN||function(e){return e!==e}},function(e,t,n){"use strict";e.exports=function(e){return null===e||"function"!=typeof e&&"object"!=typeof e}},function(e,t,n){"use strict";var r=n(0),i=r("%Math%"),o=r("%Number%");e.exports=o.MAX_SAFE_INTEGER||i.pow(2,53)-1},function(e,t,n){"use strict";var r=n(0),i=r("RegExp.prototype.test"),o=n(4);e.exports=function(e){return o(i,e)}},function(e,t,n){"use strict";e.exports=function(e){return e>=0?1:-1}},function(e,t){e.exports=function(e){var t=!0,n=!0,r=!1;if("function"==typeof e){try{e.call("f",function(e,n,r){"object"!=typeof r&&(t=!1)}),e.call([null],function(){"use strict";n="string"==typeof this},"x")}catch(e){r=!0}return!r&&t&&n}return!1}},function(e,t,n){"use strict";var r="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator,i=n(21),o=n(11),a=n(91),u=n(93),s=function(e,t){if(void 0===e||null===e)throw new TypeError("Cannot call method on "+e);if("string"!=typeof t||"number"!==t&&"string"!==t)throw new TypeError('hint must be "string" or "number"');var n,r,a,u="string"===t?["toString","valueOf"]:["valueOf","toString"];for(a=0;a1&&(arguments[1]===String?t="string":arguments[1]===Number&&(t="number"));var n;if(r&&(Symbol.toPrimitive?n=l(e,Symbol.toPrimitive):u(e)&&(n=Symbol.prototype.valueOf)),void 0!==n){var o=n.call(e,t);if(i(o))return o;throw new TypeError("unable to convert exotic object to primitive")}return"default"===t&&(a(e)||u(e))&&(t="string"),s(e,"default"===t?"number":t)}},function(e,t,n){"use strict";var r=Object.prototype.toString,i=n(21),o=n(11),a={"[[DefaultValue]]":function(e){var t;if((t=arguments.length>1?arguments[1]:"[object Date]"===r.call(e)?String:Number)===String||t===Number){var n,a,u=t===String?["toString","valueOf"]:["valueOf","toString"];for(a=0;a1?a["[[DefaultValue]]"](e,arguments[1]):a["[[DefaultValue]]"](e)}},function(e,t,n){"use strict";var r=Array.prototype.slice,i=Object.prototype.toString;e.exports=function(e){var t=this;if("function"!=typeof t||"[object Function]"!==i.call(t))throw new TypeError("Function.prototype.bind called on incompatible "+t);for(var n,o=r.call(arguments,1),a=function(){if(this instanceof n){var i=t.apply(this,o.concat(r.call(arguments)));return Object(i)===i?i:this}return t.apply(e,o.concat(r.call(arguments)))},u=Math.max(0,t.length-o.length),s=[],l=0;l'+e+""}var t=function(e,t){return t={exports:{}},e(t,t.exports),t.exports}(function(e){var t=e.exports=function(){return new RegExp("(?:"+t.line().source+")|(?:"+t.block().source+")","gm")};t.line=function(){return/(?:^|\s)\/\/(.+?)$/gm},t.block=function(){return/\/\*([\S\s]*?)\*\//gm}}),n=["23AC69","91C132","F19726","E8552D","1AAB8E","E1147F","2980C1","1BA1E6","9FA0A0","F19726","E30B20","E30B20","A3338B"];return function(r,i){void 0===i&&(i={});var o=i.colors;void 0===o&&(o=n);var a=0,u={},s=/[\u4E00-\u9FFF\u3400-\u4dbf\uf900-\ufaff\u3040-\u309f\uac00-\ud7af\u0400-\u04FF]+|\w+/,l=/'+n+"";return a=++a%o.length,s})}})},function(e,t,n){"use strict";var r={allowedAttributes:{a:["href","name","target","title","aria-label"],iframe:["allowfullscreen","frameborder","src"],img:["src","alt","title","aria-label"]},allowedClasses:{},allowedSchemes:["http","https","mailto"],allowedTags:["a","abbr","article","b","blockquote","br","caption","code","del","details","div","em","h1","h2","h3","h4","h5","h6","hr","i","img","ins","kbd","li","main","mark","ol","p","pre","section","span","strike","strong","sub","summary","sup","table","tbody","td","th","thead","tr","u","ul"],filter:null};e.exports=r},function(e,t,n){"use strict";function r(e,t,n){var r=[],s=!0===n?t:i({},u,t),l=a(r,s);return o(e,l),r.join("")}var i=(n(10),n(50)),o=n(89),a=n(90),u=n(87);r.defaults=u,e.exports=r},function(e,t,n){"use strict";function r(){var e=[];return e.lastItem=function(){return e[e.length-1]},e}function i(e,t){function n(){"\x3c!--"===e.substr(0,4)?d():p.test(e)?i(l,g):f.test(e)&&i(s,v),h()}function i(t,n){var r=e.match(t);r&&(e=e.substring(r[0].length),r[0].replace(t,n),m=!1)}function d(){var n=e.indexOf("--\x3e");n>=0&&(t.comment&&t.comment(e.substring(4,n)),e=e.substring(n+3),m=!1)}function h(){if(m){var n,r=e.indexOf("<");r>=0?(n=e.substring(0,r),e=e.substring(r)):(n=e,e=""),t.chars&&t.chars(n)}}function v(e,n,r,i){function s(e,t,n,r,i){l[t]=void 0===n&&void 0===r&&void 0===i?void 0:o.decode(n||r||i||"")}var l={},f=a(n),p=u.voids[f]||!!i;r.replace(c,s),p||y.push(f),t.start&&t.start(f,l,p)}function g(e,n){var r,i=0,o=a(n);if(o)for(i=y.length-1;i>=0&&y[i]!==o;i--);if(i>=0){for(r=y.length-1;r>=i;r--)t.end&&t.end(y[r]);y.length=i}}for(var m,y=r(),b=e;e;)!function(){m=!0,n();var t=e===b;b=e,t&&(e="")}();g()}var o=n(10),a=n(28),u=(n(26),n(27)),s=/^<\s*([\w:-]+)((?:\s+[\w:-]+(?:\s*=\s*(?:(?:"[^"]*")|(?:'[^']*')|[^>\s]+))?)*)\s*(\/?)\s*>/,l=/^<\s*\/\s*([\w:-]+)[^>]*>/,c=/([\w:-]+)(?:\s*=\s*(?:(?:"((?:[^"])*)")|(?:'((?:[^'])*)')|([^>\s]+)))?/g,f=/^":">"))}function s(e){var t=o(e);-1!==(v.allowedTags||[]).indexOf(t)&&!1===h.ignoring?(n("")):p(t)}function l(e){function t(t){return 0===e.indexOf(t+":")}var n=e[0];if("#"===n||"/"===n)return!0;var r=e.indexOf(":");if(-1===r)return!0;var i=e.indexOf("?");if(-1!==i&&r>i)return!0;var o=e.indexOf("#");return-1!==o&&r>o||v.allowedSchemes.some(t)}function c(e){!1===h.ignoring&&n(v.transformText?v.transformText(e):e)}function f(e){u.voids[e]||(!1===h.ignoring?h={ignoring:e,depth:1}:h.ignoring===e&&h.depth++)}function p(e){h.ignoring===e&&--h.depth<=0&&d()}function d(){h={ignoring:!1,depth:0}}var h,v=t||{};return d(),{start:r,end:s,chars:c}}var i=n(10),o=n(28),a=n(26),u=n(27);e.exports=r},function(e,t,n){"use strict";var r=Date.prototype.getDay,i=function(e){try{return r.call(e),!0}catch(e){return!1}},o=Object.prototype.toString,a=n(24)();e.exports=function(e){return"object"==typeof e&&null!==e&&(a?i(e):"[object Date]"===o.call(e))}},function(e,t,n){"use strict";var r=String.prototype.valueOf,i=function(e){try{return r.call(e),!0}catch(e){return!1}},o=Object.prototype.toString,a=n(24)();e.exports=function(e){return"string"==typeof e||"object"==typeof e&&(a?i(e):"[object String]"===o.call(e))}},function(e,t,n){"use strict";var r=Object.prototype.toString;if(n(22)()){var i=Symbol.prototype.toString,o=/^Symbol\(.*\)$/,a=function(e){return"symbol"==typeof e.valueOf()&&o.test(i.call(e))};e.exports=function(e){if("symbol"==typeof e)return!0;if("[object Symbol]"!==r.call(e))return!1;try{return a(e)}catch(e){return!1}}}else e.exports=function(e){return!1}},function(e,t,n){!function(e,n){n(t)}(0,function(e){"use strict";function t(e,t){for(var n=0;ne.length)&&(t=e.length);for(var n=0,r=new Array(t);n=e.length?{done:!0}:{done:!1,value:e[i++]}}}throw new TypeError("Invalid attempt to iterate non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.")}function a(){return{baseUrl:null,breaks:!1,extensions:null,gfm:!0,headerIds:!0,headerPrefix:"",highlight:null,langPrefix:"language-",mangle:!0,pedantic:!1,renderer:null,sanitize:!1,sanitizer:null,silent:!1,smartLists:!1,smartypants:!1,tokenizer:null,walkTokens:null,xhtml:!1}}function u(t){e.defaults=t}function s(e,t){if(t){if(k.test(e))return e.replace(E,_)}else if(F.test(e))return e.replace(C,_);return e}function l(e){return e.replace(O,function(e,t){return t=t.toLowerCase(),"colon"===t?":":"#"===t.charAt(0)?"x"===t.charAt(1)?String.fromCharCode(parseInt(t.substring(2),16)):String.fromCharCode(+t.substring(1)):""})}function c(e,t){e=e.source||e,t=t||"";var n={replace:function(t,r){return r=r.source||r,r=r.replace(B,"$1"),e=e.replace(t,r),n},getRegex:function(){return new RegExp(e,t)}};return n}function f(e,t,n){if(e){var r;try{r=decodeURIComponent(l(n)).replace(j,"").toLowerCase()}catch(e){return null}if(0===r.indexOf("javascript:")||0===r.indexOf("vbscript:")||0===r.indexOf("data:"))return null}t&&!$.test(n)&&(n=p(t,n));try{n=encodeURI(n).replace(/%25/g,"%")}catch(e){return null}return n}function p(e,t){T[" "+e]||(I.test(e)?T[" "+e]=e+"/":T[" "+e]=v(e,"/",!0)),e=T[" "+e];var n=-1===e.indexOf(":");return"//"===t.substring(0,2)?n?t:e.replace(z,"$1")+t:"/"===t.charAt(0)?n?t:e.replace(P,"$1")+t:e+t}function d(e){for(var t,n,r=1;r=0&&"\\"===n[i];)r=!r;return r?"|":" |"}),r=n.split(/ \|/),i=0;if(r[0].trim()||r.shift(),r.length>0&&!r[r.length-1].trim()&&r.pop(),r.length>t)r.splice(t);else for(;r.length1;)1&t&&(n+=e),t>>=1,e+=e;return n+e}function b(e,t,n,r){var i=t.href,o=t.title?s(t.title):null,a=e[1].replace(/\\([\[\]])/g,"$1");if("!"!==e[0].charAt(0)){r.state.inLink=!0;var u={type:"link",raw:n,href:i,title:o,text:a,tokens:r.inlineTokens(a,[])};return r.state.inLink=!1,u}return{type:"image",raw:n,href:i,title:o,text:s(a)}}function D(e,t){var n=e.match(/^(\s+)(?:```)/);if(null===n)return t;var r=n[1];return t.split("\n").map(function(e){var t=e.match(/^\s+/);return null===t?e:t[0].length>=r.length?e.slice(r.length):e}).join("\n")}function x(e){return e.replace(/---/g,"—").replace(/--/g,"–").replace(/(^|[-\u2014/(\[{"\s])'/g,"$1‘").replace(/'/g,"’").replace(/(^|[-\u2014/(\[{\u2018\s])"/g,"$1“").replace(/"/g,"”").replace(/\.{3}/g,"…")}function w(e){var t,n,r="",i=e.length;for(t=0;t.5&&(n="x"+n.toString(16)),r+="&#"+n+";";return r}function A(e,t,n){if(void 0===e||null===e)throw new Error("marked(): input parameter is undefined or null");if("string"!=typeof e)throw new Error("marked(): input parameter is of type "+Object.prototype.toString.call(e)+", string expected");if("function"==typeof t&&(n=t,t=null),t=d({},A.defaults,t||{}),m(t),n){var r,i=t.highlight;try{r=U.lex(e,t)}catch(e){return n(e)}var o=function(e){var o;if(!e)try{t.walkTokens&&A.walkTokens(r,t.walkTokens),o=V.parse(r,t)}catch(t){e=t}return t.highlight=i,e?n(e):n(null,o)};if(!i||i.length<3)return o();if(delete t.highlight,!r.length)return o();var a=0;return A.walkTokens(r,function(e){"code"===e.type&&(a++,setTimeout(function(){i(e.text,e.lang,function(t,n){if(t)return o(t);null!=n&&n!==e.text&&(e.text=n,e.escaped=!0),0===--a&&o()})},0))}),void(0===a&&o())}try{var u=U.lex(e,t);return t.walkTokens&&A.walkTokens(u,t.walkTokens),V.parse(u,t)}catch(e){if(e.message+="\nPlease report this to https://github.com/markedjs/marked.",t.silent)return"

An error occurred:

"+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;u1)return!0}return!1});!m.loose&&x.length&&w&&(m.loose=!0,m.items[u].loose=!0)}return m}},n.html=function(e){var t=this.rules.block.html.exec(e);if(t){var n={type:"html",raw:t[0],pre:!this.options.sanitizer&&("pre"===t[1]||"script"===t[1]||"style"===t[1]),text:t[0]};return this.options.sanitize&&(n.type="paragraph",n.text=this.options.sanitizer?this.options.sanitizer(t[0]):s(t[0]),n.tokens=[],this.lexer.inline(n.text,n.tokens)),n}},n.def=function(e){var t=this.rules.block.def.exec(e);if(t){t[3]&&(t[3]=t[3].substring(1,t[3].length-1));return{type:"def",tag:t[1].toLowerCase().replace(/\s+/g," "),raw:t[0],href:t[2],title:t[3]}}},n.table=function(e){var t=this.rules.block.table.exec(e);if(t){var n={type:"table",header:h(t[1]).map(function(e){return{text:e}}),align:t[2].replace(/^ *|\| *$/g,"").split(/ *\| */),rows:t[3]&&t[3].trim()?t[3].replace(/\n[ \t]*$/,"").split("\n"):[]};if(n.header.length===n.align.length){n.raw=t[0];var r,i,o,a,u=n.align.length;for(r=0;r/i.test(t[0])&&(this.lexer.state.inLink=!1),!this.lexer.state.inRawBlock&&/^<(pre|code|kbd|script)(\s|>)/i.test(t[0])?this.lexer.state.inRawBlock=!0:this.lexer.state.inRawBlock&&/^<\/(pre|code|kbd|script)(\s|>)/i.test(t[0])&&(this.lexer.state.inRawBlock=!1),{type:this.options.sanitize?"text":"html",raw:t[0],inLink:this.lexer.state.inLink,inRawBlock:this.lexer.state.inRawBlock,text:this.options.sanitize?this.options.sanitizer?this.options.sanitizer(t[0]):s(t[0]):t[0]}},n.link=function(e){var t=this.rules.inline.link.exec(e);if(t){var n=t[2].trim();if(!this.options.pedantic&&/^$/.test(n))return;var r=v(n.slice(0,-1),"\\");if((n.length-r.length)%2==0)return}else{var i=g(t[2],"()");if(i>-1){var o=0===t[0].indexOf("!")?5:4,a=o+t[1].length+i;t[2]=t[2].substring(0,i),t[0]=t[0].substring(0,a).trim(),t[3]=""}}var u=t[2],s="";if(this.options.pedantic){var l=/^([^'"]*[^\s])\s+(['"])(.*)\2/.exec(u);l&&(u=l[1],s=l[3])}else s=t[3]?t[3].slice(1,-1):"";return u=u.trim(),/^$/.test(n)?u.slice(1):u.slice(1,-1)),b(t,{href:u?u.replace(this.rules.inline._escapes,"$1"):u,title:s?s.replace(this.rules.inline._escapes,"$1"):s},t[0],this.lexer)}},n.reflink=function(e,t){var n;if((n=this.rules.inline.reflink.exec(e))||(n=this.rules.inline.nolink.exec(e))){var r=(n[2]||n[1]).replace(/\s+/g," ");if(!(r=t[r.toLowerCase()])||!r.href){var i=n[0].charAt(0);return{type:"text",raw:i,text:i}}return b(n,r,n[0],this.lexer)}},n.emStrong=function(e,t,n){void 0===n&&(n="");var r=this.rules.inline.emStrong.lDelim.exec(e);if(r&&(!r[3]||!n.match(/(?:[0-9A-Za-z\xAA\xB2\xB3\xB5\xB9\xBA\xBC-\xBE\xC0-\xD6\xD8-\xF6\xF8-\u02C1\u02C6-\u02D1\u02E0-\u02E4\u02EC\u02EE\u0370-\u0374\u0376\u0377\u037A-\u037D\u037F\u0386\u0388-\u038A\u038C\u038E-\u03A1\u03A3-\u03F5\u03F7-\u0481\u048A-\u052F\u0531-\u0556\u0559\u0560-\u0588\u05D0-\u05EA\u05EF-\u05F2\u0620-\u064A\u0660-\u0669\u066E\u066F\u0671-\u06D3\u06D5\u06E5\u06E6\u06EE-\u06FC\u06FF\u0710\u0712-\u072F\u074D-\u07A5\u07B1\u07C0-\u07EA\u07F4\u07F5\u07FA\u0800-\u0815\u081A\u0824\u0828\u0840-\u0858\u0860-\u086A\u0870-\u0887\u0889-\u088E\u08A0-\u08C9\u0904-\u0939\u093D\u0950\u0958-\u0961\u0966-\u096F\u0971-\u0980\u0985-\u098C\u098F\u0990\u0993-\u09A8\u09AA-\u09B0\u09B2\u09B6-\u09B9\u09BD\u09CE\u09DC\u09DD\u09DF-\u09E1\u09E6-\u09F1\u09F4-\u09F9\u09FC\u0A05-\u0A0A\u0A0F\u0A10\u0A13-\u0A28\u0A2A-\u0A30\u0A32\u0A33\u0A35\u0A36\u0A38\u0A39\u0A59-\u0A5C\u0A5E\u0A66-\u0A6F\u0A72-\u0A74\u0A85-\u0A8D\u0A8F-\u0A91\u0A93-\u0AA8\u0AAA-\u0AB0\u0AB2\u0AB3\u0AB5-\u0AB9\u0ABD\u0AD0\u0AE0\u0AE1\u0AE6-\u0AEF\u0AF9\u0B05-\u0B0C\u0B0F\u0B10\u0B13-\u0B28\u0B2A-\u0B30\u0B32\u0B33\u0B35-\u0B39\u0B3D\u0B5C\u0B5D\u0B5F-\u0B61\u0B66-\u0B6F\u0B71-\u0B77\u0B83\u0B85-\u0B8A\u0B8E-\u0B90\u0B92-\u0B95\u0B99\u0B9A\u0B9C\u0B9E\u0B9F\u0BA3\u0BA4\u0BA8-\u0BAA\u0BAE-\u0BB9\u0BD0\u0BE6-\u0BF2\u0C05-\u0C0C\u0C0E-\u0C10\u0C12-\u0C28\u0C2A-\u0C39\u0C3D\u0C58-\u0C5A\u0C5D\u0C60\u0C61\u0C66-\u0C6F\u0C78-\u0C7E\u0C80\u0C85-\u0C8C\u0C8E-\u0C90\u0C92-\u0CA8\u0CAA-\u0CB3\u0CB5-\u0CB9\u0CBD\u0CDD\u0CDE\u0CE0\u0CE1\u0CE6-\u0CEF\u0CF1\u0CF2\u0D04-\u0D0C\u0D0E-\u0D10\u0D12-\u0D3A\u0D3D\u0D4E\u0D54-\u0D56\u0D58-\u0D61\u0D66-\u0D78\u0D7A-\u0D7F\u0D85-\u0D96\u0D9A-\u0DB1\u0DB3-\u0DBB\u0DBD\u0DC0-\u0DC6\u0DE6-\u0DEF\u0E01-\u0E30\u0E32\u0E33\u0E40-\u0E46\u0E50-\u0E59\u0E81\u0E82\u0E84\u0E86-\u0E8A\u0E8C-\u0EA3\u0EA5\u0EA7-\u0EB0\u0EB2\u0EB3\u0EBD\u0EC0-\u0EC4\u0EC6\u0ED0-\u0ED9\u0EDC-\u0EDF\u0F00\u0F20-\u0F33\u0F40-\u0F47\u0F49-\u0F6C\u0F88-\u0F8C\u1000-\u102A\u103F-\u1049\u1050-\u1055\u105A-\u105D\u1061\u1065\u1066\u106E-\u1070\u1075-\u1081\u108E\u1090-\u1099\u10A0-\u10C5\u10C7\u10CD\u10D0-\u10FA\u10FC-\u1248\u124A-\u124D\u1250-\u1256\u1258\u125A-\u125D\u1260-\u1288\u128A-\u128D\u1290-\u12B0\u12B2-\u12B5\u12B8-\u12BE\u12C0\u12C2-\u12C5\u12C8-\u12D6\u12D8-\u1310\u1312-\u1315\u1318-\u135A\u1369-\u137C\u1380-\u138F\u13A0-\u13F5\u13F8-\u13FD\u1401-\u166C\u166F-\u167F\u1681-\u169A\u16A0-\u16EA\u16EE-\u16F8\u1700-\u1711\u171F-\u1731\u1740-\u1751\u1760-\u176C\u176E-\u1770\u1780-\u17B3\u17D7\u17DC\u17E0-\u17E9\u17F0-\u17F9\u1810-\u1819\u1820-\u1878\u1880-\u1884\u1887-\u18A8\u18AA\u18B0-\u18F5\u1900-\u191E\u1946-\u196D\u1970-\u1974\u1980-\u19AB\u19B0-\u19C9\u19D0-\u19DA\u1A00-\u1A16\u1A20-\u1A54\u1A80-\u1A89\u1A90-\u1A99\u1AA7\u1B05-\u1B33\u1B45-\u1B4C\u1B50-\u1B59\u1B83-\u1BA0\u1BAE-\u1BE5\u1C00-\u1C23\u1C40-\u1C49\u1C4D-\u1C7D\u1C80-\u1C88\u1C90-\u1CBA\u1CBD-\u1CBF\u1CE9-\u1CEC\u1CEE-\u1CF3\u1CF5\u1CF6\u1CFA\u1D00-\u1DBF\u1E00-\u1F15\u1F18-\u1F1D\u1F20-\u1F45\u1F48-\u1F4D\u1F50-\u1F57\u1F59\u1F5B\u1F5D\u1F5F-\u1F7D\u1F80-\u1FB4\u1FB6-\u1FBC\u1FBE\u1FC2-\u1FC4\u1FC6-\u1FCC\u1FD0-\u1FD3\u1FD6-\u1FDB\u1FE0-\u1FEC\u1FF2-\u1FF4\u1FF6-\u1FFC\u2070\u2071\u2074-\u2079\u207F-\u2089\u2090-\u209C\u2102\u2107\u210A-\u2113\u2115\u2119-\u211D\u2124\u2126\u2128\u212A-\u212D\u212F-\u2139\u213C-\u213F\u2145-\u2149\u214E\u2150-\u2189\u2460-\u249B\u24EA-\u24FF\u2776-\u2793\u2C00-\u2CE4\u2CEB-\u2CEE\u2CF2\u2CF3\u2CFD\u2D00-\u2D25\u2D27\u2D2D\u2D30-\u2D67\u2D6F\u2D80-\u2D96\u2DA0-\u2DA6\u2DA8-\u2DAE\u2DB0-\u2DB6\u2DB8-\u2DBE\u2DC0-\u2DC6\u2DC8-\u2DCE\u2DD0-\u2DD6\u2DD8-\u2DDE\u2E2F\u3005-\u3007\u3021-\u3029\u3031-\u3035\u3038-\u303C\u3041-\u3096\u309D-\u309F\u30A1-\u30FA\u30FC-\u30FF\u3105-\u312F\u3131-\u318E\u3192-\u3195\u31A0-\u31BF\u31F0-\u31FF\u3220-\u3229\u3248-\u324F\u3251-\u325F\u3280-\u3289\u32B1-\u32BF\u3400-\u4DBF\u4E00-\uA48C\uA4D0-\uA4FD\uA500-\uA60C\uA610-\uA62B\uA640-\uA66E\uA67F-\uA69D\uA6A0-\uA6EF\uA717-\uA71F\uA722-\uA788\uA78B-\uA7CA\uA7D0\uA7D1\uA7D3\uA7D5-\uA7D9\uA7F2-\uA801\uA803-\uA805\uA807-\uA80A\uA80C-\uA822\uA830-\uA835\uA840-\uA873\uA882-\uA8B3\uA8D0-\uA8D9\uA8F2-\uA8F7\uA8FB\uA8FD\uA8FE\uA900-\uA925\uA930-\uA946\uA960-\uA97C\uA984-\uA9B2\uA9CF-\uA9D9\uA9E0-\uA9E4\uA9E6-\uA9FE\uAA00-\uAA28\uAA40-\uAA42\uAA44-\uAA4B\uAA50-\uAA59\uAA60-\uAA76\uAA7A\uAA7E-\uAAAF\uAAB1\uAAB5\uAAB6\uAAB9-\uAABD\uAAC0\uAAC2\uAADB-\uAADD\uAAE0-\uAAEA\uAAF2-\uAAF4\uAB01-\uAB06\uAB09-\uAB0E\uAB11-\uAB16\uAB20-\uAB26\uAB28-\uAB2E\uAB30-\uAB5A\uAB5C-\uAB69\uAB70-\uABE2\uABF0-\uABF9\uAC00-\uD7A3\uD7B0-\uD7C6\uD7CB-\uD7FB\uF900-\uFA6D\uFA70-\uFAD9\uFB00-\uFB06\uFB13-\uFB17\uFB1D\uFB1F-\uFB28\uFB2A-\uFB36\uFB38-\uFB3C\uFB3E\uFB40\uFB41\uFB43\uFB44\uFB46-\uFBB1\uFBD3-\uFD3D\uFD50-\uFD8F\uFD92-\uFDC7\uFDF0-\uFDFB\uFE70-\uFE74\uFE76-\uFEFC\uFF10-\uFF19\uFF21-\uFF3A\uFF41-\uFF5A\uFF66-\uFFBE\uFFC2-\uFFC7\uFFCA-\uFFCF\uFFD2-\uFFD7\uFFDA-\uFFDC]|\uD800[\uDC00-\uDC0B\uDC0D-\uDC26\uDC28-\uDC3A\uDC3C\uDC3D\uDC3F-\uDC4D\uDC50-\uDC5D\uDC80-\uDCFA\uDD07-\uDD33\uDD40-\uDD78\uDD8A\uDD8B\uDE80-\uDE9C\uDEA0-\uDED0\uDEE1-\uDEFB\uDF00-\uDF23\uDF2D-\uDF4A\uDF50-\uDF75\uDF80-\uDF9D\uDFA0-\uDFC3\uDFC8-\uDFCF\uDFD1-\uDFD5]|\uD801[\uDC00-\uDC9D\uDCA0-\uDCA9\uDCB0-\uDCD3\uDCD8-\uDCFB\uDD00-\uDD27\uDD30-\uDD63\uDD70-\uDD7A\uDD7C-\uDD8A\uDD8C-\uDD92\uDD94\uDD95\uDD97-\uDDA1\uDDA3-\uDDB1\uDDB3-\uDDB9\uDDBB\uDDBC\uDE00-\uDF36\uDF40-\uDF55\uDF60-\uDF67\uDF80-\uDF85\uDF87-\uDFB0\uDFB2-\uDFBA]|\uD802[\uDC00-\uDC05\uDC08\uDC0A-\uDC35\uDC37\uDC38\uDC3C\uDC3F-\uDC55\uDC58-\uDC76\uDC79-\uDC9E\uDCA7-\uDCAF\uDCE0-\uDCF2\uDCF4\uDCF5\uDCFB-\uDD1B\uDD20-\uDD39\uDD80-\uDDB7\uDDBC-\uDDCF\uDDD2-\uDE00\uDE10-\uDE13\uDE15-\uDE17\uDE19-\uDE35\uDE40-\uDE48\uDE60-\uDE7E\uDE80-\uDE9F\uDEC0-\uDEC7\uDEC9-\uDEE4\uDEEB-\uDEEF\uDF00-\uDF35\uDF40-\uDF55\uDF58-\uDF72\uDF78-\uDF91\uDFA9-\uDFAF]|\uD803[\uDC00-\uDC48\uDC80-\uDCB2\uDCC0-\uDCF2\uDCFA-\uDD23\uDD30-\uDD39\uDE60-\uDE7E\uDE80-\uDEA9\uDEB0\uDEB1\uDF00-\uDF27\uDF30-\uDF45\uDF51-\uDF54\uDF70-\uDF81\uDFB0-\uDFCB\uDFE0-\uDFF6]|\uD804[\uDC03-\uDC37\uDC52-\uDC6F\uDC71\uDC72\uDC75\uDC83-\uDCAF\uDCD0-\uDCE8\uDCF0-\uDCF9\uDD03-\uDD26\uDD36-\uDD3F\uDD44\uDD47\uDD50-\uDD72\uDD76\uDD83-\uDDB2\uDDC1-\uDDC4\uDDD0-\uDDDA\uDDDC\uDDE1-\uDDF4\uDE00-\uDE11\uDE13-\uDE2B\uDE80-\uDE86\uDE88\uDE8A-\uDE8D\uDE8F-\uDE9D\uDE9F-\uDEA8\uDEB0-\uDEDE\uDEF0-\uDEF9\uDF05-\uDF0C\uDF0F\uDF10\uDF13-\uDF28\uDF2A-\uDF30\uDF32\uDF33\uDF35-\uDF39\uDF3D\uDF50\uDF5D-\uDF61]|\uD805[\uDC00-\uDC34\uDC47-\uDC4A\uDC50-\uDC59\uDC5F-\uDC61\uDC80-\uDCAF\uDCC4\uDCC5\uDCC7\uDCD0-\uDCD9\uDD80-\uDDAE\uDDD8-\uDDDB\uDE00-\uDE2F\uDE44\uDE50-\uDE59\uDE80-\uDEAA\uDEB8\uDEC0-\uDEC9\uDF00-\uDF1A\uDF30-\uDF3B\uDF40-\uDF46]|\uD806[\uDC00-\uDC2B\uDCA0-\uDCF2\uDCFF-\uDD06\uDD09\uDD0C-\uDD13\uDD15\uDD16\uDD18-\uDD2F\uDD3F\uDD41\uDD50-\uDD59\uDDA0-\uDDA7\uDDAA-\uDDD0\uDDE1\uDDE3\uDE00\uDE0B-\uDE32\uDE3A\uDE50\uDE5C-\uDE89\uDE9D\uDEB0-\uDEF8]|\uD807[\uDC00-\uDC08\uDC0A-\uDC2E\uDC40\uDC50-\uDC6C\uDC72-\uDC8F\uDD00-\uDD06\uDD08\uDD09\uDD0B-\uDD30\uDD46\uDD50-\uDD59\uDD60-\uDD65\uDD67\uDD68\uDD6A-\uDD89\uDD98\uDDA0-\uDDA9\uDEE0-\uDEF2\uDFB0\uDFC0-\uDFD4]|\uD808[\uDC00-\uDF99]|\uD809[\uDC00-\uDC6E\uDC80-\uDD43]|\uD80B[\uDF90-\uDFF0]|[\uD80C\uD81C-\uD820\uD822\uD840-\uD868\uD86A-\uD86C\uD86F-\uD872\uD874-\uD879\uD880-\uD883][\uDC00-\uDFFF]|\uD80D[\uDC00-\uDC2E]|\uD811[\uDC00-\uDE46]|\uD81A[\uDC00-\uDE38\uDE40-\uDE5E\uDE60-\uDE69\uDE70-\uDEBE\uDEC0-\uDEC9\uDED0-\uDEED\uDF00-\uDF2F\uDF40-\uDF43\uDF50-\uDF59\uDF5B-\uDF61\uDF63-\uDF77\uDF7D-\uDF8F]|\uD81B[\uDE40-\uDE96\uDF00-\uDF4A\uDF50\uDF93-\uDF9F\uDFE0\uDFE1\uDFE3]|\uD821[\uDC00-\uDFF7]|\uD823[\uDC00-\uDCD5\uDD00-\uDD08]|\uD82B[\uDFF0-\uDFF3\uDFF5-\uDFFB\uDFFD\uDFFE]|\uD82C[\uDC00-\uDD22\uDD50-\uDD52\uDD64-\uDD67\uDD70-\uDEFB]|\uD82F[\uDC00-\uDC6A\uDC70-\uDC7C\uDC80-\uDC88\uDC90-\uDC99]|\uD834[\uDEE0-\uDEF3\uDF60-\uDF78]|\uD835[\uDC00-\uDC54\uDC56-\uDC9C\uDC9E\uDC9F\uDCA2\uDCA5\uDCA6\uDCA9-\uDCAC\uDCAE-\uDCB9\uDCBB\uDCBD-\uDCC3\uDCC5-\uDD05\uDD07-\uDD0A\uDD0D-\uDD14\uDD16-\uDD1C\uDD1E-\uDD39\uDD3B-\uDD3E\uDD40-\uDD44\uDD46\uDD4A-\uDD50\uDD52-\uDEA5\uDEA8-\uDEC0\uDEC2-\uDEDA\uDEDC-\uDEFA\uDEFC-\uDF14\uDF16-\uDF34\uDF36-\uDF4E\uDF50-\uDF6E\uDF70-\uDF88\uDF8A-\uDFA8\uDFAA-\uDFC2\uDFC4-\uDFCB\uDFCE-\uDFFF]|\uD837[\uDF00-\uDF1E]|\uD838[\uDD00-\uDD2C\uDD37-\uDD3D\uDD40-\uDD49\uDD4E\uDE90-\uDEAD\uDEC0-\uDEEB\uDEF0-\uDEF9]|\uD839[\uDFE0-\uDFE6\uDFE8-\uDFEB\uDFED\uDFEE\uDFF0-\uDFFE]|\uD83A[\uDC00-\uDCC4\uDCC7-\uDCCF\uDD00-\uDD43\uDD4B\uDD50-\uDD59]|\uD83B[\uDC71-\uDCAB\uDCAD-\uDCAF\uDCB1-\uDCB4\uDD01-\uDD2D\uDD2F-\uDD3D\uDE00-\uDE03\uDE05-\uDE1F\uDE21\uDE22\uDE24\uDE27\uDE29-\uDE32\uDE34-\uDE37\uDE39\uDE3B\uDE42\uDE47\uDE49\uDE4B\uDE4D-\uDE4F\uDE51\uDE52\uDE54\uDE57\uDE59\uDE5B\uDE5D\uDE5F\uDE61\uDE62\uDE64\uDE67-\uDE6A\uDE6C-\uDE72\uDE74-\uDE77\uDE79-\uDE7C\uDE7E\uDE80-\uDE89\uDE8B-\uDE9B\uDEA1-\uDEA3\uDEA5-\uDEA9\uDEAB-\uDEBB]|\uD83C[\uDD00-\uDD0C]|\uD83E[\uDFF0-\uDFF9]|\uD869[\uDC00-\uDEDF\uDF00-\uDFFF]|\uD86D[\uDC00-\uDF38\uDF40-\uDFFF]|\uD86E[\uDC00-\uDC1D\uDC20-\uDFFF]|\uD873[\uDC00-\uDEA1\uDEB0-\uDFFF]|\uD87A[\uDC00-\uDFE0]|\uD87E[\uDC00-\uDE1D]|\uD884[\uDC00-\uDF4A])/))){var i=r[1]||r[2]||"";if(!i||i&&(""===n||this.rules.inline.punctuation.exec(n))){var o,a,u=r[0].length-1,s=u,l=0,c="*"===r[0][0]?this.rules.inline.emStrong.rDelimAst:this.rules.inline.emStrong.rDelimUnd;for(c.lastIndex=0,t=t.slice(-1*e.length+u);null!=(r=c.exec(t));)if(o=r[1]||r[2]||r[3]||r[4]||r[5]||r[6])if(a=o.length,r[3]||r[4])s+=a;else if(!((r[5]||r[6])&&u%3)||(u+a)%3){if(!((s-=a)>0)){if(a=Math.min(a,a+s+l),Math.min(u,a)%2){var f=e.slice(1,u+r.index+a);return{type:"em",raw:e.slice(0,u+r.index+a+1),text:f,tokens:this.lexer.inlineTokens(f,[])}}var p=e.slice(2,u+r.index+a-1);return{type:"strong",raw:e.slice(0,u+r.index+a+1),text:p,tokens:this.lexer.inlineTokens(p,[])}}}else l+=a}}},n.codespan=function(e){var t=this.rules.inline.code.exec(e);if(t){var n=t[2].replace(/\n/g," "),r=/[^ ]/.test(n),i=/^ /.test(n)&&/ $/.test(n);return r&&i&&(n=n.substring(1,n.length-1)),n=s(n,!0),{type:"codespan",raw:t[0],text:n}}},n.br=function(e){var t=this.rules.inline.br.exec(e);if(t)return{type:"br",raw:t[0]}},n.del=function(e){var t=this.rules.inline.del.exec(e);if(t)return{type:"del",raw:t[0],text:t[2],tokens:this.lexer.inlineTokens(t[2],[])}},n.autolink=function(e,t){var n=this.rules.inline.autolink.exec(e);if(n){var r,i;return"@"===n[2]?(r=s(this.options.mangle?t(n[1]):n[1]),i="mailto:"+r):(r=s(n[1]),i=r),{type:"link",raw:n[0],text:r,href:i,tokens:[{type:"text",raw:r,text:r}]}}},n.url=function(e,t){var n;if(n=this.rules.inline.url.exec(e)){var r,i;if("@"===n[2])r=s(this.options.mangle?t(n[0]):n[0]),i="mailto:"+r;else{var o;do{o=n[0],n[0]=this.rules.inline._backpedal.exec(n[0])[0]}while(o!==n[0]);r=s(n[0]),i="www."===n[1]?"http://"+r:r}return{type:"link",raw:n[0],text:r,href:i,tokens:[{type:"text",raw:r,text:r}]}}},n.inlineText=function(e,t){var n=this.rules.inline.text.exec(e);if(n){var r;return r=this.lexer.state.inRawBlock?this.options.sanitize?this.options.sanitizer?this.options.sanitizer(n[0]):s(n[0]):n[0]:s(this.options.smartypants?t(n[0]):n[0]),{type:"text",raw:n[0],text:r}}},t}(),L={newline:/^(?: *(?:\n|$))+/,code:/^( {4}[^\n]+(?:\n(?: *(?:\n|$))*)?)+/,fences:/^ {0,3}(`{3,}(?=[^`\n]*\n)|~{3,})([^\n]*)\n(?:|([\s\S]*?)\n)(?: {0,3}\1[~`]* *(?=\n|$)|$)/,hr:/^ {0,3}((?:- *){3,}|(?:_ *){3,}|(?:\* *){3,})(?:\n+|$)/,heading:/^ {0,3}(#{1,6})(?=\s|$)(.*)(?:\n+|$)/,blockquote:/^( {0,3}> ?(paragraph|[^\n]*)(?:\n|$))+/,list:/^( {0,3}bull)( [^\n]+?)?(?:\n|$)/,html:"^ {0,3}(?:<(script|pre|style|textarea)[\\s>][\\s\\S]*?(?:[^\\n]*\\n+|$)|comment[^\\n]*(\\n+|$)|<\\?[\\s\\S]*?(?:\\?>\\n*|$)|\\n*|$)|\\n*|$)|)[\\s\\S]*?(?:(?:\\n *)+\\n|$)|<(?!script|pre|style|textarea)([a-z][\\w-]*)(?:attribute)*? */?>(?=[ \\t]*(?:\\n|$))[\\s\\S]*?(?:(?:\\n *)+\\n|$)|(?=[ \\t]*(?:\\n|$))[\\s\\S]*?(?:(?:\\n *)+\\n|$))",def:/^ {0,3}\[(label)\]: *(?:\n *)?]+)>?(?:(?: +(?:\n *)?| *\n *)(title))? *(?:\n+|$)/,table:R,lheading:/^([^\n]+)\n {0,3}(=+|-+) *(?:\n+|$)/,_paragraph:/^([^\n]+(?:\n(?!hr|heading|lheading|blockquote|fences|list|html|table| +\n)[^\n]+)*)/,text:/^[^\n]+/};L._label=/(?!\s*\])(?:\\.|[^\[\]\\])+/,L._title=/(?:"(?:\\"?|[^"\\])*"|'[^'\n]*(?:\n[^'\n]+)*\n?'|\([^()]*\))/,L.def=c(L.def).replace("label",L._label).replace("title",L._title).getRegex(),L.bullet=/(?:[*+-]|\d{1,9}[.)])/,L.listItemStart=c(/^( *)(bull) */).replace("bull",L.bullet).getRegex(),L.list=c(L.list).replace(/bull/g,L.bullet).replace("hr","\\n+(?=\\1?(?:(?:- *){3,}|(?:_ *){3,}|(?:\\* *){3,})(?:\\n+|$))").replace("def","\\n+(?="+L.def.source+")").getRegex(),L._tag="address|article|aside|base|basefont|blockquote|body|caption|center|col|colgroup|dd|details|dialog|dir|div|dl|dt|fieldset|figcaption|figure|footer|form|frame|frameset|h[1-6]|head|header|hr|html|iframe|legend|li|link|main|menu|menuitem|meta|nav|noframes|ol|optgroup|option|p|param|section|source|summary|table|tbody|td|tfoot|th|thead|title|tr|track|ul",L._comment=/|$)/,L.html=c(L.html,"i").replace("comment",L._comment).replace("tag",L._tag).replace("attribute",/ +[a-zA-Z:_][\w.:-]*(?: *= *"[^"\n]*"| *= *'[^'\n]*'| *= *[^\s"'=<>`]+)?/).getRegex(),L.paragraph=c(L._paragraph).replace("hr",L.hr).replace("heading"," {0,3}#{1,6} ").replace("|lheading","").replace("|table","").replace("blockquote"," {0,3}>").replace("fences"," {0,3}(?:`{3,}(?=[^`\\n]*\\n)|~{3,})[^\\n]*\\n").replace("list"," {0,3}(?:[*+-]|1[.)]) ").replace("html",")|<(?:script|pre|style|textarea|!--)").replace("tag",L._tag).getRegex(),L.blockquote=c(L.blockquote).replace("paragraph",L.paragraph).getRegex(),L.normal=d({},L),L.gfm=d({},L.normal,{table:"^ *([^\\n ].*\\|.*)\\n {0,3}(?:\\| *)?(:?-+:? *(?:\\| *:?-+:? *)*)(?:\\| *)?(?:\\n((?:(?! *\\n|hr|heading|blockquote|code|fences|list|html).*(?:\\n|$))*)\\n*|$)"}),L.gfm.table=c(L.gfm.table).replace("hr",L.hr).replace("heading"," {0,3}#{1,6} ").replace("blockquote"," {0,3}>").replace("code"," {4}[^\\n]").replace("fences"," {0,3}(?:`{3,}(?=[^`\\n]*\\n)|~{3,})[^\\n]*\\n").replace("list"," {0,3}(?:[*+-]|1[.)]) ").replace("html",")|<(?:script|pre|style|textarea|!--)").replace("tag",L._tag).getRegex(),L.gfm.paragraph=c(L._paragraph).replace("hr",L.hr).replace("heading"," {0,3}#{1,6} ").replace("|lheading","").replace("table",L.gfm.table).replace("blockquote"," {0,3}>").replace("fences"," {0,3}(?:`{3,}(?=[^`\\n]*\\n)|~{3,})[^\\n]*\\n").replace("list"," {0,3}(?:[*+-]|1[.)]) ").replace("html",")|<(?:script|pre|style|textarea|!--)").replace("tag",L._tag).getRegex(),L.pedantic=d({},L.normal,{html:c("^ *(?:comment *(?:\\n|\\s*$)|<(tag)[\\s\\S]+? *(?:\\n{2,}|\\s*$)|\\s]*)*?/?> *(?:\\n{2,}|\\s*$))").replace("comment",L._comment).replace(/tag/g,"(?!(?:a|em|strong|small|s|cite|q|dfn|abbr|data|time|code|var|samp|kbd|sub|sup|i|b|u|mark|ruby|rt|rp|bdi|bdo|span|br|wbr|ins|del|img)\\b)\\w+(?!:|[^\\w\\s@]*@)\\b").getRegex(),def:/^ *\[([^\]]+)\]: *]+)>?(?: +(["(][^\n]+[")]))? *(?:\n+|$)/,heading:/^(#{1,6})(.*)(?:\n+|$)/,fences:R,paragraph:c(L.normal._paragraph).replace("hr",L.hr).replace("heading"," *#{1,6} *[^\n]").replace("lheading",L.lheading).replace("blockquote"," {0,3}>").replace("|fences","").replace("|list","").replace("|html","").getRegex()});var N={escape:/^\\([!"#$%&'()*+,\-./:;<=>?@\[\]\\^_`{|}~])/,autolink:/^<(scheme:[^\s\x00-\x1f<>]*|email)>/,url:R,tag:"^comment|^|^<[a-zA-Z][\\w-]*(?:attribute)*?\\s*/?>|^<\\?[\\s\\S]*?\\?>|^|^",link:/^!?\[(label)\]\(\s*(href)(?:\s+(title))?\s*\)/,reflink:/^!?\[(label)\]\[(ref)\]/,nolink:/^!?\[(ref)\](?:\[\])?/,reflinkSearch:"reflink|nolink(?!\\()",emStrong:{lDelim:/^(?:\*+(?:([punct_])|[^\s*]))|^_+(?:([punct*])|([^\s_]))/,rDelimAst:/^[^_*]*?\_\_[^_*]*?\*[^_*]*?(?=\_\_)|[punct_](\*+)(?=[\s]|$)|[^punct*_\s](\*+)(?=[punct_\s]|$)|[punct_\s](\*+)(?=[^punct*_\s])|[\s](\*+)(?=[punct_])|[punct_](\*+)(?=[punct_])|[^punct*_\s](\*+)(?=[^punct*_\s])/,rDelimUnd:/^[^_*]*?\*\*[^_*]*?\_[^_*]*?(?=\*\*)|[punct*](\_+)(?=[\s]|$)|[^punct*_\s](\_+)(?=[punct*\s]|$)|[punct*\s](\_+)(?=[^punct*_\s])|[\s](\_+)(?=[punct*])|[punct*](\_+)(?=[punct*])/},code:/^(`+)([^`]|[^`][\s\S]*?[^`])\1(?!`)/,br:/^( {2,}|\\)\n(?!\s*$)/,del:R,text:/^(`+|[^`])(?:(?= {2,}\n)|[\s\S]*?(?:(?=[\\?@\\[\\]`^{|}~",N.punctuation=c(N.punctuation).replace(/punctuation/g,N._punctuation).getRegex(),N.blockSkip=/\[[^\]]*?\]\([^\)]*?\)|`[^`]*?`|<[^>]*?>/g,N.escapedEmSt=/\\\*|\\_/g,N._comment=c(L._comment).replace("(?:--\x3e|$)","--\x3e").getRegex(),N.emStrong.lDelim=c(N.emStrong.lDelim).replace(/punct/g,N._punctuation).getRegex(),N.emStrong.rDelimAst=c(N.emStrong.rDelimAst,"g").replace(/punct/g,N._punctuation).getRegex(),N.emStrong.rDelimUnd=c(N.emStrong.rDelimUnd,"g").replace(/punct/g,N._punctuation).getRegex(),N._escapes=/\\([!"#$%&'()*+,\-./:;<=>?@\[\]\\^_`{|}~])/g,N._scheme=/[a-zA-Z][a-zA-Z0-9+.-]{1,31}/,N._email=/[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+(@)[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)+(?![-_])/,N.autolink=c(N.autolink).replace("scheme",N._scheme).replace("email",N._email).getRegex(),N._attribute=/\s+[a-zA-Z:_][\w.:-]*(?:\s*=\s*"[^"]*"|\s*=\s*'[^']*'|\s*=\s*[^\s"'=<>`]+)?/,N.tag=c(N.tag).replace("comment",N._comment).replace("attribute",N._attribute).getRegex(),N._label=/(?:\[(?:\\.|[^\[\]\\])*\]|\\.|`[^`]*`|[^\[\]\\`])*?/,N._href=/<(?:\\.|[^\n<>\\])+>|[^\s\x00-\x1f]*/,N._title=/"(?:\\"?|[^"\\])*"|'(?:\\'?|[^'\\])*'|\((?:\\\)?|[^)\\])*\)/,N.link=c(N.link).replace("label",N._label).replace("href",N._href).replace("title",N._title).getRegex(),N.reflink=c(N.reflink).replace("label",N._label).replace("ref",L._label).getRegex(),N.nolink=c(N.nolink).replace("ref",L._label).getRegex(),N.reflinkSearch=c(N.reflinkSearch,"g").replace("reflink",N.reflink).replace("nolink",N.nolink).getRegex(),N.normal=d({},N),N.pedantic=d({},N.normal,{strong:{start:/^__|\*\*/,middle:/^__(?=\S)([\s\S]*?\S)__(?!_)|^\*\*(?=\S)([\s\S]*?\S)\*\*(?!\*)/,endAst:/\*\*(?!\*)/g,endUnd:/__(?!_)/g},em:{start:/^_|\*/,middle:/^()\*(?=\S)([\s\S]*?\S)\*(?!\*)|^_(?=\S)([\s\S]*?\S)_(?!_)/,endAst:/\*(?!\*)/g,endUnd:/_(?!_)/g},link:c(/^!?\[(label)\]\((.*?)\)/).replace("label",N._label).getRegex(),reflink:c(/^!?\[(label)\]\s*\[([^\]]*)\]/).replace("label",N._label).getRegex()}),N.gfm=d({},N.normal,{escape:c(N.escape).replace("])","~|])").getRegex(),_extended_email:/[A-Za-z0-9._+-]+(@)[a-zA-Z0-9-_]+(?:\.[a-zA-Z0-9-_]*[a-zA-Z0-9])+(?![-_])/,url:/^((?:ftp|https?):\/\/|www\.)(?:[a-zA-Z0-9\-]+\.?)+[^\s<]*|^email/,_backpedal:/(?:[^?!.,:;*_~()&]+|\([^)]*\)|&(?![a-zA-Z0-9]+;$)|[?!.,:;*_~)]+(?!$))+/,del:/^(~~?)(?=[^\s~])([\s\S]*?[^\s~])\1(?=[^~]|$)/,text:/^([`~]+|[^`~])(?:(?= {2,}\n)|(?=[a-zA-Z0-9.!#$%&'*+\/=?_`{\|}~-]+@)|[\s\S]*?(?:(?=[\\0?t[t.length-1].raw+="\n":t.push(r);else if(r=this.tokenizer.code(e))e=e.substring(r.raw.length),i=t[t.length-1],!i||"paragraph"!==i.type&&"text"!==i.type?t.push(r):(i.raw+="\n"+r.raw,i.text+="\n"+r.text,this.inlineQueue[this.inlineQueue.length-1].src=i.text);else if(r=this.tokenizer.fences(e))e=e.substring(r.raw.length),t.push(r);else if(r=this.tokenizer.heading(e))e=e.substring(r.raw.length),t.push(r);else if(r=this.tokenizer.hr(e))e=e.substring(r.raw.length),t.push(r);else if(r=this.tokenizer.blockquote(e))e=e.substring(r.raw.length),t.push(r);else if(r=this.tokenizer.list(e))e=e.substring(r.raw.length),t.push(r);else if(r=this.tokenizer.html(e))e=e.substring(r.raw.length),t.push(r);else if(r=this.tokenizer.def(e))e=e.substring(r.raw.length),i=t[t.length-1],!i||"paragraph"!==i.type&&"text"!==i.type?this.tokens.links[r.tag]||(this.tokens.links[r.tag]={href:r.href,title:r.title}):(i.raw+="\n"+r.raw,i.text+="\n"+r.raw,this.inlineQueue[this.inlineQueue.length-1].src=i.text);else if(r=this.tokenizer.table(e))e=e.substring(r.raw.length),t.push(r);else if(r=this.tokenizer.lheading(e))e=e.substring(r.raw.length),t.push(r);else if(o=e,this.options.extensions&&this.options.extensions.startBlock&&function(){var t=1/0,r=e.slice(1),i=void 0;n.options.extensions.startBlock.forEach(function(e){"number"==typeof(i=e.call({lexer:this},r))&&i>=0&&(t=Math.min(t,i))}),t<1/0&&t>=0&&(o=e.substring(0,t+1))}(),this.state.top&&(r=this.tokenizer.paragraph(o)))i=t[t.length-1],a&&"paragraph"===i.type?(i.raw+="\n"+r.raw,i.text+="\n"+r.text,this.inlineQueue.pop(),this.inlineQueue[this.inlineQueue.length-1].src=i.text):t.push(r),a=o.length!==e.length,e=e.substring(r.raw.length);else if(r=this.tokenizer.text(e))e=e.substring(r.raw.length),i=t[t.length-1],i&&"text"===i.type?(i.raw+="\n"+r.raw,i.text+="\n"+r.text,this.inlineQueue.pop(),this.inlineQueue[this.inlineQueue.length-1].src=i.text):t.push(r);else if(e){var u="Infinite loop on byte: "+e.charCodeAt(0);if(this.options.silent)break;throw new Error(u)}return this.state.top=!0,t},r.inline=function(e,t){this.inlineQueue.push({src:e,tokens:t})},r.inlineTokens=function(e,t){var n=this;void 0===t&&(t=[]);var r,i,o,a,u,s,l=e;if(this.tokens.links){var c=Object.keys(this.tokens.links);if(c.length>0)for(;null!=(a=this.tokenizer.rules.inline.reflinkSearch.exec(l));)c.includes(a[0].slice(a[0].lastIndexOf("[")+1,-1))&&(l=l.slice(0,a.index)+"["+y("a",a[0].length-2)+"]"+l.slice(this.tokenizer.rules.inline.reflinkSearch.lastIndex))}for(;null!=(a=this.tokenizer.rules.inline.blockSkip.exec(l));)l=l.slice(0,a.index)+"["+y("a",a[0].length-2)+"]"+l.slice(this.tokenizer.rules.inline.blockSkip.lastIndex);for(;null!=(a=this.tokenizer.rules.inline.escapedEmSt.exec(l));)l=l.slice(0,a.index)+"++"+l.slice(this.tokenizer.rules.inline.escapedEmSt.lastIndex);for(;e;)if(u||(s=""),u=!1,!(this.options.extensions&&this.options.extensions.inline&&this.options.extensions.inline.some(function(i){return!!(r=i.call({lexer:n},e,t))&&(e=e.substring(r.raw.length),t.push(r),!0)})))if(r=this.tokenizer.escape(e))e=e.substring(r.raw.length),t.push(r);else if(r=this.tokenizer.tag(e))e=e.substring(r.raw.length),i=t[t.length-1],i&&"text"===r.type&&"text"===i.type?(i.raw+=r.raw,i.text+=r.text):t.push(r);else if(r=this.tokenizer.link(e))e=e.substring(r.raw.length),t.push(r);else if(r=this.tokenizer.reflink(e,this.tokens.links))e=e.substring(r.raw.length),i=t[t.length-1],i&&"text"===r.type&&"text"===i.type?(i.raw+=r.raw,i.text+=r.text):t.push(r);else if(r=this.tokenizer.emStrong(e,l,s))e=e.substring(r.raw.length),t.push(r);else if(r=this.tokenizer.codespan(e))e=e.substring(r.raw.length),t.push(r);else if(r=this.tokenizer.br(e))e=e.substring(r.raw.length),t.push(r);else if(r=this.tokenizer.del(e))e=e.substring(r.raw.length),t.push(r);else if(r=this.tokenizer.autolink(e,w))e=e.substring(r.raw.length),t.push(r);else if(this.state.inLink||!(r=this.tokenizer.url(e,w))){if(o=e,this.options.extensions&&this.options.extensions.startInline&&function(){var t=1/0,r=e.slice(1),i=void 0;n.options.extensions.startInline.forEach(function(e){"number"==typeof(i=e.call({lexer:this},r))&&i>=0&&(t=Math.min(t,i))}),t<1/0&&t>=0&&(o=e.substring(0,t+1))}(),r=this.tokenizer.inlineText(o,x))e=e.substring(r.raw.length),"_"!==r.raw.slice(-1)&&(s=r.raw.slice(-1)),u=!0,i=t[t.length-1],i&&"text"===i.type?(i.raw+=r.raw,i.text+=r.text):t.push(r);else if(e){var f="Infinite loop on byte: "+e.charCodeAt(0);if(this.options.silent)break;throw new Error(f)}}else e=e.substring(r.raw.length),t.push(r);return t},n(t,null,[{key:"rules",get:function(){return{block:L,inline:N}}}]),t}(),Q=function(){function t(t){this.options=t||e.defaults}var n=t.prototype;return n.code=function(e,t,n){var r=(t||"").match(/\S*/)[0];if(this.options.highlight){var i=this.options.highlight(e,r);null!=i&&i!==e&&(n=!0,e=i)}return e=e.replace(/\n$/,"")+"\n",r?'
'+(n?e:s(e,!0))+"
\n":"
"+(n?e:s(e,!0))+"
\n"},n.blockquote=function(e){return"
\n"+e+"
\n"},n.html=function(e){return e},n.heading=function(e,t,n,r){return this.options.headerIds?"'+e+"\n":""+e+"\n"},n.hr=function(){return this.options.xhtml?"
\n":"
\n"},n.list=function(e,t,n){var r=t?"ol":"ul";return"<"+r+(t&&1!==n?' start="'+n+'"':"")+">\n"+e+"\n"},n.listitem=function(e){return"
  • "+e+"
  • \n"},n.checkbox=function(e){return" "},n.paragraph=function(e){return"

    "+e+"

    \n"},n.table=function(e,t){return t&&(t=""+t+""),"\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.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.image=function(e,t,n){if(null===(e=f(this.options.sanitize,this.options.baseUrl,e)))return n;var r=''+n+'":">"},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-1e3&&e<1e3||ne.call(/e/,t))return t;var n=/[0-9](?=(?:[0-9]{3})+(?![0-9]))/g;if("number"==typeof e){var r=e<0?-ae(-e):ae(e);if(r!==e){var i=String(r),o=X.call(t,i.length+1);return Y.call(i,n,"$&_")+"."+Y.call(Y.call(o,/([0-9]{3})/g,"$&_"),/_$/,"")}}return Y.call(t,n,"$&_")}function i(e,t,n){var r="double"===(n.quoteStyle||t)?'"':"'";return r+e+r}function o(e){return Y.call(String(e),/"/g,""")}function a(e){return!("[object Array]"!==g(e)||fe&&"object"==typeof e&&fe in e)}function u(e){return!("[object Date]"!==g(e)||fe&&"object"==typeof e&&fe in e)}function s(e){return!("[object RegExp]"!==g(e)||fe&&"object"==typeof e&&fe in e)}function l(e){return!("[object Error]"!==g(e)||fe&&"object"==typeof e&&fe in e)}function c(e){return!("[object String]"!==g(e)||fe&&"object"==typeof e&&fe in e)}function f(e){return!("[object Number]"!==g(e)||fe&&"object"==typeof e&&fe in e)}function p(e){return!("[object Boolean]"!==g(e)||fe&&"object"==typeof e&&fe in e)}function d(e){if(ce)return e&&"object"==typeof e&&e instanceof Symbol;if("symbol"==typeof e)return!0;if(!e||"object"!=typeof e||!le)return!1;try{return le.call(e),!0}catch(e){}return!1}function h(e){if(!e||"object"!=typeof e||!ue)return!1;try{return ue.call(e),!0}catch(e){}return!1}function v(e,t){return ge.call(e,t)}function g(e){return G.call(e)}function m(e){if(e.name)return e.name;var t=J.call(Z.call(e),/^function\s*([\w$]+)/);return t?t[1]:null}function y(e,t){if(e.indexOf)return e.indexOf(t);for(var n=0,r=e.length;nt.maxStringLength){var n=e.length-t.maxStringLength,r="... "+n+" more character"+(n>1?"s":"");return E(X.call(e,0,t.maxStringLength),t)+r}return i(Y.call(Y.call(e,/(['\\])/g,"\\$1"),/[\x00-\x1f]/g,F),"single",t)}function F(e){var t=e.charCodeAt(0),n={8:"b",9:"t",10:"n",12:"f",13:"r"}[t];return n?"\\"+n:"\\x"+(t<16?"0":"")+ee.call(t.toString(16))}function C(e){return"Object("+e+")"}function S(e){return e+" { ? }"}function _(e,t,n,r){return e+" ("+t+") {"+(r?j(n,r):ie.call(n,", "))+"}"}function O(e){for(var t=0;t=0)return!1;return!0}function B(e,t){var n;if("\t"===e.indent)n="\t";else{if(!("number"==typeof e.indent&&e.indent>0))return null;n=ie.call(Array(e.indent+1)," ")}return{base:n,prev:ie.call(Array(t+1),n)}}function j(e,t){if(0===e.length)return"";var n="\n"+t.prev+t.base;return n+ie.call(e,","+n)+"\n"+t.prev}function $(e,t){var n=a(e),r=[];if(n){r.length=e.length;for(var i=0;i0))throw new TypeError('option "indent" must be "\\t", an integer > 0, or `null`');if(v(R,"numericSeparator")&&"boolean"!=typeof R.numericSeparator)throw new TypeError('option "numericSeparator", if provided, must be `true` or `false`');var U=R.numericSeparator;if(void 0===t)return"undefined";if(null===t)return"null";if("boolean"==typeof t)return t?"true":"false";if("string"==typeof t)return E(t,R);if("number"==typeof t){if(0===t)return 1/0/t>0?"0":"-0";var Q=String(t);return U?r(t,Q):Q}if("bigint"==typeof t){var q=String(t)+"n";return U?r(t,q):q}var W=void 0===R.depth?5:R.depth;if(void 0===F&&(F=0),F>=W&&W>0&&"object"==typeof t)return a(t)?"[Array]":"[Object]";var V=B(R,F);if(void 0===T)T=[];else if(y(T,t)>=0)return"[Circular]";if("function"==typeof t){var H=m(t),G=$(t,I);return"[Function"+(H?": "+H:" (anonymous)")+"]"+(G.length>0?" { "+ie.call(G,", ")+" }":"")}if(d(t)){var Z=ce?Y.call(String(t),/^(Symbol\(.*\))_[^)]*$/,"$1"):le.call(t);return"object"!=typeof t||ce?Z:C(Z)}if(k(t)){for(var J="<"+te.call(String(t.nodeName)),ee=t.attributes||[],ne=0;ne"}if(a(t)){if(0===t.length)return"[]";var ae=$(t,I);return V&&!O(ae)?"["+j(ae,V)+"]":"[ "+ie.call(ae,", ")+" ]"}if(l(t)){var se=$(t,I);return"cause"in t&&!pe.call(t,"cause")?"{ ["+String(t)+"] "+ie.call(re.call("[cause]: "+I(t.cause),se),", ")+" }":0===se.length?"["+String(t)+"]":"{ ["+String(t)+"] "+ie.call(se,", ")+" }"}if("object"==typeof t&&M){if(ve&&"function"==typeof t[ve])return t[ve]();if("symbol"!==M&&"function"==typeof t.inspect)return t.inspect()}if(b(t)){var he=[];return P.call(t,function(e,n){he.push(I(n,t,!0)+" => "+I(e,t))}),_("Map",z.call(t),he,V)}if(w(t)){var ge=[];return N.call(t,function(e){ge.push(I(e,t))}),_("Set",L.call(t),ge,V)}if(D(t))return S("WeakMap");if(A(t))return S("WeakSet");if(x(t))return S("WeakRef");if(f(t))return C(I(Number(t)));if(h(t))return C(I(ue.call(t)));if(p(t))return C(K.call(t));if(c(t))return C(I(String(t)));if(!u(t)&&!s(t)){var me=$(t,I),ye=de?de(t)===Object.prototype:t instanceof Object||t.constructor===Object,be=t instanceof Object?"":"null prototype",De=!ye&&fe&&Object(t)===t&&fe in t?X.call(g(t),8,-1):be?"Object":"",xe=ye||"function"!=typeof t.constructor?"":t.constructor.name?t.constructor.name+" ":"",we=xe+(De||be?"["+ie.call(re.call([],De||[],be||[]),": ")+"] ":"");return 0===me.length?we+"{}":V?we+"{"+j(me,V)+"}":we+"{ "+ie.call(me,", ")+" }"}return String(t)};var ge=Object.prototype.hasOwnProperty||function(e){return e in this}},function(e,t,n){"use strict";var r;if(!Object.keys){var i=Object.prototype.hasOwnProperty,o=Object.prototype.toString,a=n(30),u=Object.prototype.propertyIsEnumerable,s=!u.call({toString:null},"toString"),l=u.call(function(){},"prototype"),c=["toString","toLocaleString","valueOf","hasOwnProperty","isPrototypeOf","propertyIsEnumerable","constructor"],f=function(e){var t=e.constructor;return t&&t.prototype===e},p={$applicationCache:!0,$console:!0,$external:!0,$frame:!0,$frameElement:!0,$frames:!0,$innerHeight:!0,$innerWidth:!0,$onmozfullscreenchange:!0,$onmozfullscreenerror:!0,$outerHeight:!0,$outerWidth:!0,$pageXOffset:!0,$pageYOffset:!0,$parent:!0,$scrollLeft:!0,$scrollTop:!0,$scrollX:!0,$scrollY:!0,$self:!0,$webkitIndexedDB:!0,$webkitStorageInfo:!0,$window:!0},d=function(){if("undefined"==typeof window)return!1;for(var e in window)try{if(!p["$"+e]&&i.call(window,e)&&null!==window[e]&&"object"==typeof window[e])try{f(window[e])}catch(e){return!0}}catch(e){return!0}return!1}(),h=function(e){if("undefined"==typeof window||!d)return f(e);try{return f(e)}catch(e){return!1}};r=function(e){var t=null!==e&&"object"==typeof e,n="[object Function]"===o.call(e),r=a(e),u=t&&"[object String]"===o.call(e),f=[];if(!t&&!n&&!r)throw new TypeError("Object.keys called on a non-object");var p=l&&n;if(u&&e.length>0&&!i.call(e,0))for(var d=0;d0)for(var v=0;v1)for(var n=1;n1?n-1:0);for(var r=1;r2?r-2:0);for(var o=2;o";var b=i(s),D=o[n],x=f(b.html,function(e,t){var i=-1!==p.indexOf(D,e),o=l(n,e,t,i);if(!r(o))return o;if(i)return t=h(n,e,t,g),t?e+'="'+t+'"':e;var o=d(n,e,t,i);return r(o)?void 0:o}),s="<"+n;return x&&(s+=" "+x),b.closing&&(s+=" /"),s+=">"}var y=u(n,s,m);return r(y)?v(s):y},v);return m&&(y=m.remove(y)),y},e.exports=a},function(e,t){e.exports={smile:"e3/2018new_weixioa02_org.png",lovely:"09/2018new_keai_org.png",happy:"1e/2018new_taikaixin_org.png",clap:"6e/2018new_guzhang_thumb.png",whee:"33/2018new_xixi_thumb.png",haha:"8f/2018new_haha_thumb.png","laugh and cry":"4a/2018new_xiaoku_thumb.png",wink:"43/2018new_jiyan_org.png",greddy:"fa/2018new_chanzui_org.png",awkward:"a3/2018new_heixian_thumb.png",sweat:"28/2018new_han_org.png","pick nose":"9a/2018new_wabi_thumb.png",hum:"7c/2018new_heng_thumb.png",angry:"f6/2018new_nu_thumb.png",grievance:"a5/2018new_weiqu_thumb.png",poor:"96/2018new_kelian_org.png",disappoint:"aa/2018new_shiwang_thumb.png",sad:"ee/2018new_beishang_org.png",tear:"6e/2018new_leimu_org.png","no way":"83/2018new_kuxiao_org.png",shy:"c1/2018new_haixiu_org.png",dirt:"10/2018new_wu_thumb.png","love you":"f6/2018new_aini_org.png",kiss:"2c/2018new_qinqin_thumb.png",amorousness:"9d/2018new_huaxin_org.png",longing:"c9/2018new_chongjing_org.png",desire:"3e/2018new_tianping_thumb.png","bad laugh":"4d/2018new_huaixiao_org.png",blackness:"9e/2018new_yinxian_org.png","laugh without word":"2d/2018new_xiaoerbuyu_org.png",titter:"71/2018new_touxiao_org.png",cool:"c4/2018new_ku_org.png","not easy":"aa/2018new_bingbujiandan_thumb.png",think:"30/2018new_sikao_org.png",question:"b8/2018new_ningwen_org.png","no idea":"2a/2018new_wenhao_thumb.png",dizzy:"07/2018new_yun_thumb.png",bomb:"a2/2018new_shuai_thumb.png",bone:"a1/2018new_kulou_thumb.png","be quiet":"b0/2018new_xu_org.png","shut up":"62/2018new_bizui_org.png",stupid:"dd/2018new_shayan_org.png","surprise ":"49/2018new_chijing_org.png",vomit:"08/2018new_tu_org.png",cold:"40/2018new_kouzhao_thumb.png",sick:"3b/2018new_shengbing_thumb.png",bye:"fd/2018new_baibai_thumb.png","look down on":"da/2018new_bishi_org.png","white eye":"ef/2018new_landelini_org.png","left hum":"43/2018new_zuohengheng_thumb.png","right hum":"c1/2018new_youhengheng_thumb.png",crazy:"17/2018new_zhuakuang_org.png","scold ":"87/2018new_zhouma_thumb.png","hit on face":"cb/2018new_dalian_org.png",wow:"ae/2018new_ding_org.png",fan:"86/2018new_hufen02_org.png",money:"a2/2018new_qian_thumb.png",yawn:"55/2018new_dahaqian_org.png",sleepy:"3c/2018new_kun_thumb.png",sleep:"e2/2018new_shuijiao_thumb.png","watermelon ":"01/2018new_chigua_thumb.png",doge:"a1/2018new_doge02_org.png",dog:"22/2018new_erha_org.png",cat:"7b/2018new_miaomiao_thumb.png",thumb:"e6/2018new_zan_org.png",good:"8a/2018new_good_org.png",ok:"45/2018new_ok_org.png",yeah:"29/2018new_ye_thumb.png","shack hand":"e9/2018new_woshou_thumb.png",bow:"e7/2018new_zuoyi_org.png",come:"42/2018new_guolai_thumb.png",punch:"86/2018new_quantou_thumb.png"}},function(e,t){e.exports={nick:"NickName",mail:"E-Mail",link:"Website(http://)",nickFail:"NickName cannot be less than 3 bytes.",mailFail:"Please confirm your email address.",sofa:"No comment yet.",submit:"Submit",reply:"Reply",cancelReply:"Cancel reply",comments:"Comments",cancel:"Cancel",confirm:"Confirm",continue:"Continue",more:"Load More...",preview:"Preview",emoji:"Emoji",expand:"See more....",seconds:"seconds ago",minutes:"minutes ago",hours:"hours ago",days:"days ago",now:"just now",uploading:"Uploading ...",uploadDone:"Upload completed!",busy:"Submit is busy, please wait...","code-98":"Valine initialization failed, please check your version of av-min.js.","code-99":"Valine initialization failed, Please check the `el` element in the init method.","code-100":"Valine initialization failed, Please check your appId and appKey.","code-140":"The total number of API calls today has exceeded the development version limit.","code-401":"Unauthorized operation, Please check your appId and appKey.","code-403":"Access denied by API domain white list, Please check your security domain."}},function(e,t){e.exports={nick:"ニックネーム",mail:"メールアドレス",link:"サイト(http://)",nickFail:"3バイト以上のニックネームをご入力ください.",mailFail:"メールアドレスをご確認ください.",sofa:"コメントしましょう~",submit:"提出する",reply:"返信する",cancelReply:"キャンセル",comments:"コメント",cancel:"キャンセル",confirm:"確認する",continue:"继续",more:"さらに読み込む...",preview:"プレビュー",emoji:"絵文字",expand:"もっと見る",seconds:"秒前",minutes:"分前",hours:"時間前",days:"日前",now:"たっだ今",uploading:"アップロード中...",uploadDone:"アップロードが完了しました!",busy:"20 秒間隔で提出してください ...","code-98":"ロードエラーです。av-min.js のバージョンを確認してください.","code-99":"ロードエラーです。initにある`el`エレメントを確認ください.","code-100":"ロードエラーです。AppIdとAppKeyを確認ください.","code-140":"今日のAPIコールの総数が開発バージョンの上限を超えた.","code-401":"権限が制限されています。AppIdとAppKeyを確認ください.","code-403":"アクセスがAPIなどに制限されました、ドメイン名のセキュリティ設定を確認ください"}},function(e,t){e.exports={nick:"昵称",mail:"邮箱",link:"网址(http://)",nickFail:"昵称不能少于3个字符",mailFail:"请填写正确的邮件地址",sofa:"来发评论吧~",submit:"提交",reply:"回复",cancelReply:"取消回复",comments:"评论",cancel:"取消",confirm:"确认",continue:"继续",more:"加载更多...",preview:"预览",emoji:"表情",expand:"查看更多...",seconds:"秒前",minutes:"分钟前",hours:"小时前",days:"天前",now:"刚刚",uploading:"正在传输...",uploadDone:"传输完成!",busy:"操作频繁,请稍候再试...","code-98":"Valine 初始化失败,请检查 av-min.js 版本","code-99":"Valine 初始化失败,请检查init中的`el`元素.","code-100":"Valine 初始化失败,请检查你的AppId和AppKey.","code-140":"今日 API 调用总次数已超过开发版限制.","code-401":"未经授权的操作,请检查你的AppId和AppKey.","code-403":"访问被API域名白名单拒绝,请检查你的安全域名设置."}},function(e,t){e.exports={nick:"暱稱",mail:"郵箱",link:"網址(http://)",nickFail:"昵稱不能少於3個字符",mailFail:"請填寫正確的郵件地址",sofa:"來發評論吧~",submit:"提交",reply:"回覆",cancelReply:"取消回覆",comments:"評論",cancel:"取消",confirm:"確認",continue:"繼續",more:"加載更多...",preview:"預覽",emoji:"表情",expand:"查看更多...",seconds:"秒前",minutes:"分鐘前",hours:"小時前",days:"天前",now:"剛剛",uploading:"正在上傳...",uploadDone:"上傳完成!",busy:"操作頻繁,請稍候再試...","code-98":"Valine 初始化失敗,請檢查 av-min.js 版本","code-99":"Valine 初始化失敗,請檢查init中的`el`元素.","code-100":"Valine 初始化失敗,請檢查你的AppId和AppKey.","code-140":"今日 API 調用總次數已超過開發版限制.","code-401":"未經授權的操作,請檢查你的AppId和AppKey.","code-403":"訪問被API域名白名單拒絕,請檢查你的安全域名設置."}},function(e,t){},function(e,t,n){var r=n(115);"string"==typeof r&&(r=[[e.i,r,""]]);var i={};i.transform=void 0;n(117)(r,i);r.locals&&(e.exports=r.locals)},function(e,t,n){t=n(116)(!1),t.push([e.i,'.v[data-class="v"]{font-size:16px;text-align:left}.v[data-class="v"] *{-webkit-box-sizing:border-box;box-sizing:border-box;line-height:1.75}.v[data-class="v"] .vinput,.v[data-class="v"] .veditor,.v[data-class="v"] p,.v[data-class="v"] pre code,.v[data-class="v"] .status-bar{color:#555}.v[data-class="v"] .vtime,.v[data-class="v"] .vsys{color:#b3b3b3}.v[data-class="v"] .text-right{text-align:right}.v[data-class="v"] .text-center{text-align:center}.v[data-class="v"] img{max-width:100%;border:none}.v[data-class="v"] hr{margin:.825em 0;border-color:#f6f6f6;border-style:dashed}.v[data-class="v"].hide-avatar .vimg{display:none}.v[data-class="v"] a{position:relative;cursor:pointer;color:#1abc9c;text-decoration:none;display:inline-block}.v[data-class="v"] a:hover{color:#D7191A}.v[data-class="v"] pre,.v[data-class="v"] code{background-color:#f8f8f8;padding:0.2em 0.4em;border-radius:3px;font-size:85%;margin:0}.v[data-class="v"] pre{padding:10px;overflow:auto;line-height:1.45}.v[data-class="v"] pre code{padding:0;background:transparent;white-space:pre-wrap;word-break:keep-all}.v[data-class="v"] blockquote{color:#666;margin:.5em 0;padding:0 0 0 1em;border-left:8px solid rgba(238,238,238,0.5)}.v[data-class="v"] .vinput{border:none;resize:none;outline:none;padding:10px 5px;max-width:100%;font-size:.775em;-webkit-box-sizing:border-box;box-sizing:border-box}.v[data-class="v"] input[type=\'checkbox\'],.v[data-class="v"] input[type=\'radio\']{display:inline-block;vertical-align:middle;margin-top:-2px}.v[data-class="v"] .vicon{cursor:pointer;display:inline-block;overflow:hidden;fill:#555;vertical-align:middle}.v[data-class="v"] .vicon+.vicon{margin-left:10px}.v[data-class="v"] .vicon.actived{fill:#66b1ff}.v[data-class="v"] .vrow{font-size:0;padding:10px 0}.v[data-class="v"] .vrow .vcol{display:inline-block;vertical-align:middle;font-size:14px}.v[data-class="v"] .vrow .vcol.vcol-20{width:20%}.v[data-class="v"] .vrow .vcol.vcol-30{width:30%}.v[data-class="v"] .vrow .vcol.vcol-40{width:40%}.v[data-class="v"] .vrow .vcol.vcol-50{width:50%}.v[data-class="v"] .vrow .vcol.vcol-60{width:60%}.v[data-class="v"] .vrow .vcol.vcol-70{width:70%}.v[data-class="v"] .vrow .vcol.vcol-80{width:80%}.v[data-class="v"] .vrow .vcol.vctrl{font-size:12px}.v[data-class="v"] .vemoji,.v[data-class="v"] .emoji{width:26px;height:26px;overflow:hidden;vertical-align:middle;margin:0 1px;display:inline-block}.v[data-class="v"] .vwrap{border:1px solid #f0f0f0;border-radius:4px;margin-bottom:10px;overflow:hidden;position:relative;padding:10px}.v[data-class="v"] .vwrap input{background:transparent}.v[data-class="v"] .vwrap .vedit{position:relative;padding-top:10px}.v[data-class="v"] .vwrap .cancel-reply-btn{position:absolute;right:5px;top:5px;cursor:pointer}.v[data-class="v"] .vwrap .vemojis{display:none;font-size:18px;max-height:145px;overflow:auto;padding-bottom:10px;-webkit-box-shadow:0px 0 1px #f0f0f0;box-shadow:0px 0 1px #f0f0f0}.v[data-class="v"] .vwrap .vemojis i{font-style:normal;padding-top:7px;width:36px;cursor:pointer;text-align:center;display:inline-block;vertical-align:middle}.v[data-class="v"] .vwrap .vpreview{padding:7px;-webkit-box-shadow:0px 0 1px #f0f0f0;box-shadow:0px 0 1px #f0f0f0}.v[data-class="v"] .vwrap .vheader .vinput{width:33.33%;border-bottom:1px #dedede dashed}.v[data-class="v"] .vwrap .vheader.item2 .vinput{width:50%}.v[data-class="v"] .vwrap .vheader.item1 .vinput{width:100%}.v[data-class="v"] .vwrap .vheader .vinput:focus{border-bottom-color:#eb5055}@media screen and (max-width: 520px){.v[data-class="v"] .vwrap .vheader .vinput{width:100%}.v[data-class="v"] .vwrap .vheader.item2 .vinput{width:100%}}.v[data-class="v"] .vpower{color:#999;font-size:.75em;padding:.5em 0}.v[data-class="v"] .vpower a{font-size:.75em}.v[data-class="v"] .vcount{padding:5px;font-weight:600;font-size:1.25em}.v[data-class="v"] ul,.v[data-class="v"] ol{padding:0;margin-left:1.25em}.v[data-class="v"] .txt-center{text-align:center}.v[data-class="v"] .txt-right{text-align:right}.v[data-class="v"] .veditor{width:100%;min-height:8.75em;font-size:.875em;background:transparent;resize:vertical;-webkit-transition:all .25s ease;transition:all .25s ease}.v[data-class="v"] .vbtn{-webkit-transition-duration:.4s;transition-duration:.4s;text-align:center;color:#555;border:1px solid #ededed;border-radius:.3em;display:inline-block;background:transparent;margin-bottom:0;font-weight:400;vertical-align:middle;-ms-touch-action:manipulation;touch-action:manipulation;cursor:pointer;white-space:nowrap;padding:.5em 1.25em;font-size:.875em;line-height:1.42857143;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;outline:none}.v[data-class="v"] .vbtn+.vbtn{margin-left:1.25em}.v[data-class="v"] .vbtn:active,.v[data-class="v"] .vbtn:hover{color:#3090e4;border-color:#3090e4}.v[data-class="v"] .vbtn:disabled{border-color:#E1E1E1;color:#E1E1E1;background-color:#fdfafa;cursor:not-allowed}.v[data-class="v"] .vempty{padding:1.25em;text-align:center;color:#555;overflow:auto}.v[data-class="v"] .vsys{display:inline-block;padding:.2em .5em;font-size:.75em;border-radius:.2em;margin-right:.3em}@media screen and (max-width: 520px){.v[data-class="v"] .vsys{display:none}}.v[data-class="v"] .vcards{width:100%}.v[data-class="v"] .vcards .vcard{padding-top:1.25em;position:relative;display:block}.v[data-class="v"] .vcards .vcard:after{content:\'\';clear:both;display:block}.v[data-class="v"] .vcards .vcard .vimg{width:3.125em;height:3.125em;float:left;border-radius:50%;margin-right:.7525em;border:1px solid #f5f5f5;padding:.125em}@media screen and (max-width: 720px){.v[data-class="v"] .vcards .vcard .vimg{width:2.5em;height:2.5em}}.v[data-class="v"] .vcards .vcard .vhead{line-height:1.5;margin-top:0}.v[data-class="v"] .vcards .vcard .vhead .vnick{position:relative;font-size:.875em;font-weight:500;margin-right:.875em;cursor:pointer;text-decoration:none;display:inline-block}.v[data-class="v"] .vcards .vcard .vhead .vnick:hover{color:#D7191A}.v[data-class="v"] .vcards .vcard .vh{overflow:hidden;padding-bottom:.5em;border-bottom:1px dashed #f5f5f5}.v[data-class="v"] .vcards .vcard .vh .vtime{font-size:.75em;margin-right:.875em}.v[data-class="v"] .vcards .vcard .vh .vmeta{line-height:1;position:relative}.v[data-class="v"] .vcards .vcard .vh .vmeta .vat{font-size:.8125em;color:#ef2f11;cursor:pointer;float:right}.v[data-class="v"] .vcards .vcard:last-child .vh{border-bottom:none}.v[data-class="v"] .vcards .vcard .vcontent{word-wrap:break-word;word-break:break-all;font-size:.875em;line-height:2;position:relative;margin-bottom:.75em;padding-top:.625em}.v[data-class="v"] .vcards .vcard .vcontent.expand{cursor:pointer;max-height:8em;overflow:hidden}.v[data-class="v"] .vcards .vcard .vcontent.expand::before{display:block;content:"";position:absolute;width:100%;left:0;top:0;bottom:3.15em;background:-webkit-gradient(linear, left top, left bottom, from(rgba(255,255,255,0)), to(rgba(255,255,255,0.9)));background:linear-gradient(180deg, rgba(255,255,255,0), rgba(255,255,255,0.9));z-index:999}.v[data-class="v"] .vcards .vcard .vcontent.expand::after{display:block;content:attr(data-expand);text-align:center;color:#828586;position:absolute;width:100%;height:3.15em;line-height:3.15em;left:0;bottom:0;z-index:999;background:rgba(255,255,255,0.9)}.v[data-class="v"] .vcards .vcard .vquote{padding-left:1em;border-left:1px dashed rgba(238,238,238,0.5)}.v[data-class="v"] .vcards .vcard .vquote .vimg{width:2.225em;height:2.225em}.v[data-class="v"] .vpage .vmore{margin:1em 0}.v[data-class="v"] .clear{content:\'\';display:block;clear:both}@-webkit-keyframes spin{0%{-webkit-transform:rotate(0deg);transform:rotate(0deg)}100%{-webkit-transform:rotate(360deg);transform:rotate(360deg)}}@keyframes spin{0%{-webkit-transform:rotate(0deg);transform:rotate(0deg)}100%{-webkit-transform:rotate(360deg);transform:rotate(360deg)}}@-webkit-keyframes pulse{50%{background:#dcdcdc}}@keyframes pulse{50%{background:#dcdcdc}}.v[data-class="v"] .vspinner{width:22px;height:22px;display:inline-block;border:6px double #a0a0a0;border-top-color:transparent;border-bottom-color:transparent;border-radius:50%;-webkit-animation:spin 1s infinite linear;animation:spin 1s infinite linear;position:relative;vertical-align:middle;margin:0 5px}[data-theme="dark"] .v[data-class="v"] .vinput,[data-theme="dark"] .v[data-class="v"] .veditor,[data-theme="dark"] .v[data-class="v"] p,[data-theme="dark"] .v[data-class="v"] pre code,[data-theme="dark"] .v[data-class="v"] .status-bar,.dark .v[data-class="v"] .vinput,.dark .v[data-class="v"] .veditor,.dark .v[data-class="v"] p,.dark .v[data-class="v"] pre code,.dark .v[data-class="v"] .status-bar,.theme__dark .v[data-class="v"] .vinput,.theme__dark .v[data-class="v"] .veditor,.theme__dark .v[data-class="v"] p,.theme__dark .v[data-class="v"] pre code,.theme__dark .v[data-class="v"] .status-bar,.night .v[data-class="v"] .vinput,.night .v[data-class="v"] .veditor,.night .v[data-class="v"] p,.night .v[data-class="v"] pre code,.night .v[data-class="v"] .status-bar{color:#b2b2b5}[data-theme="dark"] .v[data-class="v"] .vtime,[data-theme="dark"] .v[data-class="v"] .vsys,.dark .v[data-class="v"] .vtime,.dark .v[data-class="v"] .vsys,.theme__dark .v[data-class="v"] .vtime,.theme__dark .v[data-class="v"] .vsys,.night .v[data-class="v"] .vtime,.night .v[data-class="v"] .vsys{color:#929298}[data-theme="dark"] .v[data-class="v"] pre,[data-theme="dark"] .v[data-class="v"] code,[data-theme="dark"] .v[data-class="v"] pre code,.dark .v[data-class="v"] pre,.dark .v[data-class="v"] code,.dark .v[data-class="v"] pre code,.theme__dark .v[data-class="v"] pre,.theme__dark .v[data-class="v"] code,.theme__dark .v[data-class="v"] pre code,.night .v[data-class="v"] pre,.night .v[data-class="v"] code,.night .v[data-class="v"] pre code{color:#929298;background-color:#151414}[data-theme="dark"] .v[data-class="v"] .vwrap,.dark .v[data-class="v"] .vwrap,.theme__dark .v[data-class="v"] .vwrap,.night .v[data-class="v"] .vwrap{border-color:#b2b2b5}[data-theme="dark"] .v[data-class="v"] .vicon,.dark .v[data-class="v"] .vicon,.theme__dark .v[data-class="v"] .vicon,.night .v[data-class="v"] .vicon{fill:#b2b2b5}[data-theme="dark"] .v[data-class="v"] .vicon.actived,.dark .v[data-class="v"] .vicon.actived,.theme__dark .v[data-class="v"] .vicon.actived,.night .v[data-class="v"] .vicon.actived{fill:#66b1ff}[data-theme="dark"] .v[data-class="v"] .vbtn,.dark .v[data-class="v"] .vbtn,.theme__dark .v[data-class="v"] .vbtn,.night .v[data-class="v"] .vbtn{color:#b2b2b5;border-color:#b2b2b5}[data-theme="dark"] .v[data-class="v"] .vbtn:hover,.dark .v[data-class="v"] .vbtn:hover,.theme__dark .v[data-class="v"] .vbtn:hover,.night .v[data-class="v"] .vbtn:hover{color:#66b1ff;border-color:#66b1ff}[data-theme="dark"] .v[data-class="v"] a:hover,.dark .v[data-class="v"] a:hover,.theme__dark .v[data-class="v"] a:hover,.night .v[data-class="v"] a:hover{color:#D7191A}[data-theme="dark"] .v[data-class="v"] .vcards .vcard .vcontent.expand::before,.dark .v[data-class="v"] .vcards .vcard .vcontent.expand::before,.theme__dark .v[data-class="v"] .vcards .vcard .vcontent.expand::before,.night .v[data-class="v"] .vcards .vcard .vcontent.expand::before{background:-webkit-gradient(linear, left top, left bottom, from(rgba(0,0,0,0.3)), to(rgba(0,0,0,0.7)));background:linear-gradient(180deg, rgba(0,0,0,0.3), rgba(0,0,0,0.7))}[data-theme="dark"] .v[data-class="v"] .vcards .vcard .vcontent.expand::after,.dark .v[data-class="v"] .vcards .vcard .vcontent.expand::after,.theme__dark .v[data-class="v"] .vcards .vcard .vcontent.expand::after,.night .v[data-class="v"] .vcards .vcard .vcontent.expand::after{background:rgba(0,0,0,0.7)}@media (prefers-color-scheme: dark){.v[data-class="v"] .vinput,.v[data-class="v"] .veditor,.v[data-class="v"] p,.v[data-class="v"] pre code,.v[data-class="v"] .status-bar{color:#b2b2b5}.v[data-class="v"] .vtime,.v[data-class="v"] .vsys{color:#929298}.v[data-class="v"] pre,.v[data-class="v"] code,.v[data-class="v"] pre code{color:#929298;background-color:#151414}.v[data-class="v"] .vwrap{border-color:#b2b2b5}.v[data-class="v"] .vicon{fill:#b2b2b5}.v[data-class="v"] .vicon.actived{fill:#66b1ff}.v[data-class="v"] .vbtn{color:#b2b2b5;border-color:#b2b2b5}.v[data-class="v"] .vbtn:hover{color:#66b1ff;border-color:#66b1ff}.v[data-class="v"] a:hover{color:#D7191A}.v[data-class="v"] .vcards .vcard .vcontent.expand::before{background:-webkit-gradient(linear, left top, left bottom, from(rgba(0,0,0,0.3)), to(rgba(0,0,0,0.7)));background:linear-gradient(180deg, rgba(0,0,0,0.3), rgba(0,0,0,0.7))}.v[data-class="v"] .vcards .vcard .vcontent.expand::after{background:rgba(0,0,0,0.7)}}\n',""]),e.exports=t},function(e,t,n){"use strict";function r(e,t){var n=e[1]||"",r=e[3];if(!r)return n;if(t&&"function"==typeof btoa){var o=i(r);return[n].concat(r.sources.map(function(e){return"/*# sourceURL=".concat(r.sourceRoot||"").concat(e," */")})).concat([o]).join("\n")}return[n].join("\n")}function i(e){return"/*# ".concat("sourceMappingURL=data:application/json;charset=utf-8;base64,".concat(btoa(unescape(encodeURIComponent(JSON.stringify(e)))))," */")}e.exports=function(e){var t=[];return t.toString=function(){return this.map(function(t){var n=r(t,e);return t[2]?"@media ".concat(t[2]," {").concat(n,"}"):n}).join("")},t.i=function(e,n,r){"string"==typeof e&&(e=[[null,e,""]]);var i={};if(r)for(var o=0;o=0&&b.splice(t,1)}function u(e){var t=document.createElement("style");return e.attrs.type="text/css",l(t,e.attrs),o(e,t),t}function s(e){var t=document.createElement("link");return e.attrs.type="text/css",e.attrs.rel="stylesheet",l(t,e.attrs),o(e,t),t}function l(e,t){Object.keys(t).forEach(function(n){e.setAttribute(n,t[n])})}function c(e,t){var n,r,i,o;if(t.transform&&e.css){if(!(o=t.transform(e.css)))return function(){};e.css=o}if(t.singleton){var l=y++;n=m||(m=u(t)),r=f.bind(null,n,l,!1),i=f.bind(null,n,l,!0)}else e.sourceMap&&"function"==typeof URL&&"function"==typeof URL.createObjectURL&&"function"==typeof URL.revokeObjectURL&&"function"==typeof Blob&&"function"==typeof btoa?(n=s(t),r=d.bind(null,n,t),i=function(){a(n),n.href&&URL.revokeObjectURL(n.href)}):(n=u(t),r=p.bind(null,n),i=function(){a(n)});return r(e),function(t){if(t){if(t.css===e.css&&t.media===e.media&&t.sourceMap===e.sourceMap)return;r(e=t)}else i()}}function f(e,t,n,r){var i=n?"":r.css;if(e.styleSheet)e.styleSheet.cssText=x(t,i);else{var o=document.createTextNode(i),a=e.childNodes;a[t]&&e.removeChild(a[t]),a.length?e.insertBefore(o,a[t]):e.appendChild(o)}}function p(e,t){var n=t.css,r=t.media;if(r&&e.setAttribute("media",r),e.styleSheet)e.styleSheet.cssText=n;else{for(;e.firstChild;)e.removeChild(e.firstChild);e.appendChild(document.createTextNode(n))}}function d(e,t,n){var r=n.css,i=n.sourceMap,o=void 0===t.convertToAbsoluteUrls&&i;(t.convertToAbsoluteUrls||o)&&(r=D(r)),i&&(r+="\n/*# sourceMappingURL=data:application/json;base64,"+btoa(unescape(encodeURIComponent(JSON.stringify(i))))+" */");var a=new Blob([r],{type:"text/css"}),u=e.href;e.href=URL.createObjectURL(a),u&&URL.revokeObjectURL(u)}var h={},v=function(e){var t;return function(){return void 0===t&&(t=e.apply(this,arguments)),t}}(function(){return window&&document&&document.all&&!window.atob}),g=function(e){var t={};return function(n){return void 0===t[n]&&(t[n]=e.call(this,n)),t[n]}}(function(e){return document.querySelector(e)}),m=null,y=0,b=[],D=n(118);e.exports=function(e,t){if("undefined"!=typeof DEBUG&&DEBUG&&"object"!=typeof document)throw new Error("The style-loader cannot be used in a non-browser environment");t=t||{},t.attrs="object"==typeof t.attrs?t.attrs:{},t.singleton||(t.singleton=v()),t.insertInto||(t.insertInto="head"),t.insertAt||(t.insertAt="bottom");var n=i(e,t);return r(n,t),function(e){for(var o=[],a=0;a { + const algoliaSettings = CONFIG.algolia; + const { indexName, appID, apiKey } = algoliaSettings; + + let search = instantsearch({ + indexName, + searchClient : algoliasearch(appID, apiKey), + searchFunction: helper => { + let searchInput = document.querySelector('.search-input'); + if (searchInput.value) { + helper.search(); + } + } + }); + + window.pjax && search.on('render', () => { + window.pjax.refresh(document.getElementById('algolia-hits')); + }); + + // Registering Widgets + search.addWidgets([ + instantsearch.widgets.configure({ + hitsPerPage: algoliaSettings.hits.per_page || 10 + }), + + instantsearch.widgets.searchBox({ + container : '.search-input-container', + placeholder : algoliaSettings.labels.input_placeholder, + // Hide default icons of algolia search + showReset : false, + showSubmit : false, + showLoadingIndicator: false, + cssClasses : { + input: 'search-input' + } + }), + + instantsearch.widgets.stats({ + container: '#algolia-stats', + templates: { + text: data => { + let stats = algoliaSettings.labels.hits_stats + .replace(/\$\{hits}/, data.nbHits) + .replace(/\$\{time}/, data.processingTimeMS); + return `${stats} + + Algolia + +
    `; + } + } + }), + + instantsearch.widgets.hits({ + container: '#algolia-hits', + templates: { + item: data => { + let link = data.permalink ? data.permalink : CONFIG.root + data.path; + return `
    ${data._highlightResult.title.value}`; + }, + empty: data => { + return `
    + ${algoliaSettings.labels.hits_empty.replace(/\$\{query}/, data.query)} +
    `; + } + }, + cssClasses: { + item: 'algolia-hit-item' + } + }), + + instantsearch.widgets.pagination({ + container: '#algolia-pagination', + scrollTo : false, + showFirst: false, + showLast : false, + templates: { + first : '', + last : '', + previous: '', + next : '' + }, + cssClasses: { + root : 'pagination', + item : 'pagination-item', + link : 'page-number', + selectedItem: 'current', + disabledItem: 'disabled-item' + } + }) + ]); + + search.start(); + + // Handle and trigger popup window + document.querySelectorAll('.popup-trigger').forEach(element => { + element.addEventListener('click', () => { + document.body.style.overflow = 'hidden'; + document.querySelector('.search-pop-overlay').classList.add('search-active'); + document.querySelector('.search-input').focus(); + }); + }); + + // Monitor main search box + const onPopupClose = () => { + document.body.style.overflow = ''; + document.querySelector('.search-pop-overlay').classList.remove('search-active'); + }; + + document.querySelector('.search-pop-overlay').addEventListener('click', event => { + if (event.target === document.querySelector('.search-pop-overlay')) { + onPopupClose(); + } + }); + document.querySelector('.popup-btn-close').addEventListener('click', onPopupClose); + window.addEventListener('pjax:success', onPopupClose); + window.addEventListener('keyup', event => { + if (event.key === 'Escape') { + onPopupClose(); + } + }); +}); diff --git a/js/bookmark.js b/js/bookmark.js new file mode 100644 index 0000000000..b633883b05 --- /dev/null +++ b/js/bookmark.js @@ -0,0 +1,56 @@ +/* global CONFIG */ + +window.addEventListener('DOMContentLoaded', () => { + 'use strict'; + + var doSaveScroll = () => { + localStorage.setItem('bookmark' + location.pathname, window.scrollY); + }; + + var scrollToMark = () => { + var top = localStorage.getItem('bookmark' + location.pathname); + top = parseInt(top, 10); + // If the page opens with a specific hash, just jump out + if (!isNaN(top) && location.hash === '') { + // Auto scroll to the position + window.anime({ + targets : document.scrollingElement, + duration : 200, + easing : 'linear', + scrollTop: top + }); + } + }; + // Register everything + var init = function(trigger) { + // Create a link element + var link = document.querySelector('.book-mark-link'); + // Scroll event + window.addEventListener('scroll', () => link.classList.toggle('book-mark-link-fixed', window.scrollY === 0)); + // Register beforeunload event when the trigger is auto + if (trigger === 'auto') { + // Register beforeunload event + window.addEventListener('beforeunload', doSaveScroll); + window.addEventListener('pjax:send', doSaveScroll); + } + // Save the position by clicking the icon + link.addEventListener('click', () => { + doSaveScroll(); + window.anime({ + targets : link, + duration: 200, + easing : 'linear', + top : -30, + complete: () => { + setTimeout(() => { + link.style.top = ''; + }, 400); + } + }); + }); + scrollToMark(); + window.addEventListener('pjax:success', scrollToMark); + }; + + init(CONFIG.bookmark.save); +}); diff --git a/js/local-search.js b/js/local-search.js new file mode 100644 index 0000000000..4b7b076d55 --- /dev/null +++ b/js/local-search.js @@ -0,0 +1,278 @@ +/* global CONFIG */ + +window.addEventListener('DOMContentLoaded', () => { + // Popup Window + let isfetched = false; + let datas; + let isXml = true; + // Search DB path + let searchPath = CONFIG.path; + if (searchPath.length === 0) { + searchPath = 'search.xml'; + } else if (searchPath.endsWith('json')) { + isXml = false; + } + const input = document.querySelector('.search-input'); + const resultContent = document.getElementById('search-result'); + + const getIndexByWord = (word, text, caseSensitive) => { + if (CONFIG.localsearch.unescape) { + let div = document.createElement('div'); + div.innerText = word; + word = div.innerHTML; + } + let wordLen = word.length; + if (wordLen === 0) return []; + let startPosition = 0; + let position = []; + let index = []; + if (!caseSensitive) { + text = text.toLowerCase(); + word = word.toLowerCase(); + } + while ((position = text.indexOf(word, startPosition)) > -1) { + index.push({ position, word }); + startPosition = position + wordLen; + } + return index; + }; + + // Merge hits into slices + const mergeIntoSlice = (start, end, index, searchText) => { + let item = index[index.length - 1]; + let { position, word } = item; + let hits = []; + let searchTextCountInSlice = 0; + while (position + word.length <= end && index.length !== 0) { + if (word === searchText) { + searchTextCountInSlice++; + } + hits.push({ + position, + length: word.length + }); + let wordEnd = position + word.length; + + // Move to next position of hit + index.pop(); + while (index.length !== 0) { + item = index[index.length - 1]; + position = item.position; + word = item.word; + if (wordEnd > position) { + index.pop(); + } else { + break; + } + } + } + return { + hits, + start, + end, + searchTextCount: searchTextCountInSlice + }; + }; + + // Highlight title and content + const highlightKeyword = (text, slice) => { + let result = ''; + let prevEnd = slice.start; + slice.hits.forEach(hit => { + result += text.substring(prevEnd, hit.position); + let end = hit.position + hit.length; + result += `${text.substring(hit.position, end)}`; + prevEnd = end; + }); + result += text.substring(prevEnd, slice.end); + return result; + }; + + const inputEventFunction = () => { + if (!isfetched) return; + let searchText = input.value.trim().toLowerCase(); + let keywords = searchText.split(/[-\s]+/); + if (keywords.length > 1) { + keywords.push(searchText); + } + let resultItems = []; + if (searchText.length > 0) { + // Perform local searching + datas.forEach(({ title, content, url }) => { + let titleInLowerCase = title.toLowerCase(); + let contentInLowerCase = content.toLowerCase(); + let indexOfTitle = []; + let indexOfContent = []; + let searchTextCount = 0; + keywords.forEach(keyword => { + indexOfTitle = indexOfTitle.concat(getIndexByWord(keyword, titleInLowerCase, false)); + indexOfContent = indexOfContent.concat(getIndexByWord(keyword, contentInLowerCase, false)); + }); + + // Show search results + if (indexOfTitle.length > 0 || indexOfContent.length > 0) { + let hitCount = indexOfTitle.length + indexOfContent.length; + // Sort index by position of keyword + [indexOfTitle, indexOfContent].forEach(index => { + index.sort((itemLeft, itemRight) => { + if (itemRight.position !== itemLeft.position) { + return itemRight.position - itemLeft.position; + } + return itemLeft.word.length - itemRight.word.length; + }); + }); + + let slicesOfTitle = []; + if (indexOfTitle.length !== 0) { + let tmp = mergeIntoSlice(0, title.length, indexOfTitle, searchText); + searchTextCount += tmp.searchTextCountInSlice; + slicesOfTitle.push(tmp); + } + + let slicesOfContent = []; + while (indexOfContent.length !== 0) { + let item = indexOfContent[indexOfContent.length - 1]; + let { position, word } = item; + // Cut out 100 characters + let start = position - 20; + let end = position + 80; + if (start < 0) { + start = 0; + } + if (end < position + word.length) { + end = position + word.length; + } + if (end > content.length) { + end = content.length; + } + let tmp = mergeIntoSlice(start, end, indexOfContent, searchText); + searchTextCount += tmp.searchTextCountInSlice; + slicesOfContent.push(tmp); + } + + // Sort slices in content by search text's count and hits' count + slicesOfContent.sort((sliceLeft, sliceRight) => { + if (sliceLeft.searchTextCount !== sliceRight.searchTextCount) { + return sliceRight.searchTextCount - sliceLeft.searchTextCount; + } else if (sliceLeft.hits.length !== sliceRight.hits.length) { + return sliceRight.hits.length - sliceLeft.hits.length; + } + return sliceLeft.start - sliceRight.start; + }); + + // Select top N slices in content + let upperBound = parseInt(CONFIG.localsearch.top_n_per_article, 10); + if (upperBound >= 0) { + slicesOfContent = slicesOfContent.slice(0, upperBound); + } + + let resultItem = ''; + + if (slicesOfTitle.length !== 0) { + resultItem += `
  • ${highlightKeyword(title, slicesOfTitle[0])}`; + } else { + resultItem += `
  • ${title}`; + } + + slicesOfContent.forEach(slice => { + resultItem += `

    ${highlightKeyword(content, slice)}...

    `; + }); + + resultItem += '
  • '; + resultItems.push({ + item: resultItem, + id : resultItems.length, + hitCount, + searchTextCount + }); + } + }); + } + if (keywords.length === 1 && keywords[0] === '') { + resultContent.innerHTML = '
    '; + } else if (resultItems.length === 0) { + resultContent.innerHTML = '
    '; + } else { + resultItems.sort((resultLeft, resultRight) => { + if (resultLeft.searchTextCount !== resultRight.searchTextCount) { + return resultRight.searchTextCount - resultLeft.searchTextCount; + } else if (resultLeft.hitCount !== resultRight.hitCount) { + return resultRight.hitCount - resultLeft.hitCount; + } + return resultRight.id - resultLeft.id; + }); + resultContent.innerHTML = `
      ${resultItems.map(result => result.item).join('')}
    `; + window.pjax && window.pjax.refresh(resultContent); + } + }; + + const fetchData = () => { + fetch(CONFIG.root + searchPath) + .then(response => response.text()) + .then(res => { + // Get the contents from search data + isfetched = true; + datas = isXml ? [...new DOMParser().parseFromString(res, 'text/xml').querySelectorAll('entry')].map(element => { + return { + title : element.querySelector('title').textContent, + content: element.querySelector('content').textContent, + url : element.querySelector('url').textContent + }; + }) : JSON.parse(res); + // Only match articles with not empty titles + datas = datas.filter(data => data.title).map(data => { + data.title = data.title.trim(); + data.content = data.content ? data.content.trim().replace(/<[^>]+>/g, '') : ''; + data.url = decodeURIComponent(data.url).replace(/\/{2,}/g, '/'); + return data; + }); + // Remove loading animation + document.getElementById('no-result').innerHTML = ''; + inputEventFunction(); + }); + }; + + if (CONFIG.localsearch.preload) { + fetchData(); + } + + if (CONFIG.localsearch.trigger === 'auto') { + input.addEventListener('input', inputEventFunction); + } else { + document.querySelector('.search-icon').addEventListener('click', inputEventFunction); + input.addEventListener('keypress', event => { + if (event.key === 'Enter') { + inputEventFunction(); + } + }); + } + + // Handle and trigger popup window + document.querySelectorAll('.popup-trigger').forEach(element => { + element.addEventListener('click', () => { + document.body.style.overflow = 'hidden'; + document.querySelector('.search-pop-overlay').classList.add('search-active'); + input.focus(); + if (!isfetched) fetchData(); + }); + }); + + // Monitor main search box + const onPopupClose = () => { + document.body.style.overflow = ''; + document.querySelector('.search-pop-overlay').classList.remove('search-active'); + }; + + document.querySelector('.search-pop-overlay').addEventListener('click', event => { + if (event.target === document.querySelector('.search-pop-overlay')) { + onPopupClose(); + } + }); + document.querySelector('.popup-btn-close').addEventListener('click', onPopupClose); + window.addEventListener('pjax:success', onPopupClose); + window.addEventListener('keyup', event => { + if (event.key === 'Escape') { + onPopupClose(); + } + }); +}); diff --git a/js/motion.js b/js/motion.js new file mode 100644 index 0000000000..026199aabb --- /dev/null +++ b/js/motion.js @@ -0,0 +1,177 @@ +/* global NexT, CONFIG, Velocity */ + +if (window.$ && window.$.Velocity) window.Velocity = window.$.Velocity; + +NexT.motion = {}; + +NexT.motion.integrator = { + queue : [], + cursor: -1, + init : function() { + this.queue = []; + this.cursor = -1; + return this; + }, + add: function(fn) { + this.queue.push(fn); + return this; + }, + next: function() { + this.cursor++; + var fn = this.queue[this.cursor]; + typeof fn === 'function' && fn(NexT.motion.integrator); + }, + bootstrap: function() { + this.next(); + } +}; + +NexT.motion.middleWares = { + logo: function(integrator) { + var sequence = []; + var brand = document.querySelector('.brand'); + var image = document.querySelector('.custom-logo-image'); + var title = document.querySelector('.site-title'); + var subtitle = document.querySelector('.site-subtitle'); + var logoLineTop = document.querySelector('.logo-line-before i'); + var logoLineBottom = document.querySelector('.logo-line-after i'); + + brand && sequence.push({ + e: brand, + p: {opacity: 1}, + o: {duration: 200} + }); + + function getMistLineSettings(element, translateX) { + return { + e: element, + p: {translateX}, + o: { + duration : 500, + sequenceQueue: false + } + }; + } + + function pushImageToSequence() { + sequence.push({ + e: image, + p: {opacity: 1, top: 0}, + o: {duration: 200} + }); + } + + CONFIG.scheme === 'Mist' && logoLineTop && logoLineBottom + && sequence.push( + getMistLineSettings(logoLineTop, '100%'), + getMistLineSettings(logoLineBottom, '-100%') + ); + + CONFIG.scheme === 'Muse' && image && pushImageToSequence(); + + title && sequence.push({ + e: title, + p: {opacity: 1, top: 0}, + o: {duration: 200} + }); + + subtitle && sequence.push({ + e: subtitle, + p: {opacity: 1, top: 0}, + o: {duration: 200} + }); + + (CONFIG.scheme === 'Pisces' || CONFIG.scheme === 'Gemini') && image && pushImageToSequence(); + + if (sequence.length > 0) { + sequence[sequence.length - 1].o.complete = function() { + integrator.next(); + }; + Velocity.RunSequence(sequence); + } else { + integrator.next(); + } + + if (CONFIG.motion.async) { + integrator.next(); + } + }, + + menu: function(integrator) { + Velocity(document.querySelectorAll('.menu-item'), 'transition.slideDownIn', { + display : null, + duration: 200, + complete: function() { + integrator.next(); + } + }); + + if (CONFIG.motion.async) { + integrator.next(); + } + }, + + subMenu: function(integrator) { + var subMenuItem = document.querySelectorAll('.sub-menu .menu-item'); + if (subMenuItem.length > 0) { + subMenuItem.forEach(element => { + element.style.opacity = 1; + }); + } + integrator.next(); + }, + + postList: function(integrator) { + var postBlock = document.querySelectorAll('.post-block, .pagination, .comments'); + var postBlockTransition = CONFIG.motion.transition.post_block; + var postHeader = document.querySelectorAll('.post-header'); + var postHeaderTransition = CONFIG.motion.transition.post_header; + var postBody = document.querySelectorAll('.post-body'); + var postBodyTransition = CONFIG.motion.transition.post_body; + var collHeader = document.querySelectorAll('.collection-header'); + var collHeaderTransition = CONFIG.motion.transition.coll_header; + + if (postBlock.length > 0) { + var postMotionOptions = window.postMotionOptions || { + stagger : 100, + drag : true, + complete: function() { + integrator.next(); + } + }; + + if (CONFIG.motion.transition.post_block) { + Velocity(postBlock, 'transition.' + postBlockTransition, postMotionOptions); + } + if (CONFIG.motion.transition.post_header) { + Velocity(postHeader, 'transition.' + postHeaderTransition, postMotionOptions); + } + if (CONFIG.motion.transition.post_body) { + Velocity(postBody, 'transition.' + postBodyTransition, postMotionOptions); + } + if (CONFIG.motion.transition.coll_header) { + Velocity(collHeader, 'transition.' + collHeaderTransition, postMotionOptions); + } + } + if (CONFIG.scheme === 'Pisces' || CONFIG.scheme === 'Gemini') { + integrator.next(); + } + }, + + sidebar: function(integrator) { + var sidebarAffix = document.querySelector('.sidebar-inner'); + var sidebarAffixTransition = CONFIG.motion.transition.sidebar; + // Only for Pisces | Gemini. + if (sidebarAffixTransition && (CONFIG.scheme === 'Pisces' || CONFIG.scheme === 'Gemini')) { + Velocity(sidebarAffix, 'transition.' + sidebarAffixTransition, { + display : null, + duration: 200, + complete: function() { + // After motion complete need to remove transform from sidebar to let affix work on Pisces | Gemini. + sidebarAffix.style.transform = 'initial'; + } + }); + } + integrator.next(); + } +}; diff --git a/js/next-boot.js b/js/next-boot.js new file mode 100644 index 0000000000..383b2c305a --- /dev/null +++ b/js/next-boot.js @@ -0,0 +1,114 @@ +/* global NexT, CONFIG, Velocity */ + +NexT.boot = {}; + +NexT.boot.registerEvents = function() { + + NexT.utils.registerScrollPercent(); + NexT.utils.registerCanIUseTag(); + + // Mobile top menu bar. + document.querySelector('.site-nav-toggle .toggle').addEventListener('click', () => { + event.currentTarget.classList.toggle('toggle-close'); + var siteNav = document.querySelector('.site-nav'); + var animateAction = siteNav.classList.contains('site-nav-on') ? 'slideUp' : 'slideDown'; + + if (typeof Velocity === 'function') { + Velocity(siteNav, animateAction, { + duration: 200, + complete: function() { + siteNav.classList.toggle('site-nav-on'); + } + }); + } else { + siteNav.classList.toggle('site-nav-on'); + } + }); + + var TAB_ANIMATE_DURATION = 200; + document.querySelectorAll('.sidebar-nav li').forEach((element, index) => { + element.addEventListener('click', event => { + var item = event.currentTarget; + var activeTabClassName = 'sidebar-nav-active'; + var activePanelClassName = 'sidebar-panel-active'; + if (item.classList.contains(activeTabClassName)) return; + + var targets = document.querySelectorAll('.sidebar-panel'); + var target = targets[index]; + var currentTarget = targets[1 - index]; + window.anime({ + targets : currentTarget, + duration: TAB_ANIMATE_DURATION, + easing : 'linear', + opacity : 0, + complete: () => { + // Prevent adding TOC to Overview if Overview was selected when close & open sidebar. + currentTarget.classList.remove(activePanelClassName); + target.style.opacity = 0; + target.classList.add(activePanelClassName); + window.anime({ + targets : target, + duration: TAB_ANIMATE_DURATION, + easing : 'linear', + opacity : 1 + }); + } + }); + + [...item.parentNode.children].forEach(element => { + element.classList.remove(activeTabClassName); + }); + item.classList.add(activeTabClassName); + }); + }); + + window.addEventListener('resize', NexT.utils.initSidebarDimension); + + window.addEventListener('hashchange', () => { + var tHash = location.hash; + if (tHash !== '' && !tHash.match(/%\S{2}/)) { + var target = document.querySelector(`.tabs ul.nav-tabs li a[href="${tHash}"]`); + target && target.click(); + } + }); +}; + +NexT.boot.refresh = function() { + + /** + * Register JS handlers by condition option. + * Need to add config option in Front-End at 'layout/_partials/head.swig' file. + */ + CONFIG.fancybox && NexT.utils.wrapImageWithFancyBox(); + CONFIG.mediumzoom && window.mediumZoom('.post-body :not(a) > img, .post-body > img'); + CONFIG.lazyload && window.lozad('.post-body img').observe(); + CONFIG.pangu && window.pangu.spacingPage(); + + CONFIG.exturl && NexT.utils.registerExtURL(); + CONFIG.copycode.enable && NexT.utils.registerCopyCode(); + NexT.utils.registerTabsTag(); + NexT.utils.registerActiveMenuItem(); + NexT.utils.registerLangSelect(); + NexT.utils.registerSidebarTOC(); + NexT.utils.wrapTableWithBox(); + NexT.utils.registerVideoIframe(); +}; + +NexT.boot.motion = function() { + // Define Motion Sequence & Bootstrap Motion. + if (CONFIG.motion.enable) { + NexT.motion.integrator + .add(NexT.motion.middleWares.logo) + .add(NexT.motion.middleWares.menu) + .add(NexT.motion.middleWares.postList) + .add(NexT.motion.middleWares.sidebar) + .bootstrap(); + } + NexT.utils.updateSidebarPosition(); +}; + +window.addEventListener('DOMContentLoaded', () => { + NexT.boot.registerEvents(); + NexT.boot.refresh(); + NexT.boot.motion(); +}); diff --git a/js/schemes/muse.js b/js/schemes/muse.js new file mode 100644 index 0000000000..87f1a442a0 --- /dev/null +++ b/js/schemes/muse.js @@ -0,0 +1,113 @@ +/* global NexT, CONFIG, Velocity */ + +window.addEventListener('DOMContentLoaded', () => { + + var isRight = CONFIG.sidebar.position === 'right'; + var SIDEBAR_WIDTH = CONFIG.sidebar.width || 320; + var SIDEBAR_DISPLAY_DURATION = 200; + var mousePos = {}; + + var sidebarToggleLines = { + lines: document.querySelector('.sidebar-toggle'), + init : function() { + this.lines.classList.remove('toggle-arrow', 'toggle-close'); + }, + arrow: function() { + this.lines.classList.remove('toggle-close'); + this.lines.classList.add('toggle-arrow'); + }, + close: function() { + this.lines.classList.remove('toggle-arrow'); + this.lines.classList.add('toggle-close'); + } + }; + + var sidebarToggleMotion = { + sidebarEl : document.querySelector('.sidebar'), + isSidebarVisible: false, + init : function() { + sidebarToggleLines.init(); + + window.addEventListener('mousedown', this.mousedownHandler.bind(this)); + window.addEventListener('mouseup', this.mouseupHandler.bind(this)); + document.querySelector('#sidebar-dimmer').addEventListener('click', this.clickHandler.bind(this)); + document.querySelector('.sidebar-toggle').addEventListener('click', this.clickHandler.bind(this)); + document.querySelector('.sidebar-toggle').addEventListener('mouseenter', this.mouseEnterHandler.bind(this)); + document.querySelector('.sidebar-toggle').addEventListener('mouseleave', this.mouseLeaveHandler.bind(this)); + window.addEventListener('sidebar:show', this.showSidebar.bind(this)); + window.addEventListener('sidebar:hide', this.hideSidebar.bind(this)); + }, + mousedownHandler: function(event) { + mousePos.X = event.pageX; + mousePos.Y = event.pageY; + }, + mouseupHandler: function(event) { + var deltaX = event.pageX - mousePos.X; + var deltaY = event.pageY - mousePos.Y; + var clickingBlankPart = Math.sqrt((deltaX * deltaX) + (deltaY * deltaY)) < 20 && event.target.matches('.main'); + if (this.isSidebarVisible && (clickingBlankPart || event.target.matches('img.medium-zoom-image, .fancybox img'))) { + this.hideSidebar(); + } + }, + clickHandler: function() { + this.isSidebarVisible ? this.hideSidebar() : this.showSidebar(); + }, + mouseEnterHandler: function() { + if (!this.isSidebarVisible) { + sidebarToggleLines.arrow(); + } + }, + mouseLeaveHandler: function() { + if (!this.isSidebarVisible) { + sidebarToggleLines.init(); + } + }, + showSidebar: function() { + this.isSidebarVisible = true; + this.sidebarEl.classList.add('sidebar-active'); + if (typeof Velocity === 'function') { + Velocity(document.querySelectorAll('.sidebar .motion-element'), isRight ? 'transition.slideRightIn' : 'transition.slideLeftIn', { + stagger: 50, + drag : true + }); + } + + sidebarToggleLines.close(); + NexT.utils.isDesktop() && window.anime(Object.assign({ + targets : document.body, + duration: SIDEBAR_DISPLAY_DURATION, + easing : 'linear' + }, isRight ? { + 'padding-right': SIDEBAR_WIDTH + } : { + 'padding-left': SIDEBAR_WIDTH + })); + }, + hideSidebar: function() { + this.isSidebarVisible = false; + this.sidebarEl.classList.remove('sidebar-active'); + + sidebarToggleLines.init(); + NexT.utils.isDesktop() && window.anime(Object.assign({ + targets : document.body, + duration: SIDEBAR_DISPLAY_DURATION, + easing : 'linear' + }, isRight ? { + 'padding-right': 0 + } : { + 'padding-left': 0 + })); + } + }; + sidebarToggleMotion.init(); + + function updateFooterPosition() { + var footer = document.querySelector('.footer'); + var containerHeight = document.querySelector('.header').offsetHeight + document.querySelector('.main').offsetHeight + footer.offsetHeight; + footer.classList.toggle('footer-fixed', containerHeight <= window.innerHeight); + } + + updateFooterPosition(); + window.addEventListener('resize', updateFooterPosition); + window.addEventListener('scroll', updateFooterPosition); +}); diff --git a/js/schemes/pisces.js b/js/schemes/pisces.js new file mode 100644 index 0000000000..4f384c2200 --- /dev/null +++ b/js/schemes/pisces.js @@ -0,0 +1,86 @@ +/* global NexT, CONFIG */ + +var Affix = { + init: function(element, options) { + this.element = element; + this.offset = options || 0; + this.affixed = null; + this.unpin = null; + this.pinnedOffset = null; + this.checkPosition(); + window.addEventListener('scroll', this.checkPosition.bind(this)); + window.addEventListener('click', this.checkPositionWithEventLoop.bind(this)); + window.matchMedia('(min-width: 992px)').addListener(event => { + if (event.matches) { + this.offset = NexT.utils.getAffixParam(); + this.checkPosition(); + } + }); + }, + getState: function(scrollHeight, height, offsetTop, offsetBottom) { + let scrollTop = window.scrollY; + let targetHeight = window.innerHeight; + if (offsetTop != null && this.affixed === 'top') { + if (document.querySelector('.content-wrap').offsetHeight < offsetTop) return 'top'; + return scrollTop < offsetTop ? 'top' : false; + } + if (this.affixed === 'bottom') { + if (offsetTop != null) return this.unpin <= this.element.getBoundingClientRect().top ? false : 'bottom'; + return scrollTop + targetHeight <= scrollHeight - offsetBottom ? false : 'bottom'; + } + let initializing = this.affixed === null; + let colliderTop = initializing ? scrollTop : this.element.getBoundingClientRect().top + scrollTop; + let colliderHeight = initializing ? targetHeight : height; + if (offsetTop != null && scrollTop <= offsetTop) return 'top'; + if (offsetBottom != null && (colliderTop + colliderHeight >= scrollHeight - offsetBottom)) return 'bottom'; + return false; + }, + getPinnedOffset: function() { + if (this.pinnedOffset) return this.pinnedOffset; + this.element.classList.remove('affix-top', 'affix-bottom'); + this.element.classList.add('affix'); + return (this.pinnedOffset = this.element.getBoundingClientRect().top); + }, + checkPositionWithEventLoop() { + setTimeout(this.checkPosition.bind(this), 1); + }, + checkPosition: function() { + if (window.getComputedStyle(this.element).display === 'none') return; + let height = this.element.offsetHeight; + let { offset } = this; + let offsetTop = offset.top; + let offsetBottom = offset.bottom; + let { scrollHeight } = document.body; + let affix = this.getState(scrollHeight, height, offsetTop, offsetBottom); + if (this.affixed !== affix) { + if (this.unpin != null) this.element.style.top = ''; + let affixType = 'affix' + (affix ? '-' + affix : ''); + this.affixed = affix; + this.unpin = affix === 'bottom' ? this.getPinnedOffset() : null; + this.element.classList.remove('affix', 'affix-top', 'affix-bottom'); + this.element.classList.add(affixType); + } + if (affix === 'bottom') { + this.element.style.top = scrollHeight - height - offsetBottom + 'px'; + } + } +}; + +NexT.utils.getAffixParam = function() { + const sidebarOffset = CONFIG.sidebar.offset || 12; + + let headerOffset = document.querySelector('.header-inner').offsetHeight; + let footerOffset = document.querySelector('.footer').offsetHeight; + + document.querySelector('.sidebar').style.marginTop = headerOffset + sidebarOffset + 'px'; + + return { + top : headerOffset, + bottom: footerOffset + }; +}; + +window.addEventListener('DOMContentLoaded', () => { + + Affix.init(document.querySelector('.sidebar-inner'), NexT.utils.getAffixParam()); +}); diff --git a/js/utils.js b/js/utils.js new file mode 100644 index 0000000000..c272dbebc9 --- /dev/null +++ b/js/utils.js @@ -0,0 +1,413 @@ +/* global NexT, CONFIG */ + +HTMLElement.prototype.wrap = function(wrapper) { + this.parentNode.insertBefore(wrapper, this); + this.parentNode.removeChild(this); + wrapper.appendChild(this); +}; + +NexT.utils = { + + /** + * Wrap images with fancybox. + */ + wrapImageWithFancyBox: function() { + document.querySelectorAll('.post-body :not(a) > img, .post-body > img').forEach(element => { + var $image = $(element); + var imageLink = $image.attr('data-src') || $image.attr('src'); + var $imageWrapLink = $image.wrap(``).parent('a'); + if ($image.is('.post-gallery img')) { + $imageWrapLink.attr('data-fancybox', 'gallery').attr('rel', 'gallery'); + } else if ($image.is('.group-picture img')) { + $imageWrapLink.attr('data-fancybox', 'group').attr('rel', 'group'); + } else { + $imageWrapLink.attr('data-fancybox', 'default').attr('rel', 'default'); + } + + var imageTitle = $image.attr('title') || $image.attr('alt'); + if (imageTitle) { + $imageWrapLink.append(`

    ${imageTitle}

    `); + // Make sure img title tag will show correctly in fancybox + $imageWrapLink.attr('title', imageTitle).attr('data-caption', imageTitle); + } + }); + + $.fancybox.defaults.hash = false; + $('.fancybox').fancybox({ + loop : true, + helpers: { + overlay: { + locked: false + } + } + }); + }, + + registerExtURL: function() { + document.querySelectorAll('span.exturl').forEach(element => { + let link = document.createElement('a'); + // https://stackoverflow.com/questions/30106476/using-javascripts-atob-to-decode-base64-doesnt-properly-decode-utf-8-strings + link.href = decodeURIComponent(atob(element.dataset.url).split('').map(c => { + return '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2); + }).join('')); + link.rel = 'noopener external nofollow noreferrer'; + link.target = '_blank'; + link.className = element.className; + link.innerHTML = element.innerHTML; + element.parentNode.replaceChild(link, element); + }); + }, + + /** + * One-click copy code support. + */ + registerCopyCode: function() { + document.querySelectorAll('figure.highlight').forEach(element => { + const box = document.createElement('div'); + element.wrap(box); + box.classList.add('highlight-container'); + box.insertAdjacentHTML('beforeend', '
    '); + var button = element.parentNode.querySelector('.copy-btn'); + button.addEventListener('click', event => { + var target = event.currentTarget; + var code = [...target.parentNode.querySelectorAll('.code .line')].map(line => line.innerText).join('\n'); + var ta = document.createElement('textarea'); + ta.style.top = window.scrollY + 'px'; // Prevent page scrolling + ta.style.position = 'absolute'; + ta.style.opacity = '0'; + ta.readOnly = true; + ta.value = code; + document.body.append(ta); + const selection = document.getSelection(); + const selected = selection.rangeCount > 0 ? selection.getRangeAt(0) : false; + ta.select(); + ta.setSelectionRange(0, code.length); + ta.readOnly = false; + var result = document.execCommand('copy'); + if (CONFIG.copycode.show_result) { + target.querySelector('i').className = result ? 'fa fa-check' : 'fa fa-times'; + } + ta.blur(); // For iOS + target.blur(); + if (selected) { + selection.removeAllRanges(); + selection.addRange(selected); + } + document.body.removeChild(ta); + }); + button.addEventListener('mouseleave', event => { + setTimeout(() => { + event.target.querySelector('i').className = 'fa fa-clipboard'; + }, 300); + }); + }); + }, + + wrapTableWithBox: function() { + document.querySelectorAll('table').forEach(element => { + const box = document.createElement('div'); + box.className = 'table-container'; + element.wrap(box); + }); + }, + + registerVideoIframe: function() { + document.querySelectorAll('iframe').forEach(element => { + const supported = [ + 'www.youtube.com', + 'player.vimeo.com', + 'player.youku.com', + 'player.bilibili.com', + 'www.tudou.com' + ].some(host => element.src.includes(host)); + if (supported && !element.parentNode.matches('.video-container')) { + const box = document.createElement('div'); + box.className = 'video-container'; + element.wrap(box); + let width = Number(element.width); + let height = Number(element.height); + if (width && height) { + element.parentNode.style.paddingTop = (height / width * 100) + '%'; + } + } + }); + }, + + registerScrollPercent: function() { + var THRESHOLD = 50; + var backToTop = document.querySelector('.back-to-top'); + var readingProgressBar = document.querySelector('.reading-progress-bar'); + // For init back to top in sidebar if page was scrolled after page refresh. + window.addEventListener('scroll', () => { + if (backToTop || readingProgressBar) { + var docHeight = document.querySelector('.container').offsetHeight; + var winHeight = window.innerHeight; + var contentVisibilityHeight = docHeight > winHeight ? docHeight - winHeight : document.body.scrollHeight - winHeight; + var scrollPercent = Math.min(100 * window.scrollY / contentVisibilityHeight, 100); + if (backToTop) { + backToTop.classList.toggle('back-to-top-on', window.scrollY > THRESHOLD); + backToTop.querySelector('span').innerText = Math.round(scrollPercent) + '%'; + } + if (readingProgressBar) { + readingProgressBar.style.width = scrollPercent.toFixed(2) + '%'; + } + } + }); + + backToTop && backToTop.addEventListener('click', () => { + window.anime({ + targets : document.scrollingElement, + duration : 500, + easing : 'linear', + scrollTop: 0 + }); + }); + }, + + /** + * Tabs tag listener (without twitter bootstrap). + */ + registerTabsTag: function() { + // Binding `nav-tabs` & `tab-content` by real time permalink changing. + document.querySelectorAll('.tabs ul.nav-tabs .tab').forEach(element => { + element.addEventListener('click', event => { + event.preventDefault(); + var target = event.currentTarget; + // Prevent selected tab to select again. + if (!target.classList.contains('active')) { + // Add & Remove active class on `nav-tabs` & `tab-content`. + [...target.parentNode.children].forEach(element => { + element.classList.remove('active'); + }); + target.classList.add('active'); + var tActive = document.getElementById(target.querySelector('a').getAttribute('href').replace('#', '')); + [...tActive.parentNode.children].forEach(element => { + element.classList.remove('active'); + }); + tActive.classList.add('active'); + // Trigger event + tActive.dispatchEvent(new Event('tabs:click', { + bubbles: true + })); + } + }); + }); + + window.dispatchEvent(new Event('tabs:register')); + }, + + registerCanIUseTag: function() { + // Get responsive height passed from iframe. + window.addEventListener('message', ({ data }) => { + if ((typeof data === 'string') && data.includes('ciu_embed')) { + var featureID = data.split(':')[1]; + var height = data.split(':')[2]; + document.querySelector(`iframe[data-feature=${featureID}]`).style.height = parseInt(height, 10) + 5 + 'px'; + } + }, false); + }, + + registerActiveMenuItem: function() { + document.querySelectorAll('.menu-item').forEach(element => { + var target = element.querySelector('a[href]'); + if (!target) return; + var isSamePath = target.pathname === location.pathname || target.pathname === location.pathname.replace('index.html', ''); + var isSubPath = !CONFIG.root.startsWith(target.pathname) && location.pathname.startsWith(target.pathname); + element.classList.toggle('menu-item-active', target.hostname === location.hostname && (isSamePath || isSubPath)); + }); + }, + + registerLangSelect: function() { + let sel = document.querySelector('.lang-select'); + if (!sel) return; + sel.value = CONFIG.page.lang; + sel.addEventListener('change', () => { + let target = sel.options[sel.selectedIndex]; + document.querySelector('.lang-select-label span').innerText = target.text; + let url = target.dataset.href; + window.pjax ? window.pjax.loadUrl(url) : window.location.href = url; + }); + }, + + registerSidebarTOC: function() { + const navItems = document.querySelectorAll('.post-toc li'); + const sections = [...navItems].map(element => { + var link = element.querySelector('a.nav-link'); + // TOC item animation navigate. + link.addEventListener('click', event => { + event.preventDefault(); + var target = document.getElementById(event.currentTarget.getAttribute('href').replace('#', '')); + var offset = target.getBoundingClientRect().top + window.scrollY; + window.anime({ + targets : document.scrollingElement, + duration : 500, + easing : 'linear', + scrollTop: offset + 10 + }); + }); + return document.getElementById(link.getAttribute('href').replace('#', '')); + }); + + var tocElement = document.querySelector('.post-toc-wrap'); + function activateNavByIndex(target) { + if (target.classList.contains('active-current')) return; + + document.querySelectorAll('.post-toc .active').forEach(element => { + element.classList.remove('active', 'active-current'); + }); + target.classList.add('active', 'active-current'); + var parent = target.parentNode; + while (!parent.matches('.post-toc')) { + if (parent.matches('li')) parent.classList.add('active'); + parent = parent.parentNode; + } + // Scrolling to center active TOC element if TOC content is taller then viewport. + window.anime({ + targets : tocElement, + duration : 200, + easing : 'linear', + scrollTop: tocElement.scrollTop - (tocElement.offsetHeight / 2) + target.getBoundingClientRect().top - tocElement.getBoundingClientRect().top + }); + } + + function findIndex(entries) { + let index = 0; + let entry = entries[index]; + if (entry.boundingClientRect.top > 0) { + index = sections.indexOf(entry.target); + return index === 0 ? 0 : index - 1; + } + for (; index < entries.length; index++) { + if (entries[index].boundingClientRect.top <= 0) { + entry = entries[index]; + } else { + return sections.indexOf(entry.target); + } + } + return sections.indexOf(entry.target); + } + + function createIntersectionObserver(marginTop) { + marginTop = Math.floor(marginTop + 10000); + let intersectionObserver = new IntersectionObserver((entries, observe) => { + let scrollHeight = document.documentElement.scrollHeight + 100; + if (scrollHeight > marginTop) { + observe.disconnect(); + createIntersectionObserver(scrollHeight); + return; + } + let index = findIndex(entries); + activateNavByIndex(navItems[index]); + }, { + rootMargin: marginTop + 'px 0px -100% 0px', + threshold : 0 + }); + sections.forEach(element => { + element && intersectionObserver.observe(element); + }); + } + createIntersectionObserver(document.documentElement.scrollHeight); + }, + + hasMobileUA: function() { + let ua = navigator.userAgent; + let pa = /iPad|iPhone|Android|Opera Mini|BlackBerry|webOS|UCWEB|Blazer|PSP|IEMobile|Symbian/g; + return pa.test(ua); + }, + + isTablet: function() { + return window.screen.width < 992 && window.screen.width > 767 && this.hasMobileUA(); + }, + + isMobile: function() { + return window.screen.width < 767 && this.hasMobileUA(); + }, + + isDesktop: function() { + return !this.isTablet() && !this.isMobile(); + }, + + supportsPDFs: function() { + let ua = navigator.userAgent; + let isFirefoxWithPDFJS = ua.includes('irefox') && parseInt(ua.split('rv:')[1].split('.')[0], 10) > 18; + let supportsPdfMimeType = typeof navigator.mimeTypes['application/pdf'] !== 'undefined'; + let isIOS = /iphone|ipad|ipod/i.test(ua.toLowerCase()); + return isFirefoxWithPDFJS || (supportsPdfMimeType && !isIOS); + }, + + /** + * Init Sidebar & TOC inner dimensions on all pages and for all schemes. + * Need for Sidebar/TOC inner scrolling if content taller then viewport. + */ + initSidebarDimension: function() { + var sidebarNav = document.querySelector('.sidebar-nav'); + var sidebarNavHeight = sidebarNav.style.display !== 'none' ? sidebarNav.offsetHeight : 0; + var sidebarOffset = CONFIG.sidebar.offset || 12; + var sidebarb2tHeight = CONFIG.back2top.enable && CONFIG.back2top.sidebar ? document.querySelector('.back-to-top').offsetHeight : 0; + var sidebarSchemePadding = (CONFIG.sidebar.padding * 2) + sidebarNavHeight + sidebarb2tHeight; + // Margin of sidebar b2t: -4px -10px -18px, brings a different of 22px. + if (CONFIG.scheme === 'Pisces' || CONFIG.scheme === 'Gemini') sidebarSchemePadding += (sidebarOffset * 2) - 22; + // Initialize Sidebar & TOC Height. + var sidebarWrapperHeight = document.body.offsetHeight - sidebarSchemePadding + 'px'; + document.querySelector('.site-overview-wrap').style.maxHeight = sidebarWrapperHeight; + document.querySelector('.post-toc-wrap').style.maxHeight = sidebarWrapperHeight; + }, + + updateSidebarPosition: function() { + var sidebarNav = document.querySelector('.sidebar-nav'); + var hasTOC = document.querySelector('.post-toc'); + if (hasTOC) { + sidebarNav.style.display = ''; + sidebarNav.classList.add('motion-element'); + document.querySelector('.sidebar-nav-toc').click(); + } else { + sidebarNav.style.display = 'none'; + sidebarNav.classList.remove('motion-element'); + document.querySelector('.sidebar-nav-overview').click(); + } + NexT.utils.initSidebarDimension(); + if (!this.isDesktop() || CONFIG.scheme === 'Pisces' || CONFIG.scheme === 'Gemini') return; + // Expand sidebar on post detail page by default, when post has a toc. + var display = CONFIG.page.sidebar; + if (typeof display !== 'boolean') { + // There's no definition sidebar in the page front-matter. + display = CONFIG.sidebar.display === 'always' || (CONFIG.sidebar.display === 'post' && hasTOC); + } + if (display) { + window.dispatchEvent(new Event('sidebar:show')); + } + }, + + getScript: function(url, callback, condition) { + if (condition) { + callback(); + } else { + var script = document.createElement('script'); + script.onload = script.onreadystatechange = function(_, isAbort) { + if (isAbort || !script.readyState || /loaded|complete/.test(script.readyState)) { + script.onload = script.onreadystatechange = null; + script = undefined; + if (!isAbort && callback) setTimeout(callback, 0); + } + }; + script.src = url; + document.head.appendChild(script); + } + }, + + loadComments: function(element, callback) { + if (!CONFIG.comments.lazyload || !element) { + callback(); + return; + } + let intersectionObserver = new IntersectionObserver((entries, observer) => { + let entry = entries[0]; + if (entry.isIntersecting) { + callback(); + observer.disconnect(); + } + }); + intersectionObserver.observe(element); + return intersectionObserver; + } +}; diff --git a/lib/anime.min.js b/lib/anime.min.js new file mode 100644 index 0000000000..99b263aaeb --- /dev/null +++ b/lib/anime.min.js @@ -0,0 +1,8 @@ +/* + * anime.js v3.1.0 + * (c) 2019 Julian Garnier + * Released under the MIT license + * animejs.com + */ + +!function(n,e){"object"==typeof exports&&"undefined"!=typeof module?module.exports=e():"function"==typeof define&&define.amd?define(e):n.anime=e()}(this,function(){"use strict";var n={update:null,begin:null,loopBegin:null,changeBegin:null,change:null,changeComplete:null,loopComplete:null,complete:null,loop:1,direction:"normal",autoplay:!0,timelineOffset:0},e={duration:1e3,delay:0,endDelay:0,easing:"easeOutElastic(1, .5)",round:0},r=["translateX","translateY","translateZ","rotate","rotateX","rotateY","rotateZ","scale","scaleX","scaleY","scaleZ","skew","skewX","skewY","perspective"],t={CSS:{},springs:{}};function a(n,e,r){return Math.min(Math.max(n,e),r)}function o(n,e){return n.indexOf(e)>-1}function u(n,e){return n.apply(null,e)}var i={arr:function(n){return Array.isArray(n)},obj:function(n){return o(Object.prototype.toString.call(n),"Object")},pth:function(n){return i.obj(n)&&n.hasOwnProperty("totalLength")},svg:function(n){return n instanceof SVGElement},inp:function(n){return n instanceof HTMLInputElement},dom:function(n){return n.nodeType||i.svg(n)},str:function(n){return"string"==typeof n},fnc:function(n){return"function"==typeof n},und:function(n){return void 0===n},hex:function(n){return/(^#[0-9A-F]{6}$)|(^#[0-9A-F]{3}$)/i.test(n)},rgb:function(n){return/^rgb/.test(n)},hsl:function(n){return/^hsl/.test(n)},col:function(n){return i.hex(n)||i.rgb(n)||i.hsl(n)},key:function(r){return!n.hasOwnProperty(r)&&!e.hasOwnProperty(r)&&"targets"!==r&&"keyframes"!==r}};function c(n){var e=/\(([^)]+)\)/.exec(n);return e?e[1].split(",").map(function(n){return parseFloat(n)}):[]}function s(n,e){var r=c(n),o=a(i.und(r[0])?1:r[0],.1,100),u=a(i.und(r[1])?100:r[1],.1,100),s=a(i.und(r[2])?10:r[2],.1,100),f=a(i.und(r[3])?0:r[3],.1,100),l=Math.sqrt(u/o),d=s/(2*Math.sqrt(u*o)),p=d<1?l*Math.sqrt(1-d*d):0,h=1,v=d<1?(d*l-f)/p:-f+l;function g(n){var r=e?e*n/1e3:n;return r=d<1?Math.exp(-r*d*l)*(h*Math.cos(p*r)+v*Math.sin(p*r)):(h+v*r)*Math.exp(-r*l),0===n||1===n?n:1-r}return e?g:function(){var e=t.springs[n];if(e)return e;for(var r=0,a=0;;)if(1===g(r+=1/6)){if(++a>=16)break}else a=0;var o=r*(1/6)*1e3;return t.springs[n]=o,o}}function f(n){return void 0===n&&(n=10),function(e){return Math.round(e*n)*(1/n)}}var l,d,p=function(){var n=11,e=1/(n-1);function r(n,e){return 1-3*e+3*n}function t(n,e){return 3*e-6*n}function a(n){return 3*n}function o(n,e,o){return((r(e,o)*n+t(e,o))*n+a(e))*n}function u(n,e,o){return 3*r(e,o)*n*n+2*t(e,o)*n+a(e)}return function(r,t,a,i){if(0<=r&&r<=1&&0<=a&&a<=1){var c=new Float32Array(n);if(r!==t||a!==i)for(var s=0;s=.001?function(n,e,r,t){for(var a=0;a<4;++a){var i=u(e,r,t);if(0===i)return e;e-=(o(e,r,t)-n)/i}return e}(t,l,r,a):0===d?l:function(n,e,r,t,a){for(var u,i,c=0;(u=o(i=e+(r-e)/2,t,a)-n)>0?r=i:e=i,Math.abs(u)>1e-7&&++c<10;);return i}(t,i,i+e,r,a)}}}(),h=(l={linear:function(){return function(n){return n}}},d={Sine:function(){return function(n){return 1-Math.cos(n*Math.PI/2)}},Circ:function(){return function(n){return 1-Math.sqrt(1-n*n)}},Back:function(){return function(n){return n*n*(3*n-2)}},Bounce:function(){return function(n){for(var e,r=4;n<((e=Math.pow(2,--r))-1)/11;);return 1/Math.pow(4,3-r)-7.5625*Math.pow((3*e-2)/22-n,2)}},Elastic:function(n,e){void 0===n&&(n=1),void 0===e&&(e=.5);var r=a(n,1,10),t=a(e,.1,2);return function(n){return 0===n||1===n?n:-r*Math.pow(2,10*(n-1))*Math.sin((n-1-t/(2*Math.PI)*Math.asin(1/r))*(2*Math.PI)/t)}}},["Quad","Cubic","Quart","Quint","Expo"].forEach(function(n,e){d[n]=function(){return function(n){return Math.pow(n,e+2)}}}),Object.keys(d).forEach(function(n){var e=d[n];l["easeIn"+n]=e,l["easeOut"+n]=function(n,r){return function(t){return 1-e(n,r)(1-t)}},l["easeInOut"+n]=function(n,r){return function(t){return t<.5?e(n,r)(2*t)/2:1-e(n,r)(-2*t+2)/2}}}),l);function v(n,e){if(i.fnc(n))return n;var r=n.split("(")[0],t=h[r],a=c(n);switch(r){case"spring":return s(n,e);case"cubicBezier":return u(p,a);case"steps":return u(f,a);default:return u(t,a)}}function g(n){try{return document.querySelectorAll(n)}catch(n){return}}function m(n,e){for(var r=n.length,t=arguments.length>=2?arguments[1]:void 0,a=[],o=0;o1&&(r-=1),r<1/6?n+6*(e-n)*r:r<.5?e:r<2/3?n+(e-n)*(2/3-r)*6:n}if(0==u)e=r=t=i;else{var f=i<.5?i*(1+u):i+u-i*u,l=2*i-f;e=s(l,f,o+1/3),r=s(l,f,o),t=s(l,f,o-1/3)}return"rgba("+255*e+","+255*r+","+255*t+","+c+")"}(n):void 0;var e,r,t,a}function C(n){var e=/[+-]?\d*\.?\d+(?:\.\d+)?(?:[eE][+-]?\d+)?(%|px|pt|em|rem|in|cm|mm|ex|ch|pc|vw|vh|vmin|vmax|deg|rad|turn)?$/.exec(n);if(e)return e[1]}function B(n,e){return i.fnc(n)?n(e.target,e.id,e.total):n}function P(n,e){return n.getAttribute(e)}function I(n,e,r){if(M([r,"deg","rad","turn"],C(e)))return e;var a=t.CSS[e+r];if(!i.und(a))return a;var o=document.createElement(n.tagName),u=n.parentNode&&n.parentNode!==document?n.parentNode:document.body;u.appendChild(o),o.style.position="absolute",o.style.width=100+r;var c=100/o.offsetWidth;u.removeChild(o);var s=c*parseFloat(e);return t.CSS[e+r]=s,s}function T(n,e,r){if(e in n.style){var t=e.replace(/([a-z])([A-Z])/g,"$1-$2").toLowerCase(),a=n.style[e]||getComputedStyle(n).getPropertyValue(t)||"0";return r?I(n,a,r):a}}function D(n,e){return i.dom(n)&&!i.inp(n)&&(P(n,e)||i.svg(n)&&n[e])?"attribute":i.dom(n)&&M(r,e)?"transform":i.dom(n)&&"transform"!==e&&T(n,e)?"css":null!=n[e]?"object":void 0}function E(n){if(i.dom(n)){for(var e,r=n.style.transform||"",t=/(\w+)\(([^)]*)\)/g,a=new Map;e=t.exec(r);)a.set(e[1],e[2]);return a}}function F(n,e,r,t){var a,u=o(e,"scale")?1:0+(o(a=e,"translate")||"perspective"===a?"px":o(a,"rotate")||o(a,"skew")?"deg":void 0),i=E(n).get(e)||u;return r&&(r.transforms.list.set(e,i),r.transforms.last=e),t?I(n,i,t):i}function N(n,e,r,t){switch(D(n,e)){case"transform":return F(n,e,t,r);case"css":return T(n,e,r);case"attribute":return P(n,e);default:return n[e]||0}}function A(n,e){var r=/^(\*=|\+=|-=)/.exec(n);if(!r)return n;var t=C(n)||0,a=parseFloat(e),o=parseFloat(n.replace(r[0],""));switch(r[0][0]){case"+":return a+o+t;case"-":return a-o+t;case"*":return a*o+t}}function L(n,e){if(i.col(n))return O(n);if(/\s/g.test(n))return n;var r=C(n),t=r?n.substr(0,n.length-r.length):n;return e?t+e:t}function j(n,e){return Math.sqrt(Math.pow(e.x-n.x,2)+Math.pow(e.y-n.y,2))}function S(n){for(var e,r=n.points,t=0,a=0;a0&&(t+=j(e,o)),e=o}return t}function q(n){if(n.getTotalLength)return n.getTotalLength();switch(n.tagName.toLowerCase()){case"circle":return o=n,2*Math.PI*P(o,"r");case"rect":return 2*P(a=n,"width")+2*P(a,"height");case"line":return j({x:P(t=n,"x1"),y:P(t,"y1")},{x:P(t,"x2"),y:P(t,"y2")});case"polyline":return S(n);case"polygon":return r=(e=n).points,S(e)+j(r.getItem(r.numberOfItems-1),r.getItem(0))}var e,r,t,a,o}function $(n,e){var r=e||{},t=r.el||function(n){for(var e=n.parentNode;i.svg(e)&&i.svg(e.parentNode);)e=e.parentNode;return e}(n),a=t.getBoundingClientRect(),o=P(t,"viewBox"),u=a.width,c=a.height,s=r.viewBox||(o?o.split(" "):[0,0,u,c]);return{el:t,viewBox:s,x:s[0]/1,y:s[1]/1,w:u/s[2],h:c/s[3]}}function X(n,e){function r(r){void 0===r&&(r=0);var t=e+r>=1?e+r:0;return n.el.getPointAtLength(t)}var t=$(n.el,n.svg),a=r(),o=r(-1),u=r(1);switch(n.property){case"x":return(a.x-t.x)*t.w;case"y":return(a.y-t.y)*t.h;case"angle":return 180*Math.atan2(u.y-o.y,u.x-o.x)/Math.PI}}function Y(n,e){var r=/[+-]?\d*\.?\d+(?:\.\d+)?(?:[eE][+-]?\d+)?/g,t=L(i.pth(n)?n.totalLength:n,e)+"";return{original:t,numbers:t.match(r)?t.match(r).map(Number):[0],strings:i.str(n)||e?t.split(r):[]}}function Z(n){return m(n?y(i.arr(n)?n.map(b):b(n)):[],function(n,e,r){return r.indexOf(n)===e})}function Q(n){var e=Z(n);return e.map(function(n,r){return{target:n,id:r,total:e.length,transforms:{list:E(n)}}})}function V(n,e){var r=x(e);if(/^spring/.test(r.easing)&&(r.duration=s(r.easing)),i.arr(n)){var t=n.length;2===t&&!i.obj(n[0])?n={value:n}:i.fnc(e.duration)||(r.duration=e.duration/t)}var a=i.arr(n)?n:[n];return a.map(function(n,r){var t=i.obj(n)&&!i.pth(n)?n:{value:n};return i.und(t.delay)&&(t.delay=r?0:e.delay),i.und(t.endDelay)&&(t.endDelay=r===a.length-1?e.endDelay:0),t}).map(function(n){return k(n,r)})}function z(n,e){var r=[],t=e.keyframes;for(var a in t&&(e=k(function(n){for(var e=m(y(n.map(function(n){return Object.keys(n)})),function(n){return i.key(n)}).reduce(function(n,e){return n.indexOf(e)<0&&n.push(e),n},[]),r={},t=function(t){var a=e[t];r[a]=n.map(function(n){var e={};for(var r in n)i.key(r)?r==a&&(e.value=n[r]):e[r]=n[r];return e})},a=0;a-1&&(_.splice(o,1),r=_.length)}else a.tick(e);t++}n()}else U=cancelAnimationFrame(U)}return n}();function rn(r){void 0===r&&(r={});var t,o=0,u=0,i=0,c=0,s=null;function f(n){var e=window.Promise&&new Promise(function(n){return s=n});return n.finished=e,e}var l,d,p,h,v,g,y,b,M=(d=w(n,l=r),p=w(e,l),h=z(p,l),v=Q(l.targets),g=W(v,h),y=J(g,p),b=K,K++,k(d,{id:b,children:[],animatables:v,animations:g,duration:y.duration,delay:y.delay,endDelay:y.endDelay}));f(M);function x(){var n=M.direction;"alternate"!==n&&(M.direction="normal"!==n?"normal":"reverse"),M.reversed=!M.reversed,t.forEach(function(n){return n.reversed=M.reversed})}function O(n){return M.reversed?M.duration-n:n}function C(){o=0,u=O(M.currentTime)*(1/rn.speed)}function B(n,e){e&&e.seek(n-e.timelineOffset)}function P(n){for(var e=0,r=M.animations,t=r.length;e2||(b=Math.round(b*p)/p)),h.push(b)}var k=d.length;if(k){g=d[0];for(var O=0;O0&&(M.began=!0,I("begin")),!M.loopBegan&&M.currentTime>0&&(M.loopBegan=!0,I("loopBegin")),d<=r&&0!==M.currentTime&&P(0),(d>=l&&M.currentTime!==e||!e)&&P(e),d>r&&d=e&&(u=0,M.remaining&&!0!==M.remaining&&M.remaining--,M.remaining?(o=i,I("loopComplete"),M.loopBegan=!1,"alternate"===M.direction&&x()):(M.paused=!0,M.completed||(M.completed=!0,I("loopComplete"),I("complete"),!M.passThrough&&"Promise"in window&&(s(),f(M)))))}return M.reset=function(){var n=M.direction;M.passThrough=!1,M.currentTime=0,M.progress=0,M.paused=!0,M.began=!1,M.loopBegan=!1,M.changeBegan=!1,M.completed=!1,M.changeCompleted=!1,M.reversePlayback=!1,M.reversed="reverse"===n,M.remaining=M.loop,t=M.children;for(var e=c=t.length;e--;)M.children[e].reset();(M.reversed&&!0!==M.loop||"alternate"===n&&1===M.loop)&&M.remaining++,P(M.reversed?M.duration:0)},M.set=function(n,e){return R(n,e),M},M.tick=function(n){i=n,o||(o=i),T((i+(u-o))*rn.speed)},M.seek=function(n){T(O(n))},M.pause=function(){M.paused=!0,C()},M.play=function(){M.paused&&(M.completed&&M.reset(),M.paused=!1,_.push(M),C(),U||en())},M.reverse=function(){x(),C()},M.restart=function(){M.reset(),M.play()},M.reset(),M.autoplay&&M.play(),M}function tn(n,e){for(var r=e.length;r--;)M(n,e[r].animatable.target)&&e.splice(r,1)}return"undefined"!=typeof document&&document.addEventListener("visibilitychange",function(){document.hidden?(_.forEach(function(n){return n.pause()}),nn=_.slice(0),rn.running=_=[]):nn.forEach(function(n){return n.play()})}),rn.version="3.1.0",rn.speed=1,rn.running=_,rn.remove=function(n){for(var e=Z(n),r=_.length;r--;){var t=_[r],a=t.animations,o=t.children;tn(e,a);for(var u=o.length;u--;){var i=o[u],c=i.animations;tn(e,c),c.length||i.children.length||o.splice(u,1)}a.length||o.length||t.pause()}},rn.get=N,rn.set=R,rn.convertPx=I,rn.path=function(n,e){var r=i.str(n)?g(n)[0]:n,t=e||100;return function(n){return{property:n,el:r,svg:$(r),totalLength:q(r)*(t/100)}}},rn.setDashoffset=function(n){var e=q(n);return n.setAttribute("stroke-dasharray",e),e},rn.stagger=function(n,e){void 0===e&&(e={});var r=e.direction||"normal",t=e.easing?v(e.easing):null,a=e.grid,o=e.axis,u=e.from||0,c="first"===u,s="center"===u,f="last"===u,l=i.arr(n),d=l?parseFloat(n[0]):parseFloat(n),p=l?parseFloat(n[1]):0,h=C(l?n[1]:n)||0,g=e.start||0+(l?d:0),m=[],y=0;return function(n,e,i){if(c&&(u=0),s&&(u=(i-1)/2),f&&(u=i-1),!m.length){for(var v=0;v-1&&_.splice(o,1);for(var s=0;s li { + position: relative; +} +.fa-li { + position: absolute; + left: -2.14285714em; + width: 2.14285714em; + top: 0.14285714em; + text-align: center; +} +.fa-li.fa-lg { + left: -1.85714286em; +} +.fa-border { + padding: .2em .25em .15em; + border: solid 0.08em #eeeeee; + border-radius: .1em; +} +.fa-pull-left { + float: left; +} +.fa-pull-right { + float: right; +} +.fa.fa-pull-left { + margin-right: .3em; +} +.fa.fa-pull-right { + margin-left: .3em; +} +/* Deprecated as of 4.4.0 */ +.pull-right { + float: right; +} +.pull-left { + float: left; +} +.fa.pull-left { + margin-right: .3em; +} +.fa.pull-right { + margin-left: .3em; +} +.fa-spin { + -webkit-animation: fa-spin 2s infinite linear; + animation: fa-spin 2s infinite linear; +} +.fa-pulse { + -webkit-animation: fa-spin 1s infinite steps(8); + animation: fa-spin 1s infinite steps(8); +} +@-webkit-keyframes fa-spin { + 0% { + -webkit-transform: rotate(0deg); + transform: rotate(0deg); + } + 100% { + -webkit-transform: rotate(359deg); + transform: rotate(359deg); + } +} +@keyframes fa-spin { + 0% { + -webkit-transform: rotate(0deg); + transform: rotate(0deg); + } + 100% { + -webkit-transform: rotate(359deg); + transform: rotate(359deg); + } +} +.fa-rotate-90 { + -ms-filter: "progid:DXImageTransform.Microsoft.BasicImage(rotation=1)"; + -webkit-transform: rotate(90deg); + -ms-transform: rotate(90deg); + transform: rotate(90deg); +} +.fa-rotate-180 { + -ms-filter: "progid:DXImageTransform.Microsoft.BasicImage(rotation=2)"; + -webkit-transform: rotate(180deg); + -ms-transform: rotate(180deg); + transform: rotate(180deg); +} +.fa-rotate-270 { + -ms-filter: "progid:DXImageTransform.Microsoft.BasicImage(rotation=3)"; + -webkit-transform: rotate(270deg); + -ms-transform: rotate(270deg); + transform: rotate(270deg); +} +.fa-flip-horizontal { + -ms-filter: "progid:DXImageTransform.Microsoft.BasicImage(rotation=0, mirror=1)"; + -webkit-transform: scale(-1, 1); + -ms-transform: scale(-1, 1); + transform: scale(-1, 1); +} +.fa-flip-vertical { + -ms-filter: "progid:DXImageTransform.Microsoft.BasicImage(rotation=2, mirror=1)"; + -webkit-transform: scale(1, -1); + -ms-transform: scale(1, -1); + transform: scale(1, -1); +} +:root .fa-rotate-90, +:root .fa-rotate-180, +:root .fa-rotate-270, +:root .fa-flip-horizontal, +:root .fa-flip-vertical { + filter: none; +} +.fa-stack { + position: relative; + display: inline-block; + width: 2em; + height: 2em; + line-height: 2em; + vertical-align: middle; +} +.fa-stack-1x, +.fa-stack-2x { + position: absolute; + left: 0; + width: 100%; + text-align: center; +} +.fa-stack-1x { + line-height: inherit; +} +.fa-stack-2x { + font-size: 2em; +} +.fa-inverse { + color: #ffffff; +} +/* Font Awesome uses the Unicode Private Use Area (PUA) to ensure screen + readers do not read off random characters that represent icons */ +.fa-glass:before { + content: "\f000"; +} +.fa-music:before { + content: "\f001"; +} +.fa-search:before { + content: "\f002"; +} +.fa-envelope-o:before { + content: "\f003"; +} +.fa-heart:before { + content: "\f004"; +} +.fa-star:before { + content: "\f005"; +} +.fa-star-o:before { + content: "\f006"; +} +.fa-user:before { + content: "\f007"; +} +.fa-film:before { + content: "\f008"; +} +.fa-th-large:before { + content: "\f009"; +} +.fa-th:before { + content: "\f00a"; +} +.fa-th-list:before { + content: "\f00b"; +} +.fa-check:before { + content: "\f00c"; +} +.fa-remove:before, +.fa-close:before, +.fa-times:before { + content: "\f00d"; +} +.fa-search-plus:before { + content: "\f00e"; +} +.fa-search-minus:before { + content: "\f010"; +} +.fa-power-off:before { + content: "\f011"; +} +.fa-signal:before { + content: "\f012"; +} +.fa-gear:before, +.fa-cog:before { + content: "\f013"; +} +.fa-trash-o:before { + content: "\f014"; +} +.fa-home:before { + content: "\f015"; +} +.fa-file-o:before { + content: "\f016"; +} +.fa-clock-o:before { + content: "\f017"; +} +.fa-road:before { + content: "\f018"; +} +.fa-download:before { + content: "\f019"; +} +.fa-arrow-circle-o-down:before { + content: "\f01a"; +} +.fa-arrow-circle-o-up:before { + content: "\f01b"; +} +.fa-inbox:before { + content: "\f01c"; +} +.fa-play-circle-o:before { + content: "\f01d"; +} +.fa-rotate-right:before, +.fa-repeat:before { + content: "\f01e"; +} +.fa-refresh:before { + content: "\f021"; +} +.fa-list-alt:before { + content: "\f022"; +} +.fa-lock:before { + content: "\f023"; +} +.fa-flag:before { + content: "\f024"; +} +.fa-headphones:before { + content: "\f025"; +} +.fa-volume-off:before { + content: "\f026"; +} +.fa-volume-down:before { + content: "\f027"; +} +.fa-volume-up:before { + content: "\f028"; +} +.fa-qrcode:before { + content: "\f029"; +} +.fa-barcode:before { + content: "\f02a"; +} +.fa-tag:before { + content: "\f02b"; +} +.fa-tags:before { + content: "\f02c"; +} +.fa-book:before { + content: "\f02d"; +} +.fa-bookmark:before { + content: "\f02e"; +} +.fa-print:before { + content: "\f02f"; +} +.fa-camera:before { + content: "\f030"; +} +.fa-font:before { + content: "\f031"; +} +.fa-bold:before { + content: "\f032"; +} +.fa-italic:before { + content: "\f033"; +} +.fa-text-height:before { + content: "\f034"; +} +.fa-text-width:before { + content: "\f035"; +} +.fa-align-left:before { + content: "\f036"; +} +.fa-align-center:before { + content: "\f037"; +} +.fa-align-right:before { + content: "\f038"; +} +.fa-align-justify:before { + content: "\f039"; +} +.fa-list:before { + content: "\f03a"; +} +.fa-dedent:before, +.fa-outdent:before { + content: "\f03b"; +} +.fa-indent:before { + content: "\f03c"; +} +.fa-video-camera:before { + content: "\f03d"; +} +.fa-photo:before, +.fa-image:before, +.fa-picture-o:before { + content: "\f03e"; +} +.fa-pencil:before { + content: "\f040"; +} +.fa-map-marker:before { + content: "\f041"; +} +.fa-adjust:before { + content: "\f042"; +} +.fa-tint:before { + content: "\f043"; +} +.fa-edit:before, +.fa-pencil-square-o:before { + content: "\f044"; +} +.fa-share-square-o:before { + content: "\f045"; +} +.fa-check-square-o:before { + content: "\f046"; +} +.fa-arrows:before { + content: "\f047"; +} +.fa-step-backward:before { + content: "\f048"; +} +.fa-fast-backward:before { + content: "\f049"; +} +.fa-backward:before { + content: "\f04a"; +} +.fa-play:before { + content: "\f04b"; +} +.fa-pause:before { + content: "\f04c"; +} +.fa-stop:before { + content: "\f04d"; +} +.fa-forward:before { + content: "\f04e"; +} +.fa-fast-forward:before { + content: "\f050"; +} +.fa-step-forward:before { + content: "\f051"; +} +.fa-eject:before { + content: "\f052"; +} +.fa-chevron-left:before { + content: "\f053"; +} +.fa-chevron-right:before { + content: "\f054"; +} +.fa-plus-circle:before { + content: "\f055"; +} +.fa-minus-circle:before { + content: "\f056"; +} +.fa-times-circle:before { + content: "\f057"; +} +.fa-check-circle:before { + content: "\f058"; +} +.fa-question-circle:before { + content: "\f059"; +} +.fa-info-circle:before { + content: "\f05a"; +} +.fa-crosshairs:before { + content: "\f05b"; +} +.fa-times-circle-o:before { + content: "\f05c"; +} +.fa-check-circle-o:before { + content: "\f05d"; +} +.fa-ban:before { + content: "\f05e"; +} +.fa-arrow-left:before { + content: "\f060"; +} +.fa-arrow-right:before { + content: "\f061"; +} +.fa-arrow-up:before { + content: "\f062"; +} +.fa-arrow-down:before { + content: "\f063"; +} +.fa-mail-forward:before, +.fa-share:before { + content: "\f064"; +} +.fa-expand:before { + content: "\f065"; +} +.fa-compress:before { + content: "\f066"; +} +.fa-plus:before { + content: "\f067"; +} +.fa-minus:before { + content: "\f068"; +} +.fa-asterisk:before { + content: "\f069"; +} +.fa-exclamation-circle:before { + content: "\f06a"; +} +.fa-gift:before { + content: "\f06b"; +} +.fa-leaf:before { + content: "\f06c"; +} +.fa-fire:before { + content: "\f06d"; +} +.fa-eye:before { + content: "\f06e"; +} +.fa-eye-slash:before { + content: "\f070"; +} +.fa-warning:before, +.fa-exclamation-triangle:before { + content: "\f071"; +} +.fa-plane:before { + content: "\f072"; +} +.fa-calendar:before { + content: "\f073"; +} +.fa-random:before { + content: "\f074"; +} +.fa-comment:before { + content: "\f075"; +} +.fa-magnet:before { + content: "\f076"; +} +.fa-chevron-up:before { + content: "\f077"; +} +.fa-chevron-down:before { + content: "\f078"; +} +.fa-retweet:before { + content: "\f079"; +} +.fa-shopping-cart:before { + content: "\f07a"; +} +.fa-folder:before { + content: "\f07b"; +} +.fa-folder-open:before { + content: "\f07c"; +} +.fa-arrows-v:before { + content: "\f07d"; +} +.fa-arrows-h:before { + content: "\f07e"; +} +.fa-bar-chart-o:before, +.fa-bar-chart:before { + content: "\f080"; +} +.fa-twitter-square:before { + content: "\f081"; +} +.fa-facebook-square:before { + content: "\f082"; +} +.fa-camera-retro:before { + content: "\f083"; +} +.fa-key:before { + content: "\f084"; +} +.fa-gears:before, +.fa-cogs:before { + content: "\f085"; +} +.fa-comments:before { + content: "\f086"; +} +.fa-thumbs-o-up:before { + content: "\f087"; +} +.fa-thumbs-o-down:before { + content: "\f088"; +} +.fa-star-half:before { + content: "\f089"; +} +.fa-heart-o:before { + content: "\f08a"; +} +.fa-sign-out:before { + content: "\f08b"; +} +.fa-linkedin-square:before { + content: "\f08c"; +} +.fa-thumb-tack:before { + content: "\f08d"; +} +.fa-external-link:before { + content: "\f08e"; +} +.fa-sign-in:before { + content: "\f090"; +} +.fa-trophy:before { + content: "\f091"; +} +.fa-github-square:before { + content: "\f092"; +} +.fa-upload:before { + content: "\f093"; +} +.fa-lemon-o:before { + content: "\f094"; +} +.fa-phone:before { + content: "\f095"; +} +.fa-square-o:before { + content: "\f096"; +} +.fa-bookmark-o:before { + content: "\f097"; +} +.fa-phone-square:before { + content: "\f098"; +} +.fa-twitter:before { + content: "\f099"; +} +.fa-facebook-f:before, +.fa-facebook:before { + content: "\f09a"; +} +.fa-github:before { + content: "\f09b"; +} +.fa-unlock:before { + content: "\f09c"; +} +.fa-credit-card:before { + content: "\f09d"; +} +.fa-feed:before, +.fa-rss:before { + content: "\f09e"; +} +.fa-hdd-o:before { + content: "\f0a0"; +} +.fa-bullhorn:before { + content: "\f0a1"; +} +.fa-bell:before { + content: "\f0f3"; +} +.fa-certificate:before { + content: "\f0a3"; +} +.fa-hand-o-right:before { + content: "\f0a4"; +} +.fa-hand-o-left:before { + content: "\f0a5"; +} +.fa-hand-o-up:before { + content: "\f0a6"; +} +.fa-hand-o-down:before { + content: "\f0a7"; +} +.fa-arrow-circle-left:before { + content: "\f0a8"; +} +.fa-arrow-circle-right:before { + content: "\f0a9"; +} +.fa-arrow-circle-up:before { + content: "\f0aa"; +} +.fa-arrow-circle-down:before { + content: "\f0ab"; +} +.fa-globe:before { + content: "\f0ac"; +} +.fa-wrench:before { + content: "\f0ad"; +} +.fa-tasks:before { + content: "\f0ae"; +} +.fa-filter:before { + content: "\f0b0"; +} +.fa-briefcase:before { + content: "\f0b1"; +} +.fa-arrows-alt:before { + content: "\f0b2"; +} +.fa-group:before, +.fa-users:before { + content: "\f0c0"; +} +.fa-chain:before, +.fa-link:before { + content: "\f0c1"; +} +.fa-cloud:before { + content: "\f0c2"; +} +.fa-flask:before { + content: "\f0c3"; +} +.fa-cut:before, +.fa-scissors:before { + content: "\f0c4"; +} +.fa-copy:before, +.fa-files-o:before { + content: "\f0c5"; +} +.fa-paperclip:before { + content: "\f0c6"; +} +.fa-save:before, +.fa-floppy-o:before { + content: "\f0c7"; +} +.fa-square:before { + content: "\f0c8"; +} +.fa-navicon:before, +.fa-reorder:before, +.fa-bars:before { + content: "\f0c9"; +} +.fa-list-ul:before { + content: "\f0ca"; +} +.fa-list-ol:before { + content: "\f0cb"; +} +.fa-strikethrough:before { + content: "\f0cc"; +} +.fa-underline:before { + content: "\f0cd"; +} +.fa-table:before { + content: "\f0ce"; +} +.fa-magic:before { + content: "\f0d0"; +} +.fa-truck:before { + content: "\f0d1"; +} +.fa-pinterest:before { + content: "\f0d2"; +} +.fa-pinterest-square:before { + content: "\f0d3"; +} +.fa-google-plus-square:before { + content: "\f0d4"; +} +.fa-google-plus:before { + content: "\f0d5"; +} +.fa-money:before { + content: "\f0d6"; +} +.fa-caret-down:before { + content: "\f0d7"; +} +.fa-caret-up:before { + content: "\f0d8"; +} +.fa-caret-left:before { + content: "\f0d9"; +} +.fa-caret-right:before { + content: "\f0da"; +} +.fa-columns:before { + content: "\f0db"; +} +.fa-unsorted:before, +.fa-sort:before { + content: "\f0dc"; +} +.fa-sort-down:before, +.fa-sort-desc:before { + content: "\f0dd"; +} +.fa-sort-up:before, +.fa-sort-asc:before { + content: "\f0de"; +} +.fa-envelope:before { + content: "\f0e0"; +} +.fa-linkedin:before { + content: "\f0e1"; +} +.fa-rotate-left:before, +.fa-undo:before { + content: "\f0e2"; +} +.fa-legal:before, +.fa-gavel:before { + content: "\f0e3"; +} +.fa-dashboard:before, +.fa-tachometer:before { + content: "\f0e4"; +} +.fa-comment-o:before { + content: "\f0e5"; +} +.fa-comments-o:before { + content: "\f0e6"; +} +.fa-flash:before, +.fa-bolt:before { + content: "\f0e7"; +} +.fa-sitemap:before { + content: "\f0e8"; +} +.fa-umbrella:before { + content: "\f0e9"; +} +.fa-paste:before, +.fa-clipboard:before { + content: "\f0ea"; +} +.fa-lightbulb-o:before { + content: "\f0eb"; +} +.fa-exchange:before { + content: "\f0ec"; +} +.fa-cloud-download:before { + content: "\f0ed"; +} +.fa-cloud-upload:before { + content: "\f0ee"; +} +.fa-user-md:before { + content: "\f0f0"; +} +.fa-stethoscope:before { + content: "\f0f1"; +} +.fa-suitcase:before { + content: "\f0f2"; +} +.fa-bell-o:before { + content: "\f0a2"; +} +.fa-coffee:before { + content: "\f0f4"; +} +.fa-cutlery:before { + content: "\f0f5"; +} +.fa-file-text-o:before { + content: "\f0f6"; +} +.fa-building-o:before { + content: "\f0f7"; +} +.fa-hospital-o:before { + content: "\f0f8"; +} +.fa-ambulance:before { + content: "\f0f9"; +} +.fa-medkit:before { + content: "\f0fa"; +} +.fa-fighter-jet:before { + content: "\f0fb"; +} +.fa-beer:before { + content: "\f0fc"; +} +.fa-h-square:before { + content: "\f0fd"; +} +.fa-plus-square:before { + content: "\f0fe"; +} +.fa-angle-double-left:before { + content: "\f100"; +} +.fa-angle-double-right:before { + content: "\f101"; +} +.fa-angle-double-up:before { + content: "\f102"; +} +.fa-angle-double-down:before { + content: "\f103"; +} +.fa-angle-left:before { + content: "\f104"; +} +.fa-angle-right:before { + content: "\f105"; +} +.fa-angle-up:before { + content: "\f106"; +} +.fa-angle-down:before { + content: "\f107"; +} +.fa-desktop:before { + content: "\f108"; +} +.fa-laptop:before { + content: "\f109"; +} +.fa-tablet:before { + content: "\f10a"; +} +.fa-mobile-phone:before, +.fa-mobile:before { + content: "\f10b"; +} +.fa-circle-o:before { + content: "\f10c"; +} +.fa-quote-left:before { + content: "\f10d"; +} +.fa-quote-right:before { + content: "\f10e"; +} +.fa-spinner:before { + content: "\f110"; +} +.fa-circle:before { + content: "\f111"; +} +.fa-mail-reply:before, +.fa-reply:before { + content: "\f112"; +} +.fa-github-alt:before { + content: "\f113"; +} +.fa-folder-o:before { + content: "\f114"; +} +.fa-folder-open-o:before { + content: "\f115"; +} +.fa-smile-o:before { + content: "\f118"; +} +.fa-frown-o:before { + content: "\f119"; +} +.fa-meh-o:before { + content: "\f11a"; +} +.fa-gamepad:before { + content: "\f11b"; +} +.fa-keyboard-o:before { + content: "\f11c"; +} +.fa-flag-o:before { + content: "\f11d"; +} +.fa-flag-checkered:before { + content: "\f11e"; +} +.fa-terminal:before { + content: "\f120"; +} +.fa-code:before { + content: "\f121"; +} +.fa-mail-reply-all:before, +.fa-reply-all:before { + content: "\f122"; +} +.fa-star-half-empty:before, +.fa-star-half-full:before, +.fa-star-half-o:before { + content: "\f123"; +} +.fa-location-arrow:before { + content: "\f124"; +} +.fa-crop:before { + content: "\f125"; +} +.fa-code-fork:before { + content: "\f126"; +} +.fa-unlink:before, +.fa-chain-broken:before { + content: "\f127"; +} +.fa-question:before { + content: "\f128"; +} +.fa-info:before { + content: "\f129"; +} +.fa-exclamation:before { + content: "\f12a"; +} +.fa-superscript:before { + content: "\f12b"; +} +.fa-subscript:before { + content: "\f12c"; +} +.fa-eraser:before { + content: "\f12d"; +} +.fa-puzzle-piece:before { + content: "\f12e"; +} +.fa-microphone:before { + content: "\f130"; +} +.fa-microphone-slash:before { + content: "\f131"; +} +.fa-shield:before { + content: "\f132"; +} +.fa-calendar-o:before { + content: "\f133"; +} +.fa-fire-extinguisher:before { + content: "\f134"; +} +.fa-rocket:before { + content: "\f135"; +} +.fa-maxcdn:before { + content: "\f136"; +} +.fa-chevron-circle-left:before { + content: "\f137"; +} +.fa-chevron-circle-right:before { + content: "\f138"; +} +.fa-chevron-circle-up:before { + content: "\f139"; +} +.fa-chevron-circle-down:before { + content: "\f13a"; +} +.fa-html5:before { + content: "\f13b"; +} +.fa-css3:before { + content: "\f13c"; +} +.fa-anchor:before { + content: "\f13d"; +} +.fa-unlock-alt:before { + content: "\f13e"; +} +.fa-bullseye:before { + content: "\f140"; +} +.fa-ellipsis-h:before { + content: "\f141"; +} +.fa-ellipsis-v:before { + content: "\f142"; +} +.fa-rss-square:before { + content: "\f143"; +} +.fa-play-circle:before { + content: "\f144"; +} +.fa-ticket:before { + content: "\f145"; +} +.fa-minus-square:before { + content: "\f146"; +} +.fa-minus-square-o:before { + content: "\f147"; +} +.fa-level-up:before { + content: "\f148"; +} +.fa-level-down:before { + content: "\f149"; +} +.fa-check-square:before { + content: "\f14a"; +} +.fa-pencil-square:before { + content: "\f14b"; +} +.fa-external-link-square:before { + content: "\f14c"; +} +.fa-share-square:before { + content: "\f14d"; +} +.fa-compass:before { + content: "\f14e"; +} +.fa-toggle-down:before, +.fa-caret-square-o-down:before { + content: "\f150"; +} +.fa-toggle-up:before, +.fa-caret-square-o-up:before { + content: "\f151"; +} +.fa-toggle-right:before, +.fa-caret-square-o-right:before { + content: "\f152"; +} +.fa-euro:before, +.fa-eur:before { + content: "\f153"; +} +.fa-gbp:before { + content: "\f154"; +} +.fa-dollar:before, +.fa-usd:before { + content: "\f155"; +} +.fa-rupee:before, +.fa-inr:before { + content: "\f156"; +} +.fa-cny:before, +.fa-rmb:before, +.fa-yen:before, +.fa-jpy:before { + content: "\f157"; +} +.fa-ruble:before, +.fa-rouble:before, +.fa-rub:before { + content: "\f158"; +} +.fa-won:before, +.fa-krw:before { + content: "\f159"; +} +.fa-bitcoin:before, +.fa-btc:before { + content: "\f15a"; +} +.fa-file:before { + content: "\f15b"; +} +.fa-file-text:before { + content: "\f15c"; +} +.fa-sort-alpha-asc:before { + content: "\f15d"; +} +.fa-sort-alpha-desc:before { + content: "\f15e"; +} +.fa-sort-amount-asc:before { + content: "\f160"; +} +.fa-sort-amount-desc:before { + content: "\f161"; +} +.fa-sort-numeric-asc:before { + content: "\f162"; +} +.fa-sort-numeric-desc:before { + content: "\f163"; +} +.fa-thumbs-up:before { + content: "\f164"; +} +.fa-thumbs-down:before { + content: "\f165"; +} +.fa-youtube-square:before { + content: "\f166"; +} +.fa-youtube:before { + content: "\f167"; +} +.fa-xing:before { + content: "\f168"; +} +.fa-xing-square:before { + content: "\f169"; +} +.fa-youtube-play:before { + content: "\f16a"; +} +.fa-dropbox:before { + content: "\f16b"; +} +.fa-stack-overflow:before { + content: "\f16c"; +} +.fa-instagram:before { + content: "\f16d"; +} +.fa-flickr:before { + content: "\f16e"; +} +.fa-adn:before { + content: "\f170"; +} +.fa-bitbucket:before { + content: "\f171"; +} +.fa-bitbucket-square:before { + content: "\f172"; +} +.fa-tumblr:before { + content: "\f173"; +} +.fa-tumblr-square:before { + content: "\f174"; +} +.fa-long-arrow-down:before { + content: "\f175"; +} +.fa-long-arrow-up:before { + content: "\f176"; +} +.fa-long-arrow-left:before { + content: "\f177"; +} +.fa-long-arrow-right:before { + content: "\f178"; +} +.fa-apple:before { + content: "\f179"; +} +.fa-windows:before { + content: "\f17a"; +} +.fa-android:before { + content: "\f17b"; +} +.fa-linux:before { + content: "\f17c"; +} +.fa-dribbble:before { + content: "\f17d"; +} +.fa-skype:before { + content: "\f17e"; +} +.fa-foursquare:before { + content: "\f180"; +} +.fa-trello:before { + content: "\f181"; +} +.fa-female:before { + content: "\f182"; +} +.fa-male:before { + content: "\f183"; +} +.fa-gittip:before, +.fa-gratipay:before { + content: "\f184"; +} +.fa-sun-o:before { + content: "\f185"; +} +.fa-moon-o:before { + content: "\f186"; +} +.fa-archive:before { + content: "\f187"; +} +.fa-bug:before { + content: "\f188"; +} +.fa-vk:before { + content: "\f189"; +} +.fa-weibo:before { + content: "\f18a"; +} +.fa-renren:before { + content: "\f18b"; +} +.fa-pagelines:before { + content: "\f18c"; +} +.fa-stack-exchange:before { + content: "\f18d"; +} +.fa-arrow-circle-o-right:before { + content: "\f18e"; +} +.fa-arrow-circle-o-left:before { + content: "\f190"; +} +.fa-toggle-left:before, +.fa-caret-square-o-left:before { + content: "\f191"; +} +.fa-dot-circle-o:before { + content: "\f192"; +} +.fa-wheelchair:before { + content: "\f193"; +} +.fa-vimeo-square:before { + content: "\f194"; +} +.fa-turkish-lira:before, +.fa-try:before { + content: "\f195"; +} +.fa-plus-square-o:before { + content: "\f196"; +} +.fa-space-shuttle:before { + content: "\f197"; +} +.fa-slack:before { + content: "\f198"; +} +.fa-envelope-square:before { + content: "\f199"; +} +.fa-wordpress:before { + content: "\f19a"; +} +.fa-openid:before { + content: "\f19b"; +} +.fa-institution:before, +.fa-bank:before, +.fa-university:before { + content: "\f19c"; +} +.fa-mortar-board:before, +.fa-graduation-cap:before { + content: "\f19d"; +} +.fa-yahoo:before { + content: "\f19e"; +} +.fa-google:before { + content: "\f1a0"; +} +.fa-reddit:before { + content: "\f1a1"; +} +.fa-reddit-square:before { + content: "\f1a2"; +} +.fa-stumbleupon-circle:before { + content: "\f1a3"; +} +.fa-stumbleupon:before { + content: "\f1a4"; +} +.fa-delicious:before { + content: "\f1a5"; +} +.fa-digg:before { + content: "\f1a6"; +} +.fa-pied-piper-pp:before { + content: "\f1a7"; +} +.fa-pied-piper-alt:before { + content: "\f1a8"; +} +.fa-drupal:before { + content: "\f1a9"; +} +.fa-joomla:before { + content: "\f1aa"; +} +.fa-language:before { + content: "\f1ab"; +} +.fa-fax:before { + content: "\f1ac"; +} +.fa-building:before { + content: "\f1ad"; +} +.fa-child:before { + content: "\f1ae"; +} +.fa-paw:before { + content: "\f1b0"; +} +.fa-spoon:before { + content: "\f1b1"; +} +.fa-cube:before { + content: "\f1b2"; +} +.fa-cubes:before { + content: "\f1b3"; +} +.fa-behance:before { + content: "\f1b4"; +} +.fa-behance-square:before { + content: "\f1b5"; +} +.fa-steam:before { + content: "\f1b6"; +} +.fa-steam-square:before { + content: "\f1b7"; +} +.fa-recycle:before { + content: "\f1b8"; +} +.fa-automobile:before, +.fa-car:before { + content: "\f1b9"; +} +.fa-cab:before, +.fa-taxi:before { + content: "\f1ba"; +} +.fa-tree:before { + content: "\f1bb"; +} +.fa-spotify:before { + content: "\f1bc"; +} +.fa-deviantart:before { + content: "\f1bd"; +} +.fa-soundcloud:before { + content: "\f1be"; +} +.fa-database:before { + content: "\f1c0"; +} +.fa-file-pdf-o:before { + content: "\f1c1"; +} +.fa-file-word-o:before { + content: "\f1c2"; +} +.fa-file-excel-o:before { + content: "\f1c3"; +} +.fa-file-powerpoint-o:before { + content: "\f1c4"; +} +.fa-file-photo-o:before, +.fa-file-picture-o:before, +.fa-file-image-o:before { + content: "\f1c5"; +} +.fa-file-zip-o:before, +.fa-file-archive-o:before { + content: "\f1c6"; +} +.fa-file-sound-o:before, +.fa-file-audio-o:before { + content: "\f1c7"; +} +.fa-file-movie-o:before, +.fa-file-video-o:before { + content: "\f1c8"; +} +.fa-file-code-o:before { + content: "\f1c9"; +} +.fa-vine:before { + content: "\f1ca"; +} +.fa-codepen:before { + content: "\f1cb"; +} +.fa-jsfiddle:before { + content: "\f1cc"; +} +.fa-life-bouy:before, +.fa-life-buoy:before, +.fa-life-saver:before, +.fa-support:before, +.fa-life-ring:before { + content: "\f1cd"; +} +.fa-circle-o-notch:before { + content: "\f1ce"; +} +.fa-ra:before, +.fa-resistance:before, +.fa-rebel:before { + content: "\f1d0"; +} +.fa-ge:before, +.fa-empire:before { + content: "\f1d1"; +} +.fa-git-square:before { + content: "\f1d2"; +} +.fa-git:before { + content: "\f1d3"; +} +.fa-y-combinator-square:before, +.fa-yc-square:before, +.fa-hacker-news:before { + content: "\f1d4"; +} +.fa-tencent-weibo:before { + content: "\f1d5"; +} +.fa-qq:before { + content: "\f1d6"; +} +.fa-wechat:before, +.fa-weixin:before { + content: "\f1d7"; +} +.fa-send:before, +.fa-paper-plane:before { + content: "\f1d8"; +} +.fa-send-o:before, +.fa-paper-plane-o:before { + content: "\f1d9"; +} +.fa-history:before { + content: "\f1da"; +} +.fa-circle-thin:before { + content: "\f1db"; +} +.fa-header:before { + content: "\f1dc"; +} +.fa-paragraph:before { + content: "\f1dd"; +} +.fa-sliders:before { + content: "\f1de"; +} +.fa-share-alt:before { + content: "\f1e0"; +} +.fa-share-alt-square:before { + content: "\f1e1"; +} +.fa-bomb:before { + content: "\f1e2"; +} +.fa-soccer-ball-o:before, +.fa-futbol-o:before { + content: "\f1e3"; +} +.fa-tty:before { + content: "\f1e4"; +} +.fa-binoculars:before { + content: "\f1e5"; +} +.fa-plug:before { + content: "\f1e6"; +} +.fa-slideshare:before { + content: "\f1e7"; +} +.fa-twitch:before { + content: "\f1e8"; +} +.fa-yelp:before { + content: "\f1e9"; +} +.fa-newspaper-o:before { + content: "\f1ea"; +} +.fa-wifi:before { + content: "\f1eb"; +} +.fa-calculator:before { + content: "\f1ec"; +} +.fa-paypal:before { + content: "\f1ed"; +} +.fa-google-wallet:before { + content: "\f1ee"; +} +.fa-cc-visa:before { + content: "\f1f0"; +} +.fa-cc-mastercard:before { + content: "\f1f1"; +} +.fa-cc-discover:before { + content: "\f1f2"; +} +.fa-cc-amex:before { + content: "\f1f3"; +} +.fa-cc-paypal:before { + content: "\f1f4"; +} +.fa-cc-stripe:before { + content: "\f1f5"; +} +.fa-bell-slash:before { + content: "\f1f6"; +} +.fa-bell-slash-o:before { + content: "\f1f7"; +} +.fa-trash:before { + content: "\f1f8"; +} +.fa-copyright:before { + content: "\f1f9"; +} +.fa-at:before { + content: "\f1fa"; +} +.fa-eyedropper:before { + content: "\f1fb"; +} +.fa-paint-brush:before { + content: "\f1fc"; +} +.fa-birthday-cake:before { + content: "\f1fd"; +} +.fa-area-chart:before { + content: "\f1fe"; +} +.fa-pie-chart:before { + content: "\f200"; +} +.fa-line-chart:before { + content: "\f201"; +} +.fa-lastfm:before { + content: "\f202"; +} +.fa-lastfm-square:before { + content: "\f203"; +} +.fa-toggle-off:before { + content: "\f204"; +} +.fa-toggle-on:before { + content: "\f205"; +} +.fa-bicycle:before { + content: "\f206"; +} +.fa-bus:before { + content: "\f207"; +} +.fa-ioxhost:before { + content: "\f208"; +} +.fa-angellist:before { + content: "\f209"; +} +.fa-cc:before { + content: "\f20a"; +} +.fa-shekel:before, +.fa-sheqel:before, +.fa-ils:before { + content: "\f20b"; +} +.fa-meanpath:before { + content: "\f20c"; +} +.fa-buysellads:before { + content: "\f20d"; +} +.fa-connectdevelop:before { + content: "\f20e"; +} +.fa-dashcube:before { + content: "\f210"; +} +.fa-forumbee:before { + content: "\f211"; +} +.fa-leanpub:before { + content: "\f212"; +} +.fa-sellsy:before { + content: "\f213"; +} +.fa-shirtsinbulk:before { + content: "\f214"; +} +.fa-simplybuilt:before { + content: "\f215"; +} +.fa-skyatlas:before { + content: "\f216"; +} +.fa-cart-plus:before { + content: "\f217"; +} +.fa-cart-arrow-down:before { + content: "\f218"; +} +.fa-diamond:before { + content: "\f219"; +} +.fa-ship:before { + content: "\f21a"; +} +.fa-user-secret:before { + content: "\f21b"; +} +.fa-motorcycle:before { + content: "\f21c"; +} +.fa-street-view:before { + content: "\f21d"; +} +.fa-heartbeat:before { + content: "\f21e"; +} +.fa-venus:before { + content: "\f221"; +} +.fa-mars:before { + content: "\f222"; +} +.fa-mercury:before { + content: "\f223"; +} +.fa-intersex:before, +.fa-transgender:before { + content: "\f224"; +} +.fa-transgender-alt:before { + content: "\f225"; +} +.fa-venus-double:before { + content: "\f226"; +} +.fa-mars-double:before { + content: "\f227"; +} +.fa-venus-mars:before { + content: "\f228"; +} +.fa-mars-stroke:before { + content: "\f229"; +} +.fa-mars-stroke-v:before { + content: "\f22a"; +} +.fa-mars-stroke-h:before { + content: "\f22b"; +} +.fa-neuter:before { + content: "\f22c"; +} +.fa-genderless:before { + content: "\f22d"; +} +.fa-facebook-official:before { + content: "\f230"; +} +.fa-pinterest-p:before { + content: "\f231"; +} +.fa-whatsapp:before { + content: "\f232"; +} +.fa-server:before { + content: "\f233"; +} +.fa-user-plus:before { + content: "\f234"; +} +.fa-user-times:before { + content: "\f235"; +} +.fa-hotel:before, +.fa-bed:before { + content: "\f236"; +} +.fa-viacoin:before { + content: "\f237"; +} +.fa-train:before { + content: "\f238"; +} +.fa-subway:before { + content: "\f239"; +} +.fa-medium:before { + content: "\f23a"; +} +.fa-yc:before, +.fa-y-combinator:before { + content: "\f23b"; +} +.fa-optin-monster:before { + content: "\f23c"; +} +.fa-opencart:before { + content: "\f23d"; +} +.fa-expeditedssl:before { + content: "\f23e"; +} +.fa-battery-4:before, +.fa-battery:before, +.fa-battery-full:before { + content: "\f240"; +} +.fa-battery-3:before, +.fa-battery-three-quarters:before { + content: "\f241"; +} +.fa-battery-2:before, +.fa-battery-half:before { + content: "\f242"; +} +.fa-battery-1:before, +.fa-battery-quarter:before { + content: "\f243"; +} +.fa-battery-0:before, +.fa-battery-empty:before { + content: "\f244"; +} +.fa-mouse-pointer:before { + content: "\f245"; +} +.fa-i-cursor:before { + content: "\f246"; +} +.fa-object-group:before { + content: "\f247"; +} +.fa-object-ungroup:before { + content: "\f248"; +} +.fa-sticky-note:before { + content: "\f249"; +} +.fa-sticky-note-o:before { + content: "\f24a"; +} +.fa-cc-jcb:before { + content: "\f24b"; +} +.fa-cc-diners-club:before { + content: "\f24c"; +} +.fa-clone:before { + content: "\f24d"; +} +.fa-balance-scale:before { + content: "\f24e"; +} +.fa-hourglass-o:before { + content: "\f250"; +} +.fa-hourglass-1:before, +.fa-hourglass-start:before { + content: "\f251"; +} +.fa-hourglass-2:before, +.fa-hourglass-half:before { + content: "\f252"; +} +.fa-hourglass-3:before, +.fa-hourglass-end:before { + content: "\f253"; +} +.fa-hourglass:before { + content: "\f254"; +} +.fa-hand-grab-o:before, +.fa-hand-rock-o:before { + content: "\f255"; +} +.fa-hand-stop-o:before, +.fa-hand-paper-o:before { + content: "\f256"; +} +.fa-hand-scissors-o:before { + content: "\f257"; +} +.fa-hand-lizard-o:before { + content: "\f258"; +} +.fa-hand-spock-o:before { + content: "\f259"; +} +.fa-hand-pointer-o:before { + content: "\f25a"; +} +.fa-hand-peace-o:before { + content: "\f25b"; +} +.fa-trademark:before { + content: "\f25c"; +} +.fa-registered:before { + content: "\f25d"; +} +.fa-creative-commons:before { + content: "\f25e"; +} +.fa-gg:before { + content: "\f260"; +} +.fa-gg-circle:before { + content: "\f261"; +} +.fa-tripadvisor:before { + content: "\f262"; +} +.fa-odnoklassniki:before { + content: "\f263"; +} +.fa-odnoklassniki-square:before { + content: "\f264"; +} +.fa-get-pocket:before { + content: "\f265"; +} +.fa-wikipedia-w:before { + content: "\f266"; +} +.fa-safari:before { + content: "\f267"; +} +.fa-chrome:before { + content: "\f268"; +} +.fa-firefox:before { + content: "\f269"; +} +.fa-opera:before { + content: "\f26a"; +} +.fa-internet-explorer:before { + content: "\f26b"; +} +.fa-tv:before, +.fa-television:before { + content: "\f26c"; +} +.fa-contao:before { + content: "\f26d"; +} +.fa-500px:before { + content: "\f26e"; +} +.fa-amazon:before { + content: "\f270"; +} +.fa-calendar-plus-o:before { + content: "\f271"; +} +.fa-calendar-minus-o:before { + content: "\f272"; +} +.fa-calendar-times-o:before { + content: "\f273"; +} +.fa-calendar-check-o:before { + content: "\f274"; +} +.fa-industry:before { + content: "\f275"; +} +.fa-map-pin:before { + content: "\f276"; +} +.fa-map-signs:before { + content: "\f277"; +} +.fa-map-o:before { + content: "\f278"; +} +.fa-map:before { + content: "\f279"; +} +.fa-commenting:before { + content: "\f27a"; +} +.fa-commenting-o:before { + content: "\f27b"; +} +.fa-houzz:before { + content: "\f27c"; +} +.fa-vimeo:before { + content: "\f27d"; +} +.fa-black-tie:before { + content: "\f27e"; +} +.fa-fonticons:before { + content: "\f280"; +} +.fa-reddit-alien:before { + content: "\f281"; +} +.fa-edge:before { + content: "\f282"; +} +.fa-credit-card-alt:before { + content: "\f283"; +} +.fa-codiepie:before { + content: "\f284"; +} +.fa-modx:before { + content: "\f285"; +} +.fa-fort-awesome:before { + content: "\f286"; +} +.fa-usb:before { + content: "\f287"; +} +.fa-product-hunt:before { + content: "\f288"; +} +.fa-mixcloud:before { + content: "\f289"; +} +.fa-scribd:before { + content: "\f28a"; +} +.fa-pause-circle:before { + content: "\f28b"; +} +.fa-pause-circle-o:before { + content: "\f28c"; +} +.fa-stop-circle:before { + content: "\f28d"; +} +.fa-stop-circle-o:before { + content: "\f28e"; +} +.fa-shopping-bag:before { + content: "\f290"; +} +.fa-shopping-basket:before { + content: "\f291"; +} +.fa-hashtag:before { + content: "\f292"; +} +.fa-bluetooth:before { + content: "\f293"; +} +.fa-bluetooth-b:before { + content: "\f294"; +} +.fa-percent:before { + content: "\f295"; +} +.fa-gitlab:before { + content: "\f296"; +} +.fa-wpbeginner:before { + content: "\f297"; +} +.fa-wpforms:before { + content: "\f298"; +} +.fa-envira:before { + content: "\f299"; +} +.fa-universal-access:before { + content: "\f29a"; +} +.fa-wheelchair-alt:before { + content: "\f29b"; +} +.fa-question-circle-o:before { + content: "\f29c"; +} +.fa-blind:before { + content: "\f29d"; +} +.fa-audio-description:before { + content: "\f29e"; +} +.fa-volume-control-phone:before { + content: "\f2a0"; +} +.fa-braille:before { + content: "\f2a1"; +} +.fa-assistive-listening-systems:before { + content: "\f2a2"; +} +.fa-asl-interpreting:before, +.fa-american-sign-language-interpreting:before { + content: "\f2a3"; +} +.fa-deafness:before, +.fa-hard-of-hearing:before, +.fa-deaf:before { + content: "\f2a4"; +} +.fa-glide:before { + content: "\f2a5"; +} +.fa-glide-g:before { + content: "\f2a6"; +} +.fa-signing:before, +.fa-sign-language:before { + content: "\f2a7"; +} +.fa-low-vision:before { + content: "\f2a8"; +} +.fa-viadeo:before { + content: "\f2a9"; +} +.fa-viadeo-square:before { + content: "\f2aa"; +} +.fa-snapchat:before { + content: "\f2ab"; +} +.fa-snapchat-ghost:before { + content: "\f2ac"; +} +.fa-snapchat-square:before { + content: "\f2ad"; +} +.fa-pied-piper:before { + content: "\f2ae"; +} +.fa-first-order:before { + content: "\f2b0"; +} +.fa-yoast:before { + content: "\f2b1"; +} +.fa-themeisle:before { + content: "\f2b2"; +} +.fa-google-plus-circle:before, +.fa-google-plus-official:before { + content: "\f2b3"; +} +.fa-fa:before, +.fa-font-awesome:before { + content: "\f2b4"; +} +.fa-handshake-o:before { + content: "\f2b5"; +} +.fa-envelope-open:before { + content: "\f2b6"; +} +.fa-envelope-open-o:before { + content: "\f2b7"; +} +.fa-linode:before { + content: "\f2b8"; +} +.fa-address-book:before { + content: "\f2b9"; +} +.fa-address-book-o:before { + content: "\f2ba"; +} +.fa-vcard:before, +.fa-address-card:before { + content: "\f2bb"; +} +.fa-vcard-o:before, +.fa-address-card-o:before { + content: "\f2bc"; +} +.fa-user-circle:before { + content: "\f2bd"; +} +.fa-user-circle-o:before { + content: "\f2be"; +} +.fa-user-o:before { + content: "\f2c0"; +} +.fa-id-badge:before { + content: "\f2c1"; +} +.fa-drivers-license:before, +.fa-id-card:before { + content: "\f2c2"; +} +.fa-drivers-license-o:before, +.fa-id-card-o:before { + content: "\f2c3"; +} +.fa-quora:before { + content: "\f2c4"; +} +.fa-free-code-camp:before { + content: "\f2c5"; +} +.fa-telegram:before { + content: "\f2c6"; +} +.fa-thermometer-4:before, +.fa-thermometer:before, +.fa-thermometer-full:before { + content: "\f2c7"; +} +.fa-thermometer-3:before, +.fa-thermometer-three-quarters:before { + content: "\f2c8"; +} +.fa-thermometer-2:before, +.fa-thermometer-half:before { + content: "\f2c9"; +} +.fa-thermometer-1:before, +.fa-thermometer-quarter:before { + content: "\f2ca"; +} +.fa-thermometer-0:before, +.fa-thermometer-empty:before { + content: "\f2cb"; +} +.fa-shower:before { + content: "\f2cc"; +} +.fa-bathtub:before, +.fa-s15:before, +.fa-bath:before { + content: "\f2cd"; +} +.fa-podcast:before { + content: "\f2ce"; +} +.fa-window-maximize:before { + content: "\f2d0"; +} +.fa-window-minimize:before { + content: "\f2d1"; +} +.fa-window-restore:before { + content: "\f2d2"; +} +.fa-times-rectangle:before, +.fa-window-close:before { + content: "\f2d3"; +} +.fa-times-rectangle-o:before, +.fa-window-close-o:before { + content: "\f2d4"; +} +.fa-bandcamp:before { + content: "\f2d5"; +} +.fa-grav:before { + content: "\f2d6"; +} +.fa-etsy:before { + content: "\f2d7"; +} +.fa-imdb:before { + content: "\f2d8"; +} +.fa-ravelry:before { + content: "\f2d9"; +} +.fa-eercast:before { + content: "\f2da"; +} +.fa-microchip:before { + content: "\f2db"; +} +.fa-snowflake-o:before { + content: "\f2dc"; +} +.fa-superpowers:before { + content: "\f2dd"; +} +.fa-wpexplorer:before { + content: "\f2de"; +} +.fa-meetup:before { + content: "\f2e0"; +} +.sr-only { + position: absolute; + width: 1px; + height: 1px; + padding: 0; + margin: -1px; + overflow: hidden; + clip: rect(0, 0, 0, 0); + border: 0; +} +.sr-only-focusable:active, +.sr-only-focusable:focus { + position: static; + width: auto; + height: auto; + margin: 0; + overflow: visible; + clip: auto; +} diff --git a/lib/font-awesome/css/font-awesome.css.map b/lib/font-awesome/css/font-awesome.css.map new file mode 100644 index 0000000000..60763a8640 --- /dev/null +++ b/lib/font-awesome/css/font-awesome.css.map @@ -0,0 +1,7 @@ +{ +"version": 3, +"mappings": ";;;;;;;AAGA,UAUC;EATC,WAAW,EAAE,aAAa;EAC1B,GAAG,EAAE,+CAAgE;EACrE,GAAG,EAAE,ySAAmG;EAKxG,WAAW,EAAE,MAAM;EACnB,UAAU,EAAE,MAAM;ACTpB,GAAmB;EACjB,OAAO,EAAE,YAAY;EACrB,IAAI,EAAE,uCAAwD;EAC9D,SAAS,EAAE,OAAO;EAClB,cAAc,EAAE,IAAI;EACpB,sBAAsB,EAAE,WAAW;EACnC,uBAAuB,EAAE,SAAS;EAClC,SAAS,EAAE,eAAe;;;ACN5B,MAAsB;EACpB,SAAS,EAAE,SAAS;EACpB,WAAW,EAAE,MAAS;EACtB,cAAc,EAAE,IAAI;;AAEtB,MAAsB;EAAE,SAAS,EAAE,GAAG;;AACtC,MAAsB;EAAE,SAAS,EAAE,GAAG;;AACtC,MAAsB;EAAE,SAAS,EAAE,GAAG;;AACtC,MAAsB;EAAE,SAAS,EAAE,GAAG;;ACVtC,MAAsB;EACpB,KAAK,EAAE,SAAW;EAClB,UAAU,EAAE,MAAM;;ACDpB,MAAsB;EACpB,YAAY,EAAE,CAAC;EACf,WAAW,ECKU,SAAS;EDJ9B,eAAe,EAAE,IAAI;EACrB,WAAK;IAAE,QAAQ,EAAE,QAAQ;;AAE3B,MAAsB;EACpB,QAAQ,EAAE,QAAQ;EAClB,IAAI,EAAE,UAAa;EACnB,KAAK,ECFgB,SAAS;EDG9B,GAAG,EAAE,SAAU;EACf,UAAU,EAAE,MAAM;EAClB,YAAuB;IACrB,IAAI,EAAE,UAA0B;;AEbpC,UAA0B;EACxB,OAAO,EAAE,gBAAgB;EACzB,MAAM,EAAE,iBAA4B;EACpC,aAAa,EAAE,IAAI;;AAGrB,WAAY;EAAE,KAAK,EAAE,KAAK;;AAC1B,UAAW;EAAE,KAAK,EAAE,IAAI;;AAGtB,aAAY;EAAE,YAAY,EAAE,IAAI;AAChC,cAAa;EAAE,WAAW,EAAE,IAAI;;ACXlC,QAAwB;EACtB,iBAAiB,EAAE,0BAA0B;EACrC,SAAS,EAAE,0BAA0B;;AAG/C,SAAyB;EACvB,iBAAiB,EAAE,4BAA4B;EACvC,SAAS,EAAE,4BAA4B;;AAGjD,0BASC;EARC,EAAG;IACD,iBAAiB,EAAE,YAAY;IACvB,SAAS,EAAE,YAAY;EAEjC,IAAK;IACH,iBAAiB,EAAE,cAAc;IACzB,SAAS,EAAE,cAAc;AAIrC,kBASC;EARC,EAAG;IACD,iBAAiB,EAAE,YAAY;IACvB,SAAS,EAAE,YAAY;EAEjC,IAAK;IACH,iBAAiB,EAAE,cAAc;IACzB,SAAS,EAAE,cAAc;AC5BrC,aAA8B;ECY5B,MAAM,EAAE,wDAAmE;EAC3E,iBAAiB,EAAE,aAAgB;EAC/B,aAAa,EAAE,aAAgB;EAC3B,SAAS,EAAE,aAAgB;;ADdrC,cAA8B;ECW5B,MAAM,EAAE,wDAAmE;EAC3E,iBAAiB,EAAE,cAAgB;EAC/B,aAAa,EAAE,cAAgB;EAC3B,SAAS,EAAE,cAAgB;;ADbrC,cAA8B;ECU5B,MAAM,EAAE,wDAAmE;EAC3E,iBAAiB,EAAE,cAAgB;EAC/B,aAAa,EAAE,cAAgB;EAC3B,SAAS,EAAE,cAAgB;;ADXrC,mBAAmC;ECejC,MAAM,EAAE,wDAAmE;EAC3E,iBAAiB,EAAE,YAAoB;EACnC,aAAa,EAAE,YAAoB;EAC/B,SAAS,EAAE,YAAoB;;ADjBzC,iBAAmC;ECcjC,MAAM,EAAE,wDAAmE;EAC3E,iBAAiB,EAAE,YAAoB;EACnC,aAAa,EAAE,YAAoB;EAC/B,SAAS,EAAE,YAAoB;;ADZzC;;;;uBAIuC;EACrC,MAAM,EAAE,IAAI;;AEfd,SAAyB;EACvB,QAAQ,EAAE,QAAQ;EAClB,OAAO,EAAE,YAAY;EACrB,KAAK,EAAE,GAAG;EACV,MAAM,EAAE,GAAG;EACX,WAAW,EAAE,GAAG;EAChB,cAAc,EAAE,MAAM;;AAExB,0BAAyD;EACvD,QAAQ,EAAE,QAAQ;EAClB,IAAI,EAAE,CAAC;EACP,KAAK,EAAE,IAAI;EACX,UAAU,EAAE,MAAM;;AAEpB,YAA4B;EAAE,WAAW,EAAE,OAAO;;AAClD,YAA4B;EAAE,SAAS,EAAE,GAAG;;AAC5C,WAA2B;EAAE,KAAK,ELVZ,IAAI;;;;AMN1B,gBAAgC;EAAE,OAAO,ENoQ1B,GAAO;;AMnQtB,gBAAgC;EAAE,OAAO,EN0W1B,GAAO;;AMzWtB,iBAAiC;EAAE,OAAO,ENmb1B,GAAO;;AMlbvB,qBAAqC;EAAE,OAAO,ENmL1B,GAAO;;AMlL3B,gBAAgC;EAAE,OAAO,ENkR1B,GAAO;;AMjRtB,eAA+B;EAAE,OAAO,ENke1B,GAAO;;AMjerB,iBAAiC;EAAE,OAAO,ENse1B,GAAO;;AMrevB,eAA+B;EAAE,OAAO,EN+iB1B,GAAO;;AM9iBrB,eAA+B;EAAE,OAAO,ENyN1B,GAAO;;AMxNrB,mBAAmC;EAAE,OAAO,ENggB1B,GAAO;;AM/fzB,aAA6B;EAAE,OAAO,EN8f1B,GAAO;;AM7fnB,kBAAkC;EAAE,OAAO,EN+f1B,GAAO;;AM9fxB,gBAAgC;EAAE,OAAO,ENoG1B,GAAO;;AMnGtB;;gBAEgC;EAAE,OAAO,ENkgB1B,GAAO;;AMjgBtB,sBAAsC;EAAE,OAAO,ENua1B,GAAO;;AMta5B,uBAAuC;EAAE,OAAO,ENqa1B,GAAO;;AMpa7B,oBAAoC;EAAE,OAAO,EN+X1B,GAAO;;AM9X1B,iBAAiC;EAAE,OAAO,ENsb1B,GAAO;;AMrbvB;cAC8B;EAAE,OAAO,ENwH1B,GAAO;;AMvHpB,kBAAkC;EAAE,OAAO,ENygB1B,GAAO;;AMxgBxB,eAA+B;EAAE,OAAO,ENmQ1B,GAAO;;AMlQrB,iBAAiC;EAAE,OAAO,EN6L1B,GAAO;;AM5LvB,kBAAkC;EAAE,OAAO,EN0G1B,GAAO;;AMzGxB,eAA+B;EAAE,OAAO,EN+Y1B,GAAO;;AM9YrB,mBAAmC;EAAE,OAAO,ENiJ1B,GAAO;;AMhJzB,8BAA8C;EAAE,OAAO,ENI1B,GAAO;;AMHpC,4BAA4C;EAAE,OAAO,ENM1B,GAAO;;AMLlC,gBAAgC;EAAE,OAAO,ENkQ1B,GAAO;;AMjQtB,wBAAwC;EAAE,OAAO,EN4W1B,GAAO;;AM3W9B;iBACiC;EAAE,OAAO,ENmY1B,GAAO;;AMlYvB,kBAAkC;EAAE,OAAO,EN8X1B,GAAO;;AM7XxB,mBAAmC;EAAE,OAAO,ENiS1B,GAAO;;AMhSzB,eAA+B;EAAE,OAAO,ENoS1B,GAAO;;AMnSrB,eAA+B;EAAE,OAAO,ENgM1B,GAAO;;AM/LrB,qBAAqC;EAAE,OAAO,EN+O1B,GAAO;;AM9O3B,qBAAqC;EAAE,OAAO,EN8hB1B,GAAO;;AM7hB3B,sBAAsC;EAAE,OAAO,EN4hB1B,GAAO;;AM3hB5B,oBAAoC;EAAE,OAAO,EN6hB1B,GAAO;;AM5hB1B,iBAAiC;EAAE,OAAO,EN2W1B,GAAO;;AM1WvB,kBAAkC;EAAE,OAAO,ENW1B,GAAO;;AMVxB,cAA8B;EAAE,OAAO,ENod1B,GAAO;;AMndpB,eAA+B;EAAE,OAAO,ENod1B,GAAO;;AMndrB,eAA+B;EAAE,OAAO,EN2B1B,GAAO;;AM1BrB,mBAAmC;EAAE,OAAO,EN2B1B,GAAO;;AM1BzB,gBAAgC;EAAE,OAAO,ENkW1B,GAAO;;AMjWtB,iBAAiC;EAAE,OAAO,ENwC1B,GAAO;;AMvCvB,eAA+B;EAAE,OAAO,EN8L1B,GAAO;;AM7LrB,eAA+B;EAAE,OAAO,ENmB1B,GAAO;;AMlBrB,iBAAiC;EAAE,OAAO,ENoP1B,GAAO;;AMnPvB,sBAAsC;EAAE,OAAO,ENid1B,GAAO;;AMhd5B,qBAAqC;EAAE,OAAO,ENid1B,GAAO;;AMhd3B,qBAAqC;EAAE,OAAO,EN1C1B,GAAO;;AM2C3B,uBAAuC;EAAE,OAAO,EN7C1B,GAAO;;AM8C7B,sBAAsC;EAAE,OAAO,EN3C1B,GAAO;;AM4C5B,wBAAwC;EAAE,OAAO,EN9C1B,GAAO;;AM+C9B,eAA+B;EAAE,OAAO,ENwQ1B,GAAO;;AMvQrB;kBACkC;EAAE,OAAO,ENmT1B,GAAO;;AMlTxB,iBAAiC;EAAE,OAAO,ENmO1B,GAAO;;AMlOvB,uBAAuC;EAAE,OAAO,ENigB1B,GAAO;;AMhgB7B;;oBAEoC;EAAE,OAAO,EN+T1B,GAAO;;AM9T1B,iBAAiC;EAAE,OAAO,ENwT1B,GAAO;;AMvTvB,qBAAqC;EAAE,OAAO,EN+Q1B,GAAO;;AM9Q3B,iBAAiC;EAAE,OAAO,EN5D1B,GAAO;;AM6DvB,eAA+B;EAAE,OAAO,EN8c1B,GAAO;;AM7crB;0BAC0C;EAAE,OAAO,ENqT1B,GAAO;;AMpThC,yBAAyC;EAAE,OAAO,ENuX1B,GAAO;;AMtX/B,yBAAyC;EAAE,OAAO,EN0C1B,GAAO;;AMzC/B,iBAAiC;EAAE,OAAO,ENjC1B,GAAO;;AMkCvB,wBAAwC;EAAE,OAAO,ENma1B,GAAO;;AMla9B,wBAAwC;EAAE,OAAO,EN4H1B,GAAO;;AM3H9B,mBAAmC;EAAE,OAAO,EN7B1B,GAAO;;AM8BzB,eAA+B;EAAE,OAAO,EN0T1B,GAAO;;AMzTrB,gBAAgC;EAAE,OAAO,ENwS1B,GAAO;;AMvStB,eAA+B;EAAE,OAAO,ENia1B,GAAO;;AMharB,kBAAkC;EAAE,OAAO,ENgK1B,GAAO;;AM/JxB,uBAAuC;EAAE,OAAO,ENuH1B,GAAO;;AMtH7B,uBAAuC;EAAE,OAAO,EN4Z1B,GAAO;;AM3Z7B,gBAAgC;EAAE,OAAO,EN4F1B,GAAO;;AM3FtB,uBAAuC;EAAE,OAAO,ENoC1B,GAAO;;AMnC7B,wBAAwC;EAAE,OAAO,ENoC1B,GAAO;;AMnC9B,sBAAsC;EAAE,OAAO,ENsT1B,GAAO;;AMrT5B,uBAAuC;EAAE,OAAO,ENyQ1B,GAAO;;AMxQ7B,uBAAuC;EAAE,OAAO,ENwb1B,GAAO;;AMvb7B,uBAAuC;EAAE,OAAO,ENsB1B,GAAO;;AMrB7B,0BAA0C;EAAE,OAAO,EN2T1B,GAAO;;AM1ThC,sBAAsC;EAAE,OAAO,ENsM1B,GAAO;;AMrM5B,qBAAqC;EAAE,OAAO,EN6D1B,GAAO;;AM5D3B,yBAAyC;EAAE,OAAO,ENob1B,GAAO;;AMnb/B,yBAAyC;EAAE,OAAO,ENkB1B,GAAO;;AMjB/B,cAA8B;EAAE,OAAO,EN/C1B,GAAO;;AMgDpB,qBAAqC;EAAE,OAAO,EN3D1B,GAAO;;AM4D3B,sBAAsC;EAAE,OAAO,EN3D1B,GAAO;;AM4D5B,mBAAmC;EAAE,OAAO,EN3D1B,GAAO;;AM4DzB,qBAAqC;EAAE,OAAO,EN/D1B,GAAO;;AMgE3B;gBACgC;EAAE,OAAO,ENqV1B,GAAO;;AMpVtB,iBAAiC;EAAE,OAAO,ENuF1B,GAAO;;AMtFvB,mBAAmC;EAAE,OAAO,EN4C1B,GAAO;;AM3CzB,eAA+B;EAAE,OAAO,ENmS1B,GAAO;;AMlSrB,gBAAgC;EAAE,OAAO,ENsP1B,GAAO;;AMrPtB,mBAAmC;EAAE,OAAO,EN9D1B,GAAO;;AM+DzB,6BAA6C;EAAE,OAAO,ENgF1B,GAAO;;AM/EnC,eAA+B;EAAE,OAAO,EN+I1B,GAAO;;AM9IrB,eAA+B;EAAE,OAAO,ENoM1B,GAAO;;AMnMrB,eAA+B;EAAE,OAAO,ENmH1B,GAAO;;AMlHrB,cAA8B;EAAE,OAAO,ENiF1B,GAAO;;AMhFpB,oBAAoC;EAAE,OAAO,ENiF1B,GAAO;;AMhF1B;+BAC+C;EAAE,OAAO,EN0E1B,GAAO;;AMzErC,gBAAgC;EAAE,OAAO,ENmR1B,GAAO;;AMlRtB,mBAAmC;EAAE,OAAO,EN/B1B,GAAO;;AMgCzB,iBAAiC;EAAE,OAAO,ENoS1B,GAAO;;AMnSvB,kBAAkC;EAAE,OAAO,ENwB1B,GAAO;;AMvBxB,iBAAiC;EAAE,OAAO,ENqN1B,GAAO;;AMpNvB,qBAAqC;EAAE,OAAO,ENE1B,GAAO;;AMD3B,uBAAuC;EAAE,OAAO,ENF1B,GAAO;;AMG7B,kBAAkC;EAAE,OAAO,EN2S1B,GAAO;;AM1SxB,wBAAwC;EAAE,OAAO,ENyU1B,GAAO;;AMxU9B,iBAAiC;EAAE,OAAO,EN8G1B,GAAO;;AM7GvB,sBAAsC;EAAE,OAAO,EN+G1B,GAAO;;AM9G5B,mBAAmC;EAAE,OAAO,ENnF1B,GAAO;;AMoFzB,mBAAmC;EAAE,OAAO,ENrF1B,GAAO;;AMsFzB;oBACoC;EAAE,OAAO,EN/E1B,GAAO;;AMgF1B,yBAAyC;EAAE,OAAO,ENua1B,GAAO;;AMta/B,0BAA0C;EAAE,OAAO,ENmE1B,GAAO;;AMlEhC,uBAAuC;EAAE,OAAO,EN5C1B,GAAO;;AM6C7B,cAA8B;EAAE,OAAO,ENqK1B,GAAO;;AMpKpB;eAC+B;EAAE,OAAO,ENK1B,GAAO;;AMJrB,mBAAmC;EAAE,OAAO,ENQ1B,GAAO;;AMPzB,sBAAsC;EAAE,OAAO,ENmY1B,GAAO;;AMlY5B,wBAAwC;EAAE,OAAO,ENiY1B,GAAO;;AMhY9B,oBAAoC;EAAE,OAAO,EN2V1B,GAAO;;AM1V1B,kBAAkC;EAAE,OAAO,ENyI1B,GAAO;;AMxIxB,mBAAmC;EAAE,OAAO,ENyT1B,GAAO;;AMxTzB,0BAA0C;EAAE,OAAO,ENiL1B,GAAO;;AMhLhC,qBAAqC;EAAE,OAAO,EN0X1B,GAAO;;AMzX3B,wBAAwC;EAAE,OAAO,EN8C1B,GAAO;;AM7C9B,kBAAkC;EAAE,OAAO,ENoT1B,GAAO;;AMnTxB,iBAAiC;EAAE,OAAO,EN8Y1B,GAAO;;AM7YvB,wBAAwC;EAAE,OAAO,EN6G1B,GAAO;;AM5G9B,iBAAiC;EAAE,OAAO,EN8Z1B,GAAO;;AM7ZvB,kBAAkC;EAAE,OAAO,EN+J1B,GAAO;;AM9JxB,gBAAgC;EAAE,OAAO,ENsO1B,GAAO;;AMrOtB,mBAAmC;EAAE,OAAO,EN2U1B,GAAO;;AM1UzB,qBAAqC;EAAE,OAAO,EN/E1B,GAAO;;AMgF3B,uBAAuC;EAAE,OAAO,ENoO1B,GAAO;;AMnO7B,kBAAkC;EAAE,OAAO,EN8Y1B,GAAO;;AM7YxB;mBACmC;EAAE,OAAO,ENuC1B,GAAO;;AMtCzB,iBAAiC;EAAE,OAAO,ENiG1B,GAAO;;AMhGvB,iBAAiC;EAAE,OAAO,ENiZ1B,GAAO;;AMhZvB,sBAAsC;EAAE,OAAO,ENR1B,GAAO;;AMS5B,cAA8B;EAAE,OAAO,EN4Q1B,GAAO;;AM3QpB,gBAAgC;EAAE,OAAO,ENgH1B,GAAO;;AM/GtB,mBAAmC;EAAE,OAAO,ENnF1B,GAAO;;AMoFzB,eAA+B;EAAE,OAAO,ENzG1B,GAAO;;AM0GrB,sBAAsC;EAAE,OAAO,ENzD1B,GAAO;;AM0D5B,uBAAuC;EAAE,OAAO,EN0G1B,GAAO;;AMzG7B,sBAAsC;EAAE,OAAO,ENwG1B,GAAO;;AMvG5B,oBAAoC;EAAE,OAAO,ENyG1B,GAAO;;AMxG1B,sBAAsC;EAAE,OAAO,ENqG1B,GAAO;;AMpG5B,4BAA4C;EAAE,OAAO,EN5I1B,GAAO;;AM6IlC,6BAA6C;EAAE,OAAO,ENxI1B,GAAO;;AMyInC,0BAA0C;EAAE,OAAO,ENxI1B,GAAO;;AMyIhC,4BAA4C;EAAE,OAAO,ENhJ1B,GAAO;;AMiJlC,gBAAgC;EAAE,OAAO,ENsF1B,GAAO;;AMrFtB,iBAAiC;EAAE,OAAO,ENia1B,GAAO;;AMhavB,gBAAgC;EAAE,OAAO,ENiV1B,GAAO;;AMhVtB,iBAAiC;EAAE,OAAO,ENgD1B,GAAO;;AM/CvB,oBAAoC;EAAE,OAAO,ENvG1B,GAAO;;AMwG1B,qBAAqC;EAAE,OAAO,ENzI1B,GAAO;;AM0I3B;gBACgC;EAAE,OAAO,ENqY1B,GAAO;;AMpYtB;eAC+B;EAAE,OAAO,ENuI1B,GAAO;;AMtIrB,gBAAgC;EAAE,OAAO,ENpD1B,GAAO;;AMqDtB,gBAAgC;EAAE,OAAO,EN+C1B,GAAO;;AM9CtB;mBACmC;EAAE,OAAO,ENwP1B,GAAO;;AMvPzB;kBACkC;EAAE,OAAO,ENkC1B,GAAO;;AMjCxB,oBAAoC;EAAE,OAAO,ENsL1B,GAAO;;AMrL1B;mBACmC;EAAE,OAAO,EN0C1B,GAAO;;AMzCzB,iBAAiC;EAAE,OAAO,ENiS1B,GAAO;;AMhSvB;;eAE+B;EAAE,OAAO,EN9I1B,GAAO;;AM+IrB,kBAAkC;EAAE,OAAO,ENgI1B,GAAO;;AM/HxB,kBAAkC;EAAE,OAAO,EN8H1B,GAAO;;AM7HxB,wBAAwC;EAAE,OAAO,EN4S1B,GAAO;;AM3S9B,oBAAoC;EAAE,OAAO,ENoW1B,GAAO;;AMnW1B,gBAAgC;EAAE,OAAO,ENmT1B,GAAO;;AMlTtB,gBAAgC;EAAE,OAAO,ENkI1B,GAAO;;AMjItB,gBAAgC;EAAE,OAAO,ENuV1B,GAAO;;AMtVtB,oBAAoC;EAAE,OAAO,ENwL1B,GAAO;;AMvL1B,2BAA2C;EAAE,OAAO,ENyL1B,GAAO;;AMxLjC,6BAA6C;EAAE,OAAO,ENyD1B,GAAO;;AMxDnC,sBAAsC;EAAE,OAAO,ENuD1B,GAAO;;AMtD5B,gBAAgC;EAAE,OAAO,ENsJ1B,GAAO;;AMrJtB,qBAAqC;EAAE,OAAO,ENtH1B,GAAO;;AMuH3B,mBAAmC;EAAE,OAAO,ENhH1B,GAAO;;AMiHzB,qBAAqC;EAAE,OAAO,ENvH1B,GAAO;;AMwH3B,sBAAsC;EAAE,OAAO,ENvH1B,GAAO;;AMwH5B,kBAAkC;EAAE,OAAO,ENvE1B,GAAO;;AMwExB;eAC+B;EAAE,OAAO,EN2P1B,GAAO;;AM1PrB;oBACoC;EAAE,OAAO,EN+P1B,GAAO;;AM9P1B;mBACmC;EAAE,OAAO,EN4P1B,GAAO;;AM3PzB,mBAAmC;EAAE,OAAO,ENxC1B,GAAO;;AMyCzB,mBAAmC;EAAE,OAAO,ENkG1B,GAAO;;AMjGzB;eAC+B;EAAE,OAAO,EN8U1B,GAAO;;AM7UrB;gBACgC;EAAE,OAAO,ENqB1B,GAAO;;AMpBtB;qBACqC;EAAE,OAAO,EN2R1B,GAAO;;AM1R3B,oBAAoC;EAAE,OAAO,ENpF1B,GAAO;;AMqF1B,qBAAqC;EAAE,OAAO,ENnF1B,GAAO;;AMoF3B;eAC+B;EAAE,OAAO,ENjK1B,GAAO;;AMkKrB,kBAAkC;EAAE,OAAO,ENkO1B,GAAO;;AMjOxB,mBAAmC;EAAE,OAAO,ENkU1B,GAAO;;AMjUzB;oBACoC;EAAE,OAAO,EN1G1B,GAAO;;AM2G1B,sBAAsC;EAAE,OAAO,ENgF1B,GAAO;;AM/E5B,mBAAmC;EAAE,OAAO,ENnD1B,GAAO;;AMoDzB,yBAAyC;EAAE,OAAO,ENzG1B,GAAO;;AM0G/B,uBAAuC;EAAE,OAAO,ENzG1B,GAAO;;AM0G7B,kBAAkC;EAAE,OAAO,ENsU1B,GAAO;;AMrUxB,sBAAsC;EAAE,OAAO,EN+P1B,GAAO;;AM9P5B,mBAAmC;EAAE,OAAO,ENsQ1B,GAAO;;AMrQzB,iBAAiC;EAAE,OAAO,ENvL1B,GAAO;;AMwLvB,iBAAiC;EAAE,OAAO,ENzG1B,GAAO;;AM0GvB,kBAAkC;EAAE,OAAO,ENtF1B,GAAO;;AMuFxB,sBAAsC;EAAE,OAAO,EN3B1B,GAAO;;AM4B5B,qBAAqC;EAAE,OAAO,ENxK1B,GAAO;;AMyK3B,qBAAqC;EAAE,OAAO,ENkC1B,GAAO;;AMjC3B,oBAAoC;EAAE,OAAO,EN3O1B,GAAO;;AM4O1B,iBAAiC;EAAE,OAAO,ENiG1B,GAAO;;AMhGvB,sBAAsC;EAAE,OAAO,EN/C1B,GAAO;;AMgD5B,eAA+B;EAAE,OAAO,ENpM1B,GAAO;;AMqMrB,mBAAmC;EAAE,OAAO,ENe1B,GAAO;;AMdzB,sBAAsC;EAAE,OAAO,ENgJ1B,GAAO;;AM/I5B,4BAA4C;EAAE,OAAO,EN5O1B,GAAO;;AM6OlC,6BAA6C;EAAE,OAAO,EN5O1B,GAAO;;AM6OnC,0BAA0C;EAAE,OAAO,EN5O1B,GAAO;;AM6OhC,4BAA4C;EAAE,OAAO,ENhP1B,GAAO;;AMiPlC,qBAAqC;EAAE,OAAO,EN5O1B,GAAO;;AM6O3B,sBAAsC;EAAE,OAAO,EN5O1B,GAAO;;AM6O5B,mBAAmC;EAAE,OAAO,EN5O1B,GAAO;;AM6OzB,qBAAqC;EAAE,OAAO,ENhP1B,GAAO;;AMiP3B,kBAAkC;EAAE,OAAO,ENlG1B,GAAO;;AMmGxB,iBAAiC;EAAE,OAAO,ENuC1B,GAAO;;AMtCvB,iBAAiC;EAAE,OAAO,ENoP1B,GAAO;;AMnPvB;iBACiC;EAAE,OAAO,ENyF1B,GAAO;;AMxFvB,mBAAmC;EAAE,OAAO,EN9I1B,GAAO;;AM+IzB,qBAAqC;EAAE,OAAO,EN0I1B,GAAO;;AMzI3B,sBAAsC;EAAE,OAAO,EN0I1B,GAAO;;AMzI5B,kBAAkC;EAAE,OAAO,ENgN1B,GAAO;;AM/MxB,iBAAiC;EAAE,OAAO,ENnJ1B,GAAO;;AMoJvB;gBACgC;EAAE,OAAO,ENkJ1B,GAAO;;AMjJtB,qBAAqC;EAAE,OAAO,ENnB1B,GAAO;;AMoB3B,mBAAmC;EAAE,OAAO,ENxC1B,GAAO;;AMyCzB,wBAAwC;EAAE,OAAO,ENvC1B,GAAO;;AMwC9B,kBAAkC;EAAE,OAAO,EN0L1B,GAAO;;AMzLxB,kBAAkC;EAAE,OAAO,ENpC1B,GAAO;;AMqCxB,gBAAgC;EAAE,OAAO,ENoE1B,GAAO;;AMnEtB,kBAAkC;EAAE,OAAO,ENpC1B,GAAO;;AMqCxB,qBAAqC;EAAE,OAAO,ENkB1B,GAAO;;AMjB3B,iBAAiC;EAAE,OAAO,ENrD1B,GAAO;;AMsDvB,yBAAyC;EAAE,OAAO,ENvD1B,GAAO;;AMwD/B,mBAAmC;EAAE,OAAO,ENuO1B,GAAO;;AMtOzB,eAA+B;EAAE,OAAO,ENtJ1B,GAAO;;AMuJrB;oBACoC;EAAE,OAAO,ENqI1B,GAAO;;AMpI1B;;sBAEsC;EAAE,OAAO,ENuM1B,GAAO;;AMtM5B,yBAAyC;EAAE,OAAO,ENkC1B,GAAO;;AMjC/B,eAA+B;EAAE,OAAO,EN5I1B,GAAO;;AM6IrB,oBAAoC;EAAE,OAAO,EN7J1B,GAAO;;AM8J1B;uBACuC;EAAE,OAAO,EN1L1B,GAAO;;AM2L7B,mBAAmC;EAAE,OAAO,EN4G1B,GAAO;;AM3GzB,eAA+B;EAAE,OAAO,ENT1B,GAAO;;AMUrB,sBAAsC;EAAE,OAAO,ENhH1B,GAAO;;AMiH5B,sBAAsC;EAAE,OAAO,EN8M1B,GAAO;;AM7M5B,oBAAoC;EAAE,OAAO,ENyM1B,GAAO;;AMxM1B,iBAAiC;EAAE,OAAO,ENvH1B,GAAO;;AMwHvB,uBAAuC;EAAE,OAAO,ENmG1B,GAAO;;AMlG7B,qBAAqC;EAAE,OAAO,EN8C1B,GAAO;;AM7C3B,2BAA2C;EAAE,OAAO,EN8C1B,GAAO;;AM7CjC,iBAAiC;EAAE,OAAO,ENgJ1B,GAAO;;AM/IvB,qBAAqC;EAAE,OAAO,EN5N1B,GAAO;;AM6N3B,4BAA4C;EAAE,OAAO,ENjF1B,GAAO;;AMkFlC,iBAAiC;EAAE,OAAO,ENoH1B,GAAO;;AMnHvB,iBAAiC;EAAE,OAAO,ENkC1B,GAAO;;AMjCvB,8BAA8C;EAAE,OAAO,ENlM1B,GAAO;;AMmMpC,+BAA+C;EAAE,OAAO,ENlM1B,GAAO;;AMmMrC,4BAA4C;EAAE,OAAO,ENlM1B,GAAO;;AMmMlC,8BAA8C;EAAE,OAAO,ENtM1B,GAAO;;AMuMpC,gBAAgC;EAAE,OAAO,EN/B1B,GAAO;;AMgCtB,eAA+B;EAAE,OAAO,ENjK1B,GAAO;;AMkKrB,iBAAiC;EAAE,OAAO,EN9S1B,GAAO;;AM+SvB,qBAAqC;EAAE,OAAO,ENmP1B,GAAO;;AMlP3B,mBAAmC;EAAE,OAAO,EN9O1B,GAAO;;AM+OzB,qBAAqC;EAAE,OAAO,EN/I1B,GAAO;;AMgJ3B,qBAAqC;EAAE,OAAO,EN/I1B,GAAO;;AMgJ3B,qBAAqC;EAAE,OAAO,EN4G1B,GAAO;;AM3G3B,sBAAsC;EAAE,OAAO,ENsE1B,GAAO;;AMrE5B,iBAAiC;EAAE,OAAO,EN2M1B,GAAO;;AM1MvB,uBAAuC;EAAE,OAAO,EN6B1B,GAAO;;AM5B7B,yBAAyC;EAAE,OAAO,EN6B1B,GAAO;;AM5B/B,mBAAmC;EAAE,OAAO,ENhB1B,GAAO;;AMiBzB,qBAAqC;EAAE,OAAO,ENlB1B,GAAO;;AMmB3B,uBAAuC;EAAE,OAAO,ENvN1B,GAAO;;AMwN7B,wBAAwC;EAAE,OAAO,ENiD1B,GAAO;;AMhD9B,+BAA+C;EAAE,OAAO,EN3I1B,GAAO;;AM4IrC,uBAAuC;EAAE,OAAO,ENkH1B,GAAO;;AMjH7B,kBAAkC;EAAE,OAAO,EN1L1B,GAAO;;AM2LxB;8BAC8C;EAAE,OAAO,ENjP1B,GAAO;;AMkPpC;4BAC4C;EAAE,OAAO,ENhP1B,GAAO;;AMiPlC;+BAC+C;EAAE,OAAO,ENnP1B,GAAO;;AMoPrC;cAC8B;EAAE,OAAO,EN7J1B,GAAO;;AM8JpB,cAA8B;EAAE,OAAO,EN/F1B,GAAO;;AMgGpB;cAC8B;EAAE,OAAO,EN4N1B,GAAO;;AM3NpB;cAC8B;EAAE,OAAO,ENvD1B,GAAO;;AMwDpB;;;cAG8B;EAAE,OAAO,ENrD1B,GAAO;;AMsDpB;;cAE8B;EAAE,OAAO,EN8E1B,GAAO;;AM7EpB;cAC8B;EAAE,OAAO,ENtD1B,GAAO;;AMuDpB;cAC8B;EAAE,OAAO,ENzR1B,GAAO;;AM0RpB,eAA+B;EAAE,OAAO,ENzJ1B,GAAO;;AM0JrB,oBAAoC;EAAE,OAAO,EN7I1B,GAAO;;AM8I1B,yBAAyC;EAAE,OAAO,EN2G1B,GAAO;;AM1G/B,0BAA0C;EAAE,OAAO,EN2G1B,GAAO;;AM1GhC,0BAA0C;EAAE,OAAO,EN2G1B,GAAO;;AM1GhC,2BAA2C;EAAE,OAAO,EN2G1B,GAAO;;AM1GjC,2BAA2C;EAAE,OAAO,EN8G1B,GAAO;;AM7GjC,4BAA4C;EAAE,OAAO,EN8G1B,GAAO;;AM7GlC,oBAAoC;EAAE,OAAO,ENgK1B,GAAO;;AM/J1B,sBAAsC;EAAE,OAAO,EN4J1B,GAAO;;AM3J5B,yBAAyC;EAAE,OAAO,ENwO1B,GAAO;;AMvO/B,kBAAkC;EAAE,OAAO,ENqO1B,GAAO;;AMpOxB,eAA+B;EAAE,OAAO,EN+N1B,GAAO;;AM9NrB,sBAAsC;EAAE,OAAO,EN+N1B,GAAO;;AM9N5B,uBAAuC;EAAE,OAAO,ENmO1B,GAAO;;AMlO7B,kBAAkC;EAAE,OAAO,ENxM1B,GAAO;;AMyMxB,yBAAyC;EAAE,OAAO,EN+G1B,GAAO;;AM9G/B,oBAAoC;EAAE,OAAO,ENnF1B,GAAO;;AMoF1B,iBAAiC;EAAE,OAAO,EN/I1B,GAAO;;AMgJvB,cAA8B;EAAE,OAAO,ENhX1B,GAAO;;AMiXpB,oBAAoC;EAAE,OAAO,ENxT1B,GAAO;;AMyT1B,2BAA2C;EAAE,OAAO,ENxT1B,GAAO;;AMyTjC,iBAAiC;EAAE,OAAO,ENyK1B,GAAO;;AMxKvB,wBAAwC;EAAE,OAAO,ENyK1B,GAAO;;AMxK9B,0BAA0C;EAAE,OAAO,ENtD1B,GAAO;;AMuDhC,wBAAwC;EAAE,OAAO,ENpD1B,GAAO;;AMqD9B,0BAA0C;EAAE,OAAO,ENvD1B,GAAO;;AMwDhC,2BAA2C;EAAE,OAAO,ENvD1B,GAAO;;AMwDjC,gBAAgC;EAAE,OAAO,ENxW1B,GAAO;;AMyWtB,kBAAkC;EAAE,OAAO,EN0M1B,GAAO;;AMzMxB,kBAAkC;EAAE,OAAO,ENpX1B,GAAO;;AMqXxB,gBAAgC;EAAE,OAAO,ENpE1B,GAAO;;AMqEtB,mBAAmC;EAAE,OAAO,EN1N1B,GAAO;;AM2NzB,gBAAgC;EAAE,OAAO,ENqE1B,GAAO;;AMpEtB,qBAAqC;EAAE,OAAO,ENtJ1B,GAAO;;AMuJ3B,iBAAiC;EAAE,OAAO,ENuJ1B,GAAO;;AMtJvB,iBAAiC;EAAE,OAAO,EN/L1B,GAAO;;AMgMvB,eAA+B;EAAE,OAAO,EN1D1B,GAAO;;AM2DrB;mBACmC;EAAE,OAAO,ENnI1B,GAAO;;AMoIzB,gBAAgC;EAAE,OAAO,EN2G1B,GAAO;;AM1GtB,iBAAiC;EAAE,OAAO,ENxC1B,GAAO;;AMyCvB,kBAAkC;EAAE,OAAO,ENrX1B,GAAO;;AMsXxB,cAA8B;EAAE,OAAO,ENpU1B,GAAO;;AMqUpB,aAA6B;EAAE,OAAO,ENgL1B,GAAO;;AM/KnB,gBAAgC;EAAE,OAAO,ENqL1B,GAAO;;AMpLtB,iBAAiC;EAAE,OAAO,ENa1B,GAAO;;AMZvB,oBAAoC;EAAE,OAAO,ENrC1B,GAAO;;AMsC1B,yBAAyC;EAAE,OAAO,EN8E1B,GAAO;;AM7E/B,+BAA+C;EAAE,OAAO,ENtX1B,GAAO;;AMuXrC,8BAA8C;EAAE,OAAO,ENxX1B,GAAO;;AMyXpC;8BAC8C;EAAE,OAAO,EN3T1B,GAAO;;AM4TpC,uBAAuC;EAAE,OAAO,ENjP1B,GAAO;;AMkP7B,qBAAqC;EAAE,OAAO,EN+K1B,GAAO;;AM9K3B,uBAAuC;EAAE,OAAO,ENmK1B,GAAO;;AMlK7B;cAC8B;EAAE,OAAO,ENoI1B,GAAO;;AMnIpB,wBAAwC;EAAE,OAAO,ENjB1B,GAAO;;AMkB9B,wBAAwC;EAAE,OAAO,EN6D1B,GAAO;;AM5D9B,gBAAgC;EAAE,OAAO,EN2C1B,GAAO;;AM1CtB,0BAA0C;EAAE,OAAO,EN7O1B,GAAO;;AM8OhC,oBAAoC;EAAE,OAAO,EN2K1B,GAAO;;AM1K1B,iBAAiC;EAAE,OAAO,ENvD1B,GAAO;;AMwDvB;;qBAEqC;EAAE,OAAO,ENsI1B,GAAO;;AMrI3B;yBACyC;EAAE,OAAO,ENjK1B,GAAO;;AMkK/B,gBAAgC;EAAE,OAAO,ENwK1B,GAAO;;AMvKtB,iBAAiC;EAAE,OAAO,ENvK1B,GAAO;;AMwKvB,iBAAiC;EAAE,OAAO,ENhB1B,GAAO;;AMiBvB,wBAAwC;EAAE,OAAO,ENhB1B,GAAO;;AMiB9B,6BAA6C;EAAE,OAAO,ENsE1B,GAAO;;AMrEnC,sBAAsC;EAAE,OAAO,ENoE1B,GAAO;;AMnE5B,oBAAoC;EAAE,OAAO,EN7Q1B,GAAO;;AM8Q1B,eAA+B;EAAE,OAAO,EN1Q1B,GAAO;;AM2QrB,qBAAqC;EAAE,OAAO,ENjD1B,GAAO;;AMkD3B,yBAAyC;EAAE,OAAO,ENjD1B,GAAO;;AMkD/B,iBAAiC;EAAE,OAAO,ENvQ1B,GAAO;;AMwQvB,iBAAiC;EAAE,OAAO,EN9I1B,GAAO;;AM+IvB,mBAAmC;EAAE,OAAO,ENzI1B,GAAO;;AM0IzB,cAA8B;EAAE,OAAO,EN9O1B,GAAO;;AM+OpB,mBAAmC;EAAE,OAAO,EN3W1B,GAAO;;AM4WzB,gBAAgC;EAAE,OAAO,EN9T1B,GAAO;;AM+TtB,cAA8B;EAAE,OAAO,ENnE1B,GAAO;;AMoEpB,gBAAgC;EAAE,OAAO,ENoC1B,GAAO;;AMnCtB,eAA+B;EAAE,OAAO,ENjS1B,GAAO;;AMkSrB,gBAAgC;EAAE,OAAO,ENjS1B,GAAO;;AMkStB,kBAAkC;EAAE,OAAO,ENtY1B,GAAO;;AMuYxB,yBAAyC;EAAE,OAAO,ENtY1B,GAAO;;AMuY/B,gBAAgC;EAAE,OAAO,EN2C1B,GAAO;;AM1CtB,uBAAuC;EAAE,OAAO,EN2C1B,GAAO;;AM1C7B,kBAAkC;EAAE,OAAO,ENvC1B,GAAO;;AMwCxB;cAC8B;EAAE,OAAO,EN3W1B,GAAO;;AM4WpB;eAC+B;EAAE,OAAO,EN2D1B,GAAO;;AM1DrB,eAA+B;EAAE,OAAO,ENuF1B,GAAO;;AMtFrB,kBAAkC;EAAE,OAAO,ENwB1B,GAAO;;AMvBxB,qBAAqC;EAAE,OAAO,ENpS1B,GAAO;;AMqS3B,qBAAqC;EAAE,OAAO,ENkB1B,GAAO;;AMjB3B,mBAAmC;EAAE,OAAO,EN1S1B,GAAO;;AM2SzB,qBAAqC;EAAE,OAAO,ENxP1B,GAAO;;AMyP3B,sBAAsC;EAAE,OAAO,ENjP1B,GAAO;;AMkP5B,uBAAuC;EAAE,OAAO,EN9P1B,GAAO;;AM+P7B,4BAA4C;EAAE,OAAO,ENxP1B,GAAO;;AMyPlC;;uBAEuC;EAAE,OAAO,ENjQ1B,GAAO;;AMkQ7B;yBACyC;EAAE,OAAO,ENvQ1B,GAAO;;AMwQ/B;uBACuC;EAAE,OAAO,ENxQ1B,GAAO;;AMyQ7B;uBACuC;EAAE,OAAO,EN7P1B,GAAO;;AM8P7B,sBAAsC;EAAE,OAAO,EN1Q1B,GAAO;;AM2Q5B,eAA+B;EAAE,OAAO,ENsG1B,GAAO;;AMrGrB,kBAAkC;EAAE,OAAO,ENlV1B,GAAO;;AMmVxB,mBAAmC;EAAE,OAAO,ENnL1B,GAAO;;AMoLzB;;;;oBAIoC;EAAE,OAAO,ENxK1B,GAAO;;AMyK1B,yBAAyC;EAAE,OAAO,ENpW1B,GAAO;;AMqW/B;gBACgC;EAAE,OAAO,EN1E1B,GAAO;;AM2EtB;iBACiC;EAAE,OAAO,ENpT1B,GAAO;;AMqTvB,qBAAqC;EAAE,OAAO,EN1O1B,GAAO;;AM2O3B,cAA8B;EAAE,OAAO,EN5O1B,GAAO;;AM6OpB,sBAAsC;EAAE,OAAO,EN7N1B,GAAO;;AM8N5B,wBAAwC;EAAE,OAAO,ENwB1B,GAAO;;AMvB9B,aAA6B;EAAE,OAAO,ENzF1B,GAAO;;AM0FnB;iBACiC;EAAE,OAAO,EN2F1B,GAAO;;AM1FvB;sBACsC;EAAE,OAAO,EN9H1B,GAAO;;AM+H5B;wBACwC;EAAE,OAAO,EN/H1B,GAAO;;AMgI9B,kBAAkC;EAAE,OAAO,EN3N1B,GAAO;;AM4NxB;sBACsC;EAAE,OAAO,ENrX1B,GAAO;;AMsX5B,iBAAiC;EAAE,OAAO,ENnO1B,GAAO;;AMoOvB,oBAAoC;EAAE,OAAO,ENlI1B,GAAO;;AMmI1B,kBAAkC;EAAE,OAAO,EN1C1B,GAAO;;AM2CxB,oBAAoC;EAAE,OAAO,EN7D1B,GAAO;;AM8D1B,2BAA2C;EAAE,OAAO,EN7D1B,GAAO;;AM8DjC,eAA+B;EAAE,OAAO,ENpb1B,GAAO;;AMqbrB;mBACmC;EAAE,OAAO,ENzQ1B,GAAO;;AM0QzB,cAA8B;EAAE,OAAO,ENsC1B,GAAO;;AMrCpB,qBAAqC;EAAE,OAAO,EN/b1B,GAAO;;AMgc3B,eAA+B;EAAE,OAAO,ENrH1B,GAAO;;AMsHrB,qBAAqC;EAAE,OAAO,ENlD1B,GAAO;;AMmD3B,iBAAiC;EAAE,OAAO,ENsC1B,GAAO;;AMrCvB,eAA+B;EAAE,OAAO,ENiF1B,GAAO;;AMhFrB,sBAAsC;EAAE,OAAO,ENvJ1B,GAAO;;AMwJ5B,eAA+B;EAAE,OAAO,ENuE1B,GAAO;;AMtErB,qBAAqC;EAAE,OAAO,ENjb1B,GAAO;;AMkb3B,iBAAiC;EAAE,OAAO,EN9I1B,GAAO;;AM+IvB,wBAAwC;EAAE,OAAO,ENhQ1B,GAAO;;AMiQ9B,kBAAkC;EAAE,OAAO,EN9Z1B,GAAO;;AM+ZxB,wBAAwC;EAAE,OAAO,ENla1B,GAAO;;AMma9B,sBAAsC;EAAE,OAAO,ENpa1B,GAAO;;AMqa5B,kBAAkC;EAAE,OAAO,ENta1B,GAAO;;AMuaxB,oBAAoC;EAAE,OAAO,ENpa1B,GAAO;;AMqa1B,oBAAoC;EAAE,OAAO,ENpa1B,GAAO;;AMqa1B,qBAAqC;EAAE,OAAO,ENld1B,GAAO;;AMmd3B,uBAAuC;EAAE,OAAO,ENld1B,GAAO;;AMmd7B,gBAAgC;EAAE,OAAO,ENY1B,GAAO;;AMXtB,oBAAoC;EAAE,OAAO,EN3X1B,GAAO;;AM4X1B,aAA6B;EAAE,OAAO,ENre1B,GAAO;;AMsenB,qBAAqC;EAAE,OAAO,ENjV1B,GAAO;;AMkV3B,sBAAsC;EAAE,OAAO,ENpK1B,GAAO;;AMqK5B,wBAAwC;EAAE,OAAO,ENrd1B,GAAO;;AMsd9B,qBAAqC;EAAE,OAAO,EN3f1B,GAAO;;AM4f3B,oBAAoC;EAAE,OAAO,ENvJ1B,GAAO;;AMwJ1B,qBAAqC;EAAE,OAAO,EN5N1B,GAAO;;AM6N3B,iBAAiC;EAAE,OAAO,EN1O1B,GAAO;;AM2OvB,wBAAwC;EAAE,OAAO,EN1O1B,GAAO;;AM2O9B,qBAAqC;EAAE,OAAO,ENN1B,GAAO;;AMO3B,oBAAoC;EAAE,OAAO,ENN1B,GAAO;;AMO1B,kBAAkC;EAAE,OAAO,EN/d1B,GAAO;;AMgexB,cAA8B;EAAE,OAAO,EN7c1B,GAAO;;AM8cpB,kBAAkC;EAAE,OAAO,EN1P1B,GAAO;;AM2PxB,oBAAoC;EAAE,OAAO,ENhhB1B,GAAO;;AMihB1B,aAA6B;EAAE,OAAO,EN7b1B,GAAO;;AM8bnB;;cAE8B;EAAE,OAAO,ENxQ1B,GAAO;;AMyQpB,mBAAmC;EAAE,OAAO,EN7M1B,GAAO;;AM8MzB,qBAAqC;EAAE,OAAO,ENpd1B,GAAO;;AMqd3B,yBAAyC;EAAE,OAAO,ENnZ1B,GAAO;;AMoZ/B,mBAAmC;EAAE,OAAO,ENxY1B,GAAO;;AMyYzB,mBAAmC;EAAE,OAAO,EN1T1B,GAAO;;AM2TzB,kBAAkC;EAAE,OAAO,ENxP1B,GAAO;;AMyPxB,iBAAiC;EAAE,OAAO,ENrH1B,GAAO;;AMsHvB,uBAAuC;EAAE,OAAO,ENzG1B,GAAO;;AM0G7B,sBAAsC;EAAE,OAAO,ENrG1B,GAAO;;AMsG5B,mBAAmC;EAAE,OAAO,ENpG1B,GAAO;;AMqGzB,oBAAoC;EAAE,OAAO,EN5c1B,GAAO;;AM6c1B,0BAA0C;EAAE,OAAO,EN9c1B,GAAO;;AM+chC,kBAAkC;EAAE,OAAO,EN3Y1B,GAAO;;AM4YxB,eAA+B;EAAE,OAAO,ENhH1B,GAAO;;AMiHrB,sBAAsC;EAAE,OAAO,ENI1B,GAAO;;AMH5B,qBAAqC;EAAE,OAAO,EN5M1B,GAAO;;AM6M3B,sBAAsC;EAAE,OAAO,ENpE1B,GAAO;;AMqE5B,oBAAoC;EAAE,OAAO,ENhS1B,GAAO;;AMiS1B,gBAAgC;EAAE,OAAO,ENG1B,GAAO;;AMFtB,eAA+B;EAAE,OAAO,ENtO1B,GAAO;;AMuOrB,kBAAkC;EAAE,OAAO,EN7N1B,GAAO;;AM8NxB,sBAAsC;EAAE,OAAO,ENhC1B,GAAO;;AMiC5B,0BAA0C;EAAE,OAAO,ENhC1B,GAAO;;AMiChC,uBAAuC;EAAE,OAAO,END1B,GAAO;;AME7B,sBAAsC;EAAE,OAAO,EN1O1B,GAAO;;AM2O5B,qBAAqC;EAAE,OAAO,ENF1B,GAAO;;AMG3B,sBAAsC;EAAE,OAAO,EN3O1B,GAAO;;AM4O5B,wBAAwC;EAAE,OAAO,EN1O1B,GAAO;;AM2O9B,wBAAwC;EAAE,OAAO,EN5O1B,GAAO;;AM6O9B,iBAAiC;EAAE,OAAO,ENvN1B,GAAO;;AMwNvB,4BAA4C;EAAE,OAAO,EN9X1B,GAAO;;AM+XlC,sBAAsC;EAAE,OAAO,ENhM1B,GAAO;;AMiM5B,mBAAmC;EAAE,OAAO,ENI1B,GAAO;;AMHzB,iBAAiC;EAAE,OAAO,EN7I1B,GAAO;;AM8IvB,oBAAoC;EAAE,OAAO,ENjB1B,GAAO;;AMkB1B,qBAAqC;EAAE,OAAO,ENhB1B,GAAO;;AMiB3B;cAC8B;EAAE,OAAO,ENphB1B,GAAO;;AMqhBpB,kBAAkC;EAAE,OAAO,ENd1B,GAAO;;AMexB,gBAAgC;EAAE,OAAO,ENnD1B,GAAO;;AMoDtB,iBAAiC;EAAE,OAAO,ENvF1B,GAAO;;AMwFvB,iBAAiC;EAAE,OAAO,ENrP1B,GAAO", +"sources": ["../scss/_path.scss","../scss/_core.scss","../scss/_larger.scss","../scss/_fixed-width.scss","../scss/_list.scss","../scss/_variables.scss","../scss/_bordered-pulled.scss","../scss/_animated.scss","../scss/_rotated-flipped.scss","../scss/_mixins.scss","../scss/_stacked.scss","../scss/_icons.scss"], +"names": [], +"file": "font-awesome.css" +} diff --git a/lib/font-awesome/css/font-awesome.min.css b/lib/font-awesome/css/font-awesome.min.css new file mode 100644 index 0000000000..540440ce89 --- /dev/null +++ b/lib/font-awesome/css/font-awesome.min.css @@ -0,0 +1,4 @@ +/*! + * Font Awesome 4.7.0 by @davegandy - http://fontawesome.io - @fontawesome + * License - http://fontawesome.io/license (Font: SIL OFL 1.1, CSS: MIT License) + */@font-face{font-family:'FontAwesome';src:url('../fonts/fontawesome-webfont.eot?v=4.7.0');src:url('../fonts/fontawesome-webfont.eot?#iefix&v=4.7.0') format('embedded-opentype'),url('../fonts/fontawesome-webfont.woff2?v=4.7.0') format('woff2'),url('../fonts/fontawesome-webfont.woff?v=4.7.0') format('woff'),url('../fonts/fontawesome-webfont.ttf?v=4.7.0') format('truetype'),url('../fonts/fontawesome-webfont.svg?v=4.7.0#fontawesomeregular') format('svg');font-weight:normal;font-style:normal}.fa{display:inline-block;font:normal normal normal 14px/1 FontAwesome;font-size:inherit;text-rendering:auto;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.fa-lg{font-size:1.33333333em;line-height:.75em;vertical-align:-15%}.fa-2x{font-size:2em}.fa-3x{font-size:3em}.fa-4x{font-size:4em}.fa-5x{font-size:5em}.fa-fw{width:1.28571429em;text-align:center}.fa-ul{padding-left:0;margin-left:2.14285714em;list-style-type:none}.fa-ul>li{position:relative}.fa-li{position:absolute;left:-2.14285714em;width:2.14285714em;top:.14285714em;text-align:center}.fa-li.fa-lg{left:-1.85714286em}.fa-border{padding:.2em .25em .15em;border:solid .08em #eee;border-radius:.1em}.fa-pull-left{float:left}.fa-pull-right{float:right}.fa.fa-pull-left{margin-right:.3em}.fa.fa-pull-right{margin-left:.3em}.pull-right{float:right}.pull-left{float:left}.fa.pull-left{margin-right:.3em}.fa.pull-right{margin-left:.3em}.fa-spin{-webkit-animation:fa-spin 2s infinite linear;animation:fa-spin 2s infinite linear}.fa-pulse{-webkit-animation:fa-spin 1s infinite steps(8);animation:fa-spin 1s infinite steps(8)}@-webkit-keyframes fa-spin{0%{-webkit-transform:rotate(0deg);transform:rotate(0deg)}100%{-webkit-transform:rotate(359deg);transform:rotate(359deg)}}@keyframes fa-spin{0%{-webkit-transform:rotate(0deg);transform:rotate(0deg)}100%{-webkit-transform:rotate(359deg);transform:rotate(359deg)}}.fa-rotate-90{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=1)";-webkit-transform:rotate(90deg);-ms-transform:rotate(90deg);transform:rotate(90deg)}.fa-rotate-180{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=2)";-webkit-transform:rotate(180deg);-ms-transform:rotate(180deg);transform:rotate(180deg)}.fa-rotate-270{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=3)";-webkit-transform:rotate(270deg);-ms-transform:rotate(270deg);transform:rotate(270deg)}.fa-flip-horizontal{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=0, mirror=1)";-webkit-transform:scale(-1, 1);-ms-transform:scale(-1, 1);transform:scale(-1, 1)}.fa-flip-vertical{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=2, mirror=1)";-webkit-transform:scale(1, -1);-ms-transform:scale(1, -1);transform:scale(1, -1)}:root .fa-rotate-90,:root .fa-rotate-180,:root .fa-rotate-270,:root .fa-flip-horizontal,:root .fa-flip-vertical{filter:none}.fa-stack{position:relative;display:inline-block;width:2em;height:2em;line-height:2em;vertical-align:middle}.fa-stack-1x,.fa-stack-2x{position:absolute;left:0;width:100%;text-align:center}.fa-stack-1x{line-height:inherit}.fa-stack-2x{font-size:2em}.fa-inverse{color:#fff}.fa-glass:before{content:"\f000"}.fa-music:before{content:"\f001"}.fa-search:before{content:"\f002"}.fa-envelope-o:before{content:"\f003"}.fa-heart:before{content:"\f004"}.fa-star:before{content:"\f005"}.fa-star-o:before{content:"\f006"}.fa-user:before{content:"\f007"}.fa-film:before{content:"\f008"}.fa-th-large:before{content:"\f009"}.fa-th:before{content:"\f00a"}.fa-th-list:before{content:"\f00b"}.fa-check:before{content:"\f00c"}.fa-remove:before,.fa-close:before,.fa-times:before{content:"\f00d"}.fa-search-plus:before{content:"\f00e"}.fa-search-minus:before{content:"\f010"}.fa-power-off:before{content:"\f011"}.fa-signal:before{content:"\f012"}.fa-gear:before,.fa-cog:before{content:"\f013"}.fa-trash-o:before{content:"\f014"}.fa-home:before{content:"\f015"}.fa-file-o:before{content:"\f016"}.fa-clock-o:before{content:"\f017"}.fa-road:before{content:"\f018"}.fa-download:before{content:"\f019"}.fa-arrow-circle-o-down:before{content:"\f01a"}.fa-arrow-circle-o-up:before{content:"\f01b"}.fa-inbox:before{content:"\f01c"}.fa-play-circle-o:before{content:"\f01d"}.fa-rotate-right:before,.fa-repeat:before{content:"\f01e"}.fa-refresh:before{content:"\f021"}.fa-list-alt:before{content:"\f022"}.fa-lock:before{content:"\f023"}.fa-flag:before{content:"\f024"}.fa-headphones:before{content:"\f025"}.fa-volume-off:before{content:"\f026"}.fa-volume-down:before{content:"\f027"}.fa-volume-up:before{content:"\f028"}.fa-qrcode:before{content:"\f029"}.fa-barcode:before{content:"\f02a"}.fa-tag:before{content:"\f02b"}.fa-tags:before{content:"\f02c"}.fa-book:before{content:"\f02d"}.fa-bookmark:before{content:"\f02e"}.fa-print:before{content:"\f02f"}.fa-camera:before{content:"\f030"}.fa-font:before{content:"\f031"}.fa-bold:before{content:"\f032"}.fa-italic:before{content:"\f033"}.fa-text-height:before{content:"\f034"}.fa-text-width:before{content:"\f035"}.fa-align-left:before{content:"\f036"}.fa-align-center:before{content:"\f037"}.fa-align-right:before{content:"\f038"}.fa-align-justify:before{content:"\f039"}.fa-list:before{content:"\f03a"}.fa-dedent:before,.fa-outdent:before{content:"\f03b"}.fa-indent:before{content:"\f03c"}.fa-video-camera:before{content:"\f03d"}.fa-photo:before,.fa-image:before,.fa-picture-o:before{content:"\f03e"}.fa-pencil:before{content:"\f040"}.fa-map-marker:before{content:"\f041"}.fa-adjust:before{content:"\f042"}.fa-tint:before{content:"\f043"}.fa-edit:before,.fa-pencil-square-o:before{content:"\f044"}.fa-share-square-o:before{content:"\f045"}.fa-check-square-o:before{content:"\f046"}.fa-arrows:before{content:"\f047"}.fa-step-backward:before{content:"\f048"}.fa-fast-backward:before{content:"\f049"}.fa-backward:before{content:"\f04a"}.fa-play:before{content:"\f04b"}.fa-pause:before{content:"\f04c"}.fa-stop:before{content:"\f04d"}.fa-forward:before{content:"\f04e"}.fa-fast-forward:before{content:"\f050"}.fa-step-forward:before{content:"\f051"}.fa-eject:before{content:"\f052"}.fa-chevron-left:before{content:"\f053"}.fa-chevron-right:before{content:"\f054"}.fa-plus-circle:before{content:"\f055"}.fa-minus-circle:before{content:"\f056"}.fa-times-circle:before{content:"\f057"}.fa-check-circle:before{content:"\f058"}.fa-question-circle:before{content:"\f059"}.fa-info-circle:before{content:"\f05a"}.fa-crosshairs:before{content:"\f05b"}.fa-times-circle-o:before{content:"\f05c"}.fa-check-circle-o:before{content:"\f05d"}.fa-ban:before{content:"\f05e"}.fa-arrow-left:before{content:"\f060"}.fa-arrow-right:before{content:"\f061"}.fa-arrow-up:before{content:"\f062"}.fa-arrow-down:before{content:"\f063"}.fa-mail-forward:before,.fa-share:before{content:"\f064"}.fa-expand:before{content:"\f065"}.fa-compress:before{content:"\f066"}.fa-plus:before{content:"\f067"}.fa-minus:before{content:"\f068"}.fa-asterisk:before{content:"\f069"}.fa-exclamation-circle:before{content:"\f06a"}.fa-gift:before{content:"\f06b"}.fa-leaf:before{content:"\f06c"}.fa-fire:before{content:"\f06d"}.fa-eye:before{content:"\f06e"}.fa-eye-slash:before{content:"\f070"}.fa-warning:before,.fa-exclamation-triangle:before{content:"\f071"}.fa-plane:before{content:"\f072"}.fa-calendar:before{content:"\f073"}.fa-random:before{content:"\f074"}.fa-comment:before{content:"\f075"}.fa-magnet:before{content:"\f076"}.fa-chevron-up:before{content:"\f077"}.fa-chevron-down:before{content:"\f078"}.fa-retweet:before{content:"\f079"}.fa-shopping-cart:before{content:"\f07a"}.fa-folder:before{content:"\f07b"}.fa-folder-open:before{content:"\f07c"}.fa-arrows-v:before{content:"\f07d"}.fa-arrows-h:before{content:"\f07e"}.fa-bar-chart-o:before,.fa-bar-chart:before{content:"\f080"}.fa-twitter-square:before{content:"\f081"}.fa-facebook-square:before{content:"\f082"}.fa-camera-retro:before{content:"\f083"}.fa-key:before{content:"\f084"}.fa-gears:before,.fa-cogs:before{content:"\f085"}.fa-comments:before{content:"\f086"}.fa-thumbs-o-up:before{content:"\f087"}.fa-thumbs-o-down:before{content:"\f088"}.fa-star-half:before{content:"\f089"}.fa-heart-o:before{content:"\f08a"}.fa-sign-out:before{content:"\f08b"}.fa-linkedin-square:before{content:"\f08c"}.fa-thumb-tack:before{content:"\f08d"}.fa-external-link:before{content:"\f08e"}.fa-sign-in:before{content:"\f090"}.fa-trophy:before{content:"\f091"}.fa-github-square:before{content:"\f092"}.fa-upload:before{content:"\f093"}.fa-lemon-o:before{content:"\f094"}.fa-phone:before{content:"\f095"}.fa-square-o:before{content:"\f096"}.fa-bookmark-o:before{content:"\f097"}.fa-phone-square:before{content:"\f098"}.fa-twitter:before{content:"\f099"}.fa-facebook-f:before,.fa-facebook:before{content:"\f09a"}.fa-github:before{content:"\f09b"}.fa-unlock:before{content:"\f09c"}.fa-credit-card:before{content:"\f09d"}.fa-feed:before,.fa-rss:before{content:"\f09e"}.fa-hdd-o:before{content:"\f0a0"}.fa-bullhorn:before{content:"\f0a1"}.fa-bell:before{content:"\f0f3"}.fa-certificate:before{content:"\f0a3"}.fa-hand-o-right:before{content:"\f0a4"}.fa-hand-o-left:before{content:"\f0a5"}.fa-hand-o-up:before{content:"\f0a6"}.fa-hand-o-down:before{content:"\f0a7"}.fa-arrow-circle-left:before{content:"\f0a8"}.fa-arrow-circle-right:before{content:"\f0a9"}.fa-arrow-circle-up:before{content:"\f0aa"}.fa-arrow-circle-down:before{content:"\f0ab"}.fa-globe:before{content:"\f0ac"}.fa-wrench:before{content:"\f0ad"}.fa-tasks:before{content:"\f0ae"}.fa-filter:before{content:"\f0b0"}.fa-briefcase:before{content:"\f0b1"}.fa-arrows-alt:before{content:"\f0b2"}.fa-group:before,.fa-users:before{content:"\f0c0"}.fa-chain:before,.fa-link:before{content:"\f0c1"}.fa-cloud:before{content:"\f0c2"}.fa-flask:before{content:"\f0c3"}.fa-cut:before,.fa-scissors:before{content:"\f0c4"}.fa-copy:before,.fa-files-o:before{content:"\f0c5"}.fa-paperclip:before{content:"\f0c6"}.fa-save:before,.fa-floppy-o:before{content:"\f0c7"}.fa-square:before{content:"\f0c8"}.fa-navicon:before,.fa-reorder:before,.fa-bars:before{content:"\f0c9"}.fa-list-ul:before{content:"\f0ca"}.fa-list-ol:before{content:"\f0cb"}.fa-strikethrough:before{content:"\f0cc"}.fa-underline:before{content:"\f0cd"}.fa-table:before{content:"\f0ce"}.fa-magic:before{content:"\f0d0"}.fa-truck:before{content:"\f0d1"}.fa-pinterest:before{content:"\f0d2"}.fa-pinterest-square:before{content:"\f0d3"}.fa-google-plus-square:before{content:"\f0d4"}.fa-google-plus:before{content:"\f0d5"}.fa-money:before{content:"\f0d6"}.fa-caret-down:before{content:"\f0d7"}.fa-caret-up:before{content:"\f0d8"}.fa-caret-left:before{content:"\f0d9"}.fa-caret-right:before{content:"\f0da"}.fa-columns:before{content:"\f0db"}.fa-unsorted:before,.fa-sort:before{content:"\f0dc"}.fa-sort-down:before,.fa-sort-desc:before{content:"\f0dd"}.fa-sort-up:before,.fa-sort-asc:before{content:"\f0de"}.fa-envelope:before{content:"\f0e0"}.fa-linkedin:before{content:"\f0e1"}.fa-rotate-left:before,.fa-undo:before{content:"\f0e2"}.fa-legal:before,.fa-gavel:before{content:"\f0e3"}.fa-dashboard:before,.fa-tachometer:before{content:"\f0e4"}.fa-comment-o:before{content:"\f0e5"}.fa-comments-o:before{content:"\f0e6"}.fa-flash:before,.fa-bolt:before{content:"\f0e7"}.fa-sitemap:before{content:"\f0e8"}.fa-umbrella:before{content:"\f0e9"}.fa-paste:before,.fa-clipboard:before{content:"\f0ea"}.fa-lightbulb-o:before{content:"\f0eb"}.fa-exchange:before{content:"\f0ec"}.fa-cloud-download:before{content:"\f0ed"}.fa-cloud-upload:before{content:"\f0ee"}.fa-user-md:before{content:"\f0f0"}.fa-stethoscope:before{content:"\f0f1"}.fa-suitcase:before{content:"\f0f2"}.fa-bell-o:before{content:"\f0a2"}.fa-coffee:before{content:"\f0f4"}.fa-cutlery:before{content:"\f0f5"}.fa-file-text-o:before{content:"\f0f6"}.fa-building-o:before{content:"\f0f7"}.fa-hospital-o:before{content:"\f0f8"}.fa-ambulance:before{content:"\f0f9"}.fa-medkit:before{content:"\f0fa"}.fa-fighter-jet:before{content:"\f0fb"}.fa-beer:before{content:"\f0fc"}.fa-h-square:before{content:"\f0fd"}.fa-plus-square:before{content:"\f0fe"}.fa-angle-double-left:before{content:"\f100"}.fa-angle-double-right:before{content:"\f101"}.fa-angle-double-up:before{content:"\f102"}.fa-angle-double-down:before{content:"\f103"}.fa-angle-left:before{content:"\f104"}.fa-angle-right:before{content:"\f105"}.fa-angle-up:before{content:"\f106"}.fa-angle-down:before{content:"\f107"}.fa-desktop:before{content:"\f108"}.fa-laptop:before{content:"\f109"}.fa-tablet:before{content:"\f10a"}.fa-mobile-phone:before,.fa-mobile:before{content:"\f10b"}.fa-circle-o:before{content:"\f10c"}.fa-quote-left:before{content:"\f10d"}.fa-quote-right:before{content:"\f10e"}.fa-spinner:before{content:"\f110"}.fa-circle:before{content:"\f111"}.fa-mail-reply:before,.fa-reply:before{content:"\f112"}.fa-github-alt:before{content:"\f113"}.fa-folder-o:before{content:"\f114"}.fa-folder-open-o:before{content:"\f115"}.fa-smile-o:before{content:"\f118"}.fa-frown-o:before{content:"\f119"}.fa-meh-o:before{content:"\f11a"}.fa-gamepad:before{content:"\f11b"}.fa-keyboard-o:before{content:"\f11c"}.fa-flag-o:before{content:"\f11d"}.fa-flag-checkered:before{content:"\f11e"}.fa-terminal:before{content:"\f120"}.fa-code:before{content:"\f121"}.fa-mail-reply-all:before,.fa-reply-all:before{content:"\f122"}.fa-star-half-empty:before,.fa-star-half-full:before,.fa-star-half-o:before{content:"\f123"}.fa-location-arrow:before{content:"\f124"}.fa-crop:before{content:"\f125"}.fa-code-fork:before{content:"\f126"}.fa-unlink:before,.fa-chain-broken:before{content:"\f127"}.fa-question:before{content:"\f128"}.fa-info:before{content:"\f129"}.fa-exclamation:before{content:"\f12a"}.fa-superscript:before{content:"\f12b"}.fa-subscript:before{content:"\f12c"}.fa-eraser:before{content:"\f12d"}.fa-puzzle-piece:before{content:"\f12e"}.fa-microphone:before{content:"\f130"}.fa-microphone-slash:before{content:"\f131"}.fa-shield:before{content:"\f132"}.fa-calendar-o:before{content:"\f133"}.fa-fire-extinguisher:before{content:"\f134"}.fa-rocket:before{content:"\f135"}.fa-maxcdn:before{content:"\f136"}.fa-chevron-circle-left:before{content:"\f137"}.fa-chevron-circle-right:before{content:"\f138"}.fa-chevron-circle-up:before{content:"\f139"}.fa-chevron-circle-down:before{content:"\f13a"}.fa-html5:before{content:"\f13b"}.fa-css3:before{content:"\f13c"}.fa-anchor:before{content:"\f13d"}.fa-unlock-alt:before{content:"\f13e"}.fa-bullseye:before{content:"\f140"}.fa-ellipsis-h:before{content:"\f141"}.fa-ellipsis-v:before{content:"\f142"}.fa-rss-square:before{content:"\f143"}.fa-play-circle:before{content:"\f144"}.fa-ticket:before{content:"\f145"}.fa-minus-square:before{content:"\f146"}.fa-minus-square-o:before{content:"\f147"}.fa-level-up:before{content:"\f148"}.fa-level-down:before{content:"\f149"}.fa-check-square:before{content:"\f14a"}.fa-pencil-square:before{content:"\f14b"}.fa-external-link-square:before{content:"\f14c"}.fa-share-square:before{content:"\f14d"}.fa-compass:before{content:"\f14e"}.fa-toggle-down:before,.fa-caret-square-o-down:before{content:"\f150"}.fa-toggle-up:before,.fa-caret-square-o-up:before{content:"\f151"}.fa-toggle-right:before,.fa-caret-square-o-right:before{content:"\f152"}.fa-euro:before,.fa-eur:before{content:"\f153"}.fa-gbp:before{content:"\f154"}.fa-dollar:before,.fa-usd:before{content:"\f155"}.fa-rupee:before,.fa-inr:before{content:"\f156"}.fa-cny:before,.fa-rmb:before,.fa-yen:before,.fa-jpy:before{content:"\f157"}.fa-ruble:before,.fa-rouble:before,.fa-rub:before{content:"\f158"}.fa-won:before,.fa-krw:before{content:"\f159"}.fa-bitcoin:before,.fa-btc:before{content:"\f15a"}.fa-file:before{content:"\f15b"}.fa-file-text:before{content:"\f15c"}.fa-sort-alpha-asc:before{content:"\f15d"}.fa-sort-alpha-desc:before{content:"\f15e"}.fa-sort-amount-asc:before{content:"\f160"}.fa-sort-amount-desc:before{content:"\f161"}.fa-sort-numeric-asc:before{content:"\f162"}.fa-sort-numeric-desc:before{content:"\f163"}.fa-thumbs-up:before{content:"\f164"}.fa-thumbs-down:before{content:"\f165"}.fa-youtube-square:before{content:"\f166"}.fa-youtube:before{content:"\f167"}.fa-xing:before{content:"\f168"}.fa-xing-square:before{content:"\f169"}.fa-youtube-play:before{content:"\f16a"}.fa-dropbox:before{content:"\f16b"}.fa-stack-overflow:before{content:"\f16c"}.fa-instagram:before{content:"\f16d"}.fa-flickr:before{content:"\f16e"}.fa-adn:before{content:"\f170"}.fa-bitbucket:before{content:"\f171"}.fa-bitbucket-square:before{content:"\f172"}.fa-tumblr:before{content:"\f173"}.fa-tumblr-square:before{content:"\f174"}.fa-long-arrow-down:before{content:"\f175"}.fa-long-arrow-up:before{content:"\f176"}.fa-long-arrow-left:before{content:"\f177"}.fa-long-arrow-right:before{content:"\f178"}.fa-apple:before{content:"\f179"}.fa-windows:before{content:"\f17a"}.fa-android:before{content:"\f17b"}.fa-linux:before{content:"\f17c"}.fa-dribbble:before{content:"\f17d"}.fa-skype:before{content:"\f17e"}.fa-foursquare:before{content:"\f180"}.fa-trello:before{content:"\f181"}.fa-female:before{content:"\f182"}.fa-male:before{content:"\f183"}.fa-gittip:before,.fa-gratipay:before{content:"\f184"}.fa-sun-o:before{content:"\f185"}.fa-moon-o:before{content:"\f186"}.fa-archive:before{content:"\f187"}.fa-bug:before{content:"\f188"}.fa-vk:before{content:"\f189"}.fa-weibo:before{content:"\f18a"}.fa-renren:before{content:"\f18b"}.fa-pagelines:before{content:"\f18c"}.fa-stack-exchange:before{content:"\f18d"}.fa-arrow-circle-o-right:before{content:"\f18e"}.fa-arrow-circle-o-left:before{content:"\f190"}.fa-toggle-left:before,.fa-caret-square-o-left:before{content:"\f191"}.fa-dot-circle-o:before{content:"\f192"}.fa-wheelchair:before{content:"\f193"}.fa-vimeo-square:before{content:"\f194"}.fa-turkish-lira:before,.fa-try:before{content:"\f195"}.fa-plus-square-o:before{content:"\f196"}.fa-space-shuttle:before{content:"\f197"}.fa-slack:before{content:"\f198"}.fa-envelope-square:before{content:"\f199"}.fa-wordpress:before{content:"\f19a"}.fa-openid:before{content:"\f19b"}.fa-institution:before,.fa-bank:before,.fa-university:before{content:"\f19c"}.fa-mortar-board:before,.fa-graduation-cap:before{content:"\f19d"}.fa-yahoo:before{content:"\f19e"}.fa-google:before{content:"\f1a0"}.fa-reddit:before{content:"\f1a1"}.fa-reddit-square:before{content:"\f1a2"}.fa-stumbleupon-circle:before{content:"\f1a3"}.fa-stumbleupon:before{content:"\f1a4"}.fa-delicious:before{content:"\f1a5"}.fa-digg:before{content:"\f1a6"}.fa-pied-piper-pp:before{content:"\f1a7"}.fa-pied-piper-alt:before{content:"\f1a8"}.fa-drupal:before{content:"\f1a9"}.fa-joomla:before{content:"\f1aa"}.fa-language:before{content:"\f1ab"}.fa-fax:before{content:"\f1ac"}.fa-building:before{content:"\f1ad"}.fa-child:before{content:"\f1ae"}.fa-paw:before{content:"\f1b0"}.fa-spoon:before{content:"\f1b1"}.fa-cube:before{content:"\f1b2"}.fa-cubes:before{content:"\f1b3"}.fa-behance:before{content:"\f1b4"}.fa-behance-square:before{content:"\f1b5"}.fa-steam:before{content:"\f1b6"}.fa-steam-square:before{content:"\f1b7"}.fa-recycle:before{content:"\f1b8"}.fa-automobile:before,.fa-car:before{content:"\f1b9"}.fa-cab:before,.fa-taxi:before{content:"\f1ba"}.fa-tree:before{content:"\f1bb"}.fa-spotify:before{content:"\f1bc"}.fa-deviantart:before{content:"\f1bd"}.fa-soundcloud:before{content:"\f1be"}.fa-database:before{content:"\f1c0"}.fa-file-pdf-o:before{content:"\f1c1"}.fa-file-word-o:before{content:"\f1c2"}.fa-file-excel-o:before{content:"\f1c3"}.fa-file-powerpoint-o:before{content:"\f1c4"}.fa-file-photo-o:before,.fa-file-picture-o:before,.fa-file-image-o:before{content:"\f1c5"}.fa-file-zip-o:before,.fa-file-archive-o:before{content:"\f1c6"}.fa-file-sound-o:before,.fa-file-audio-o:before{content:"\f1c7"}.fa-file-movie-o:before,.fa-file-video-o:before{content:"\f1c8"}.fa-file-code-o:before{content:"\f1c9"}.fa-vine:before{content:"\f1ca"}.fa-codepen:before{content:"\f1cb"}.fa-jsfiddle:before{content:"\f1cc"}.fa-life-bouy:before,.fa-life-buoy:before,.fa-life-saver:before,.fa-support:before,.fa-life-ring:before{content:"\f1cd"}.fa-circle-o-notch:before{content:"\f1ce"}.fa-ra:before,.fa-resistance:before,.fa-rebel:before{content:"\f1d0"}.fa-ge:before,.fa-empire:before{content:"\f1d1"}.fa-git-square:before{content:"\f1d2"}.fa-git:before{content:"\f1d3"}.fa-y-combinator-square:before,.fa-yc-square:before,.fa-hacker-news:before{content:"\f1d4"}.fa-tencent-weibo:before{content:"\f1d5"}.fa-qq:before{content:"\f1d6"}.fa-wechat:before,.fa-weixin:before{content:"\f1d7"}.fa-send:before,.fa-paper-plane:before{content:"\f1d8"}.fa-send-o:before,.fa-paper-plane-o:before{content:"\f1d9"}.fa-history:before{content:"\f1da"}.fa-circle-thin:before{content:"\f1db"}.fa-header:before{content:"\f1dc"}.fa-paragraph:before{content:"\f1dd"}.fa-sliders:before{content:"\f1de"}.fa-share-alt:before{content:"\f1e0"}.fa-share-alt-square:before{content:"\f1e1"}.fa-bomb:before{content:"\f1e2"}.fa-soccer-ball-o:before,.fa-futbol-o:before{content:"\f1e3"}.fa-tty:before{content:"\f1e4"}.fa-binoculars:before{content:"\f1e5"}.fa-plug:before{content:"\f1e6"}.fa-slideshare:before{content:"\f1e7"}.fa-twitch:before{content:"\f1e8"}.fa-yelp:before{content:"\f1e9"}.fa-newspaper-o:before{content:"\f1ea"}.fa-wifi:before{content:"\f1eb"}.fa-calculator:before{content:"\f1ec"}.fa-paypal:before{content:"\f1ed"}.fa-google-wallet:before{content:"\f1ee"}.fa-cc-visa:before{content:"\f1f0"}.fa-cc-mastercard:before{content:"\f1f1"}.fa-cc-discover:before{content:"\f1f2"}.fa-cc-amex:before{content:"\f1f3"}.fa-cc-paypal:before{content:"\f1f4"}.fa-cc-stripe:before{content:"\f1f5"}.fa-bell-slash:before{content:"\f1f6"}.fa-bell-slash-o:before{content:"\f1f7"}.fa-trash:before{content:"\f1f8"}.fa-copyright:before{content:"\f1f9"}.fa-at:before{content:"\f1fa"}.fa-eyedropper:before{content:"\f1fb"}.fa-paint-brush:before{content:"\f1fc"}.fa-birthday-cake:before{content:"\f1fd"}.fa-area-chart:before{content:"\f1fe"}.fa-pie-chart:before{content:"\f200"}.fa-line-chart:before{content:"\f201"}.fa-lastfm:before{content:"\f202"}.fa-lastfm-square:before{content:"\f203"}.fa-toggle-off:before{content:"\f204"}.fa-toggle-on:before{content:"\f205"}.fa-bicycle:before{content:"\f206"}.fa-bus:before{content:"\f207"}.fa-ioxhost:before{content:"\f208"}.fa-angellist:before{content:"\f209"}.fa-cc:before{content:"\f20a"}.fa-shekel:before,.fa-sheqel:before,.fa-ils:before{content:"\f20b"}.fa-meanpath:before{content:"\f20c"}.fa-buysellads:before{content:"\f20d"}.fa-connectdevelop:before{content:"\f20e"}.fa-dashcube:before{content:"\f210"}.fa-forumbee:before{content:"\f211"}.fa-leanpub:before{content:"\f212"}.fa-sellsy:before{content:"\f213"}.fa-shirtsinbulk:before{content:"\f214"}.fa-simplybuilt:before{content:"\f215"}.fa-skyatlas:before{content:"\f216"}.fa-cart-plus:before{content:"\f217"}.fa-cart-arrow-down:before{content:"\f218"}.fa-diamond:before{content:"\f219"}.fa-ship:before{content:"\f21a"}.fa-user-secret:before{content:"\f21b"}.fa-motorcycle:before{content:"\f21c"}.fa-street-view:before{content:"\f21d"}.fa-heartbeat:before{content:"\f21e"}.fa-venus:before{content:"\f221"}.fa-mars:before{content:"\f222"}.fa-mercury:before{content:"\f223"}.fa-intersex:before,.fa-transgender:before{content:"\f224"}.fa-transgender-alt:before{content:"\f225"}.fa-venus-double:before{content:"\f226"}.fa-mars-double:before{content:"\f227"}.fa-venus-mars:before{content:"\f228"}.fa-mars-stroke:before{content:"\f229"}.fa-mars-stroke-v:before{content:"\f22a"}.fa-mars-stroke-h:before{content:"\f22b"}.fa-neuter:before{content:"\f22c"}.fa-genderless:before{content:"\f22d"}.fa-facebook-official:before{content:"\f230"}.fa-pinterest-p:before{content:"\f231"}.fa-whatsapp:before{content:"\f232"}.fa-server:before{content:"\f233"}.fa-user-plus:before{content:"\f234"}.fa-user-times:before{content:"\f235"}.fa-hotel:before,.fa-bed:before{content:"\f236"}.fa-viacoin:before{content:"\f237"}.fa-train:before{content:"\f238"}.fa-subway:before{content:"\f239"}.fa-medium:before{content:"\f23a"}.fa-yc:before,.fa-y-combinator:before{content:"\f23b"}.fa-optin-monster:before{content:"\f23c"}.fa-opencart:before{content:"\f23d"}.fa-expeditedssl:before{content:"\f23e"}.fa-battery-4:before,.fa-battery:before,.fa-battery-full:before{content:"\f240"}.fa-battery-3:before,.fa-battery-three-quarters:before{content:"\f241"}.fa-battery-2:before,.fa-battery-half:before{content:"\f242"}.fa-battery-1:before,.fa-battery-quarter:before{content:"\f243"}.fa-battery-0:before,.fa-battery-empty:before{content:"\f244"}.fa-mouse-pointer:before{content:"\f245"}.fa-i-cursor:before{content:"\f246"}.fa-object-group:before{content:"\f247"}.fa-object-ungroup:before{content:"\f248"}.fa-sticky-note:before{content:"\f249"}.fa-sticky-note-o:before{content:"\f24a"}.fa-cc-jcb:before{content:"\f24b"}.fa-cc-diners-club:before{content:"\f24c"}.fa-clone:before{content:"\f24d"}.fa-balance-scale:before{content:"\f24e"}.fa-hourglass-o:before{content:"\f250"}.fa-hourglass-1:before,.fa-hourglass-start:before{content:"\f251"}.fa-hourglass-2:before,.fa-hourglass-half:before{content:"\f252"}.fa-hourglass-3:before,.fa-hourglass-end:before{content:"\f253"}.fa-hourglass:before{content:"\f254"}.fa-hand-grab-o:before,.fa-hand-rock-o:before{content:"\f255"}.fa-hand-stop-o:before,.fa-hand-paper-o:before{content:"\f256"}.fa-hand-scissors-o:before{content:"\f257"}.fa-hand-lizard-o:before{content:"\f258"}.fa-hand-spock-o:before{content:"\f259"}.fa-hand-pointer-o:before{content:"\f25a"}.fa-hand-peace-o:before{content:"\f25b"}.fa-trademark:before{content:"\f25c"}.fa-registered:before{content:"\f25d"}.fa-creative-commons:before{content:"\f25e"}.fa-gg:before{content:"\f260"}.fa-gg-circle:before{content:"\f261"}.fa-tripadvisor:before{content:"\f262"}.fa-odnoklassniki:before{content:"\f263"}.fa-odnoklassniki-square:before{content:"\f264"}.fa-get-pocket:before{content:"\f265"}.fa-wikipedia-w:before{content:"\f266"}.fa-safari:before{content:"\f267"}.fa-chrome:before{content:"\f268"}.fa-firefox:before{content:"\f269"}.fa-opera:before{content:"\f26a"}.fa-internet-explorer:before{content:"\f26b"}.fa-tv:before,.fa-television:before{content:"\f26c"}.fa-contao:before{content:"\f26d"}.fa-500px:before{content:"\f26e"}.fa-amazon:before{content:"\f270"}.fa-calendar-plus-o:before{content:"\f271"}.fa-calendar-minus-o:before{content:"\f272"}.fa-calendar-times-o:before{content:"\f273"}.fa-calendar-check-o:before{content:"\f274"}.fa-industry:before{content:"\f275"}.fa-map-pin:before{content:"\f276"}.fa-map-signs:before{content:"\f277"}.fa-map-o:before{content:"\f278"}.fa-map:before{content:"\f279"}.fa-commenting:before{content:"\f27a"}.fa-commenting-o:before{content:"\f27b"}.fa-houzz:before{content:"\f27c"}.fa-vimeo:before{content:"\f27d"}.fa-black-tie:before{content:"\f27e"}.fa-fonticons:before{content:"\f280"}.fa-reddit-alien:before{content:"\f281"}.fa-edge:before{content:"\f282"}.fa-credit-card-alt:before{content:"\f283"}.fa-codiepie:before{content:"\f284"}.fa-modx:before{content:"\f285"}.fa-fort-awesome:before{content:"\f286"}.fa-usb:before{content:"\f287"}.fa-product-hunt:before{content:"\f288"}.fa-mixcloud:before{content:"\f289"}.fa-scribd:before{content:"\f28a"}.fa-pause-circle:before{content:"\f28b"}.fa-pause-circle-o:before{content:"\f28c"}.fa-stop-circle:before{content:"\f28d"}.fa-stop-circle-o:before{content:"\f28e"}.fa-shopping-bag:before{content:"\f290"}.fa-shopping-basket:before{content:"\f291"}.fa-hashtag:before{content:"\f292"}.fa-bluetooth:before{content:"\f293"}.fa-bluetooth-b:before{content:"\f294"}.fa-percent:before{content:"\f295"}.fa-gitlab:before{content:"\f296"}.fa-wpbeginner:before{content:"\f297"}.fa-wpforms:before{content:"\f298"}.fa-envira:before{content:"\f299"}.fa-universal-access:before{content:"\f29a"}.fa-wheelchair-alt:before{content:"\f29b"}.fa-question-circle-o:before{content:"\f29c"}.fa-blind:before{content:"\f29d"}.fa-audio-description:before{content:"\f29e"}.fa-volume-control-phone:before{content:"\f2a0"}.fa-braille:before{content:"\f2a1"}.fa-assistive-listening-systems:before{content:"\f2a2"}.fa-asl-interpreting:before,.fa-american-sign-language-interpreting:before{content:"\f2a3"}.fa-deafness:before,.fa-hard-of-hearing:before,.fa-deaf:before{content:"\f2a4"}.fa-glide:before{content:"\f2a5"}.fa-glide-g:before{content:"\f2a6"}.fa-signing:before,.fa-sign-language:before{content:"\f2a7"}.fa-low-vision:before{content:"\f2a8"}.fa-viadeo:before{content:"\f2a9"}.fa-viadeo-square:before{content:"\f2aa"}.fa-snapchat:before{content:"\f2ab"}.fa-snapchat-ghost:before{content:"\f2ac"}.fa-snapchat-square:before{content:"\f2ad"}.fa-pied-piper:before{content:"\f2ae"}.fa-first-order:before{content:"\f2b0"}.fa-yoast:before{content:"\f2b1"}.fa-themeisle:before{content:"\f2b2"}.fa-google-plus-circle:before,.fa-google-plus-official:before{content:"\f2b3"}.fa-fa:before,.fa-font-awesome:before{content:"\f2b4"}.fa-handshake-o:before{content:"\f2b5"}.fa-envelope-open:before{content:"\f2b6"}.fa-envelope-open-o:before{content:"\f2b7"}.fa-linode:before{content:"\f2b8"}.fa-address-book:before{content:"\f2b9"}.fa-address-book-o:before{content:"\f2ba"}.fa-vcard:before,.fa-address-card:before{content:"\f2bb"}.fa-vcard-o:before,.fa-address-card-o:before{content:"\f2bc"}.fa-user-circle:before{content:"\f2bd"}.fa-user-circle-o:before{content:"\f2be"}.fa-user-o:before{content:"\f2c0"}.fa-id-badge:before{content:"\f2c1"}.fa-drivers-license:before,.fa-id-card:before{content:"\f2c2"}.fa-drivers-license-o:before,.fa-id-card-o:before{content:"\f2c3"}.fa-quora:before{content:"\f2c4"}.fa-free-code-camp:before{content:"\f2c5"}.fa-telegram:before{content:"\f2c6"}.fa-thermometer-4:before,.fa-thermometer:before,.fa-thermometer-full:before{content:"\f2c7"}.fa-thermometer-3:before,.fa-thermometer-three-quarters:before{content:"\f2c8"}.fa-thermometer-2:before,.fa-thermometer-half:before{content:"\f2c9"}.fa-thermometer-1:before,.fa-thermometer-quarter:before{content:"\f2ca"}.fa-thermometer-0:before,.fa-thermometer-empty:before{content:"\f2cb"}.fa-shower:before{content:"\f2cc"}.fa-bathtub:before,.fa-s15:before,.fa-bath:before{content:"\f2cd"}.fa-podcast:before{content:"\f2ce"}.fa-window-maximize:before{content:"\f2d0"}.fa-window-minimize:before{content:"\f2d1"}.fa-window-restore:before{content:"\f2d2"}.fa-times-rectangle:before,.fa-window-close:before{content:"\f2d3"}.fa-times-rectangle-o:before,.fa-window-close-o:before{content:"\f2d4"}.fa-bandcamp:before{content:"\f2d5"}.fa-grav:before{content:"\f2d6"}.fa-etsy:before{content:"\f2d7"}.fa-imdb:before{content:"\f2d8"}.fa-ravelry:before{content:"\f2d9"}.fa-eercast:before{content:"\f2da"}.fa-microchip:before{content:"\f2db"}.fa-snowflake-o:before{content:"\f2dc"}.fa-superpowers:before{content:"\f2dd"}.fa-wpexplorer:before{content:"\f2de"}.fa-meetup:before{content:"\f2e0"}.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0, 0, 0, 0);border:0}.sr-only-focusable:active,.sr-only-focusable:focus{position:static;width:auto;height:auto;margin:0;overflow:visible;clip:auto} diff --git a/lib/font-awesome/fonts/fontawesome-webfont.eot b/lib/font-awesome/fonts/fontawesome-webfont.eot new file mode 100644 index 0000000000..e9f60ca953 Binary files /dev/null and b/lib/font-awesome/fonts/fontawesome-webfont.eot differ diff --git a/lib/font-awesome/fonts/fontawesome-webfont.woff b/lib/font-awesome/fonts/fontawesome-webfont.woff new file mode 100644 index 0000000000..400014a4b0 Binary files /dev/null and b/lib/font-awesome/fonts/fontawesome-webfont.woff differ diff --git a/lib/font-awesome/fonts/fontawesome-webfont.woff2 b/lib/font-awesome/fonts/fontawesome-webfont.woff2 new file mode 100644 index 0000000000..4d13fc6040 Binary files /dev/null and b/lib/font-awesome/fonts/fontawesome-webfont.woff2 differ diff --git a/lib/velocity/velocity.min.js b/lib/velocity/velocity.min.js new file mode 100644 index 0000000000..58244c80e3 --- /dev/null +++ b/lib/velocity/velocity.min.js @@ -0,0 +1,4 @@ +/*! VelocityJS.org (1.2.2). (C) 2014 Julian Shapiro. MIT @license: en.wikipedia.org/wiki/MIT_License */ +/*! VelocityJS.org jQuery Shim (1.0.1). (C) 2014 The jQuery Foundation. MIT @license: en.wikipedia.org/wiki/MIT_License. */ +!function(e){function t(e){var t=e.length,r=$.type(e);return"function"===r||$.isWindow(e)?!1:1===e.nodeType&&t?!0:"array"===r||0===t||"number"==typeof t&&t>0&&t-1 in e}if(!e.jQuery){var $=function(e,t){return new $.fn.init(e,t)};$.isWindow=function(e){return null!=e&&e==e.window},$.type=function(e){return null==e?e+"":"object"==typeof e||"function"==typeof e?a[o.call(e)]||"object":typeof e},$.isArray=Array.isArray||function(e){return"array"===$.type(e)},$.isPlainObject=function(e){var t;if(!e||"object"!==$.type(e)||e.nodeType||$.isWindow(e))return!1;try{if(e.constructor&&!n.call(e,"constructor")&&!n.call(e.constructor.prototype,"isPrototypeOf"))return!1}catch(r){return!1}for(t in e);return void 0===t||n.call(e,t)},$.each=function(e,r,a){var n,o=0,i=e.length,s=t(e);if(a){if(s)for(;i>o&&(n=r.apply(e[o],a),n!==!1);o++);else for(o in e)if(n=r.apply(e[o],a),n===!1)break}else if(s)for(;i>o&&(n=r.call(e[o],o,e[o]),n!==!1);o++);else for(o in e)if(n=r.call(e[o],o,e[o]),n===!1)break;return e},$.data=function(e,t,a){if(void 0===a){var n=e[$.expando],o=n&&r[n];if(void 0===t)return o;if(o&&t in o)return o[t]}else if(void 0!==t){var n=e[$.expando]||(e[$.expando]=++$.uuid);return r[n]=r[n]||{},r[n][t]=a,a}},$.removeData=function(e,t){var a=e[$.expando],n=a&&r[a];n&&$.each(t,function(e,t){delete n[t]})},$.extend=function(){var e,t,r,a,n,o,i=arguments[0]||{},s=1,l=arguments.length,u=!1;for("boolean"==typeof i&&(u=i,i=arguments[s]||{},s++),"object"!=typeof i&&"function"!==$.type(i)&&(i={}),s===l&&(i=this,s--);l>s;s++)if(null!=(n=arguments[s]))for(a in n)e=i[a],r=n[a],i!==r&&(u&&r&&($.isPlainObject(r)||(t=$.isArray(r)))?(t?(t=!1,o=e&&$.isArray(e)?e:[]):o=e&&$.isPlainObject(e)?e:{},i[a]=$.extend(u,o,r)):void 0!==r&&(i[a]=r));return i},$.queue=function(e,r,a){function n(e,r){var a=r||[];return null!=e&&(t(Object(e))?!function(e,t){for(var r=+t.length,a=0,n=e.length;r>a;)e[n++]=t[a++];if(r!==r)for(;void 0!==t[a];)e[n++]=t[a++];return e.length=n,e}(a,"string"==typeof e?[e]:e):[].push.call(a,e)),a}if(e){r=(r||"fx")+"queue";var o=$.data(e,r);return a?(!o||$.isArray(a)?o=$.data(e,r,n(a)):o.push(a),o):o||[]}},$.dequeue=function(e,t){$.each(e.nodeType?[e]:e,function(e,r){t=t||"fx";var a=$.queue(r,t),n=a.shift();"inprogress"===n&&(n=a.shift()),n&&("fx"===t&&a.unshift("inprogress"),n.call(r,function(){$.dequeue(r,t)}))})},$.fn=$.prototype={init:function(e){if(e.nodeType)return this[0]=e,this;throw new Error("Not a DOM node.")},offset:function(){var t=this[0].getBoundingClientRect?this[0].getBoundingClientRect():{top:0,left:0};return{top:t.top+(e.pageYOffset||document.scrollTop||0)-(document.clientTop||0),left:t.left+(e.pageXOffset||document.scrollLeft||0)-(document.clientLeft||0)}},position:function(){function e(){for(var e=this.offsetParent||document;e&&"html"===!e.nodeType.toLowerCase&&"static"===e.style.position;)e=e.offsetParent;return e||document}var t=this[0],e=e.apply(t),r=this.offset(),a=/^(?:body|html)$/i.test(e.nodeName)?{top:0,left:0}:$(e).offset();return r.top-=parseFloat(t.style.marginTop)||0,r.left-=parseFloat(t.style.marginLeft)||0,e.style&&(a.top+=parseFloat(e.style.borderTopWidth)||0,a.left+=parseFloat(e.style.borderLeftWidth)||0),{top:r.top-a.top,left:r.left-a.left}}};var r={};$.expando="velocity"+(new Date).getTime(),$.uuid=0;for(var a={},n=a.hasOwnProperty,o=a.toString,i="Boolean Number String Function Array Date RegExp Object Error".split(" "),s=0;sn;++n){var o=u(r,e,a);if(0===o)return r;var i=l(r,e,a)-t;r-=i/o}return r}function p(){for(var t=0;b>t;++t)w[t]=l(t*x,e,a)}function f(t,r,n){var o,i,s=0;do i=r+(n-r)/2,o=l(i,e,a)-t,o>0?n=i:r=i;while(Math.abs(o)>h&&++s=y?c(t,s):0==l?s:f(t,r,r+x)}function g(){V=!0,(e!=r||a!=n)&&p()}var m=4,y=.001,h=1e-7,v=10,b=11,x=1/(b-1),S="Float32Array"in t;if(4!==arguments.length)return!1;for(var P=0;4>P;++P)if("number"!=typeof arguments[P]||isNaN(arguments[P])||!isFinite(arguments[P]))return!1;e=Math.min(e,1),a=Math.min(a,1),e=Math.max(e,0),a=Math.max(a,0);var w=S?new Float32Array(b):new Array(b),V=!1,C=function(t){return V||g(),e===r&&a===n?t:0===t?0:1===t?1:l(d(t),r,n)};C.getControlPoints=function(){return[{x:e,y:r},{x:a,y:n}]};var T="generateBezier("+[e,r,a,n]+")";return C.toString=function(){return T},C}function u(e,t){var r=e;return g.isString(e)?v.Easings[e]||(r=!1):r=g.isArray(e)&&1===e.length?s.apply(null,e):g.isArray(e)&&2===e.length?b.apply(null,e.concat([t])):g.isArray(e)&&4===e.length?l.apply(null,e):!1,r===!1&&(r=v.Easings[v.defaults.easing]?v.defaults.easing:h),r}function c(e){if(e){var t=(new Date).getTime(),r=v.State.calls.length;r>1e4&&(v.State.calls=n(v.State.calls));for(var o=0;r>o;o++)if(v.State.calls[o]){var s=v.State.calls[o],l=s[0],u=s[2],f=s[3],d=!!f,m=null;f||(f=v.State.calls[o][3]=t-16);for(var y=Math.min((t-f)/u.duration,1),h=0,b=l.length;b>h;h++){var S=l[h],w=S.element;if(i(w)){var V=!1;if(u.display!==a&&null!==u.display&&"none"!==u.display){if("flex"===u.display){var C=["-webkit-box","-moz-box","-ms-flexbox","-webkit-flex"];$.each(C,function(e,t){x.setPropertyValue(w,"display",t)})}x.setPropertyValue(w,"display",u.display)}u.visibility!==a&&"hidden"!==u.visibility&&x.setPropertyValue(w,"visibility",u.visibility);for(var T in S)if("element"!==T){var k=S[T],A,F=g.isString(k.easing)?v.Easings[k.easing]:k.easing;if(1===y)A=k.endValue;else{var E=k.endValue-k.startValue;if(A=k.startValue+E*F(y,u,E),!d&&A===k.currentValue)continue}if(k.currentValue=A,"tween"===T)m=A;else{if(x.Hooks.registered[T]){var j=x.Hooks.getRoot(T),H=i(w).rootPropertyValueCache[j];H&&(k.rootPropertyValue=H)}var N=x.setPropertyValue(w,T,k.currentValue+(0===parseFloat(A)?"":k.unitType),k.rootPropertyValue,k.scrollData);x.Hooks.registered[T]&&(i(w).rootPropertyValueCache[j]=x.Normalizations.registered[j]?x.Normalizations.registered[j]("extract",null,N[1]):N[1]),"transform"===N[0]&&(V=!0)}}u.mobileHA&&i(w).transformCache.translate3d===a&&(i(w).transformCache.translate3d="(0px, 0px, 0px)",V=!0),V&&x.flushTransformCache(w)}}u.display!==a&&"none"!==u.display&&(v.State.calls[o][2].display=!1),u.visibility!==a&&"hidden"!==u.visibility&&(v.State.calls[o][2].visibility=!1),u.progress&&u.progress.call(s[1],s[1],y,Math.max(0,f+u.duration-t),f,m),1===y&&p(o)}}v.State.isTicking&&P(c)}function p(e,t){if(!v.State.calls[e])return!1;for(var r=v.State.calls[e][0],n=v.State.calls[e][1],o=v.State.calls[e][2],s=v.State.calls[e][4],l=!1,u=0,c=r.length;c>u;u++){var p=r[u].element;if(t||o.loop||("none"===o.display&&x.setPropertyValue(p,"display",o.display),"hidden"===o.visibility&&x.setPropertyValue(p,"visibility",o.visibility)),o.loop!==!0&&($.queue(p)[1]===a||!/\.velocityQueueEntryFlag/i.test($.queue(p)[1]))&&i(p)){i(p).isAnimating=!1,i(p).rootPropertyValueCache={};var f=!1;$.each(x.Lists.transforms3D,function(e,t){var r=/^scale/.test(t)?1:0,n=i(p).transformCache[t];i(p).transformCache[t]!==a&&new RegExp("^\\("+r+"[^.]").test(n)&&(f=!0,delete i(p).transformCache[t])}),o.mobileHA&&(f=!0,delete i(p).transformCache.translate3d),f&&x.flushTransformCache(p),x.Values.removeClass(p,"velocity-animating")}if(!t&&o.complete&&!o.loop&&u===c-1)try{o.complete.call(n,n)}catch(d){setTimeout(function(){throw d},1)}s&&o.loop!==!0&&s(n),i(p)&&o.loop===!0&&!t&&($.each(i(p).tweensContainer,function(e,t){/^rotate/.test(e)&&360===parseFloat(t.endValue)&&(t.endValue=0,t.startValue=360),/^backgroundPosition/.test(e)&&100===parseFloat(t.endValue)&&"%"===t.unitType&&(t.endValue=0,t.startValue=100)}),v(p,"reverse",{loop:!0,delay:o.delay})),o.queue!==!1&&$.dequeue(p,o.queue)}v.State.calls[e]=!1;for(var g=0,m=v.State.calls.length;m>g;g++)if(v.State.calls[g]!==!1){l=!0;break}l===!1&&(v.State.isTicking=!1,delete v.State.calls,v.State.calls=[])}var f=function(){if(r.documentMode)return r.documentMode;for(var e=7;e>4;e--){var t=r.createElement("div");if(t.innerHTML="",t.getElementsByTagName("span").length)return t=null,e}return a}(),d=function(){var e=0;return t.webkitRequestAnimationFrame||t.mozRequestAnimationFrame||function(t){var r=(new Date).getTime(),a;return a=Math.max(0,16-(r-e)),e=r+a,setTimeout(function(){t(r+a)},a)}}(),g={isString:function(e){return"string"==typeof e},isArray:Array.isArray||function(e){return"[object Array]"===Object.prototype.toString.call(e)},isFunction:function(e){return"[object Function]"===Object.prototype.toString.call(e)},isNode:function(e){return e&&e.nodeType},isNodeList:function(e){return"object"==typeof e&&/^\[object (HTMLCollection|NodeList|Object)\]$/.test(Object.prototype.toString.call(e))&&e.length!==a&&(0===e.length||"object"==typeof e[0]&&e[0].nodeType>0)},isWrapped:function(e){return e&&(e.jquery||t.Zepto&&t.Zepto.zepto.isZ(e))},isSVG:function(e){return t.SVGElement&&e instanceof t.SVGElement},isEmptyObject:function(e){for(var t in e)return!1;return!0}},$,m=!1;if(e.fn&&e.fn.jquery?($=e,m=!0):$=t.Velocity.Utilities,8>=f&&!m)throw new Error("Velocity: IE8 and below require jQuery to be loaded before Velocity.");if(7>=f)return void(jQuery.fn.velocity=jQuery.fn.animate);var y=400,h="swing",v={State:{isMobile:/Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent),isAndroid:/Android/i.test(navigator.userAgent),isGingerbread:/Android 2\.3\.[3-7]/i.test(navigator.userAgent),isChrome:t.chrome,isFirefox:/Firefox/i.test(navigator.userAgent),prefixElement:r.createElement("div"),prefixMatches:{},scrollAnchor:null,scrollPropertyLeft:null,scrollPropertyTop:null,isTicking:!1,calls:[]},CSS:{},Utilities:$,Redirects:{},Easings:{},Promise:t.Promise,defaults:{queue:"",duration:y,easing:h,begin:a,complete:a,progress:a,display:a,visibility:a,loop:!1,delay:!1,mobileHA:!0,_cacheValues:!0},init:function(e){$.data(e,"velocity",{isSVG:g.isSVG(e),isAnimating:!1,computedStyle:null,tweensContainer:null,rootPropertyValueCache:{},transformCache:{}})},hook:null,mock:!1,version:{major:1,minor:2,patch:2},debug:!1};t.pageYOffset!==a?(v.State.scrollAnchor=t,v.State.scrollPropertyLeft="pageXOffset",v.State.scrollPropertyTop="pageYOffset"):(v.State.scrollAnchor=r.documentElement||r.body.parentNode||r.body,v.State.scrollPropertyLeft="scrollLeft",v.State.scrollPropertyTop="scrollTop");var b=function(){function e(e){return-e.tension*e.x-e.friction*e.v}function t(t,r,a){var n={x:t.x+a.dx*r,v:t.v+a.dv*r,tension:t.tension,friction:t.friction};return{dx:n.v,dv:e(n)}}function r(r,a){var n={dx:r.v,dv:e(r)},o=t(r,.5*a,n),i=t(r,.5*a,o),s=t(r,a,i),l=1/6*(n.dx+2*(o.dx+i.dx)+s.dx),u=1/6*(n.dv+2*(o.dv+i.dv)+s.dv);return r.x=r.x+l*a,r.v=r.v+u*a,r}return function a(e,t,n){var o={x:-1,v:0,tension:null,friction:null},i=[0],s=0,l=1e-4,u=.016,c,p,f;for(e=parseFloat(e)||500,t=parseFloat(t)||20,n=n||null,o.tension=e,o.friction=t,c=null!==n,c?(s=a(e,t),p=s/n*u):p=u;;)if(f=r(f||o,p),i.push(1+f.x),s+=16,!(Math.abs(f.x)>l&&Math.abs(f.v)>l))break;return c?function(e){return i[e*(i.length-1)|0]}:s}}();v.Easings={linear:function(e){return e},swing:function(e){return.5-Math.cos(e*Math.PI)/2},spring:function(e){return 1-Math.cos(4.5*e*Math.PI)*Math.exp(6*-e)}},$.each([["ease",[.25,.1,.25,1]],["ease-in",[.42,0,1,1]],["ease-out",[0,0,.58,1]],["ease-in-out",[.42,0,.58,1]],["easeInSine",[.47,0,.745,.715]],["easeOutSine",[.39,.575,.565,1]],["easeInOutSine",[.445,.05,.55,.95]],["easeInQuad",[.55,.085,.68,.53]],["easeOutQuad",[.25,.46,.45,.94]],["easeInOutQuad",[.455,.03,.515,.955]],["easeInCubic",[.55,.055,.675,.19]],["easeOutCubic",[.215,.61,.355,1]],["easeInOutCubic",[.645,.045,.355,1]],["easeInQuart",[.895,.03,.685,.22]],["easeOutQuart",[.165,.84,.44,1]],["easeInOutQuart",[.77,0,.175,1]],["easeInQuint",[.755,.05,.855,.06]],["easeOutQuint",[.23,1,.32,1]],["easeInOutQuint",[.86,0,.07,1]],["easeInExpo",[.95,.05,.795,.035]],["easeOutExpo",[.19,1,.22,1]],["easeInOutExpo",[1,0,0,1]],["easeInCirc",[.6,.04,.98,.335]],["easeOutCirc",[.075,.82,.165,1]],["easeInOutCirc",[.785,.135,.15,.86]]],function(e,t){v.Easings[t[0]]=l.apply(null,t[1])});var x=v.CSS={RegEx:{isHex:/^#([A-f\d]{3}){1,2}$/i,valueUnwrap:/^[A-z]+\((.*)\)$/i,wrappedValueAlreadyExtracted:/[0-9.]+ [0-9.]+ [0-9.]+( [0-9.]+)?/,valueSplit:/([A-z]+\(.+\))|(([A-z0-9#-.]+?)(?=\s|$))/gi},Lists:{colors:["fill","stroke","stopColor","color","backgroundColor","borderColor","borderTopColor","borderRightColor","borderBottomColor","borderLeftColor","outlineColor"],transformsBase:["translateX","translateY","scale","scaleX","scaleY","skewX","skewY","rotateZ"],transforms3D:["transformPerspective","translateZ","scaleZ","rotateX","rotateY"]},Hooks:{templates:{textShadow:["Color X Y Blur","black 0px 0px 0px"],boxShadow:["Color X Y Blur Spread","black 0px 0px 0px 0px"],clip:["Top Right Bottom Left","0px 0px 0px 0px"],backgroundPosition:["X Y","0% 0%"],transformOrigin:["X Y Z","50% 50% 0px"],perspectiveOrigin:["X Y","50% 50%"]},registered:{},register:function(){for(var e=0;e=f)switch(e){case"name":return"filter";case"extract":var a=r.toString().match(/alpha\(opacity=(.*)\)/i);return r=a?a[1]/100:1;case"inject":return t.style.zoom=1,parseFloat(r)>=1?"":"alpha(opacity="+parseInt(100*parseFloat(r),10)+")"}else switch(e){case"name":return"opacity";case"extract":return r;case"inject":return r}}},register:function(){9>=f||v.State.isGingerbread||(x.Lists.transformsBase=x.Lists.transformsBase.concat(x.Lists.transforms3D));for(var e=0;en&&(n=1),o=!/(\d)$/i.test(n);break;case"skew":o=!/(deg|\d)$/i.test(n);break;case"rotate":o=!/(deg|\d)$/i.test(n)}return o||(i(r).transformCache[t]="("+n+")"),i(r).transformCache[t]}}}();for(var e=0;e=f||3!==o.split(" ").length||(o+=" 1"),o;case"inject":return 8>=f?4===n.split(" ").length&&(n=n.split(/\s+/).slice(0,3).join(" ")):3===n.split(" ").length&&(n+=" 1"),(8>=f?"rgb":"rgba")+"("+n.replace(/\s+/g,",").replace(/\.(\d)+(?=,)/g,"")+")"}}}()}},Names:{camelCase:function(e){return e.replace(/-(\w)/g,function(e,t){return t.toUpperCase()})},SVGAttribute:function(e){var t="width|height|x|y|cx|cy|r|rx|ry|x1|x2|y1|y2";return(f||v.State.isAndroid&&!v.State.isChrome)&&(t+="|transform"),new RegExp("^("+t+")$","i").test(e)},prefixCheck:function(e){if(v.State.prefixMatches[e])return[v.State.prefixMatches[e],!0];for(var t=["","Webkit","Moz","ms","O"],r=0,a=t.length;a>r;r++){var n;if(n=0===r?e:t[r]+e.replace(/^\w/,function(e){return e.toUpperCase()}),g.isString(v.State.prefixElement.style[n]))return v.State.prefixMatches[e]=n,[n,!0]}return[e,!1]}},Values:{hexToRgb:function(e){var t=/^#?([a-f\d])([a-f\d])([a-f\d])$/i,r=/^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i,a;return e=e.replace(t,function(e,t,r,a){return t+t+r+r+a+a}),a=r.exec(e),a?[parseInt(a[1],16),parseInt(a[2],16),parseInt(a[3],16)]:[0,0,0]},isCSSNullValue:function(e){return 0==e||/^(none|auto|transparent|(rgba\(0, ?0, ?0, ?0\)))$/i.test(e)},getUnitType:function(e){return/^(rotate|skew)/i.test(e)?"deg":/(^(scale|scaleX|scaleY|scaleZ|alpha|flexGrow|flexHeight|zIndex|fontWeight)$)|((opacity|red|green|blue|alpha)$)/i.test(e)?"":"px"},getDisplayType:function(e){var t=e&&e.tagName.toString().toLowerCase();return/^(b|big|i|small|tt|abbr|acronym|cite|code|dfn|em|kbd|strong|samp|var|a|bdo|br|img|map|object|q|script|span|sub|sup|button|input|label|select|textarea)$/i.test(t)?"inline":/^(li)$/i.test(t)?"list-item":/^(tr)$/i.test(t)?"table-row":/^(table)$/i.test(t)?"table":/^(tbody)$/i.test(t)?"table-row-group":"block"},addClass:function(e,t){e.classList?e.classList.add(t):e.className+=(e.className.length?" ":"")+t},removeClass:function(e,t){e.classList?e.classList.remove(t):e.className=e.className.toString().replace(new RegExp("(^|\\s)"+t.split(" ").join("|")+"(\\s|$)","gi")," ")}},getPropertyValue:function(e,r,n,o){function s(e,r){function n(){u&&x.setPropertyValue(e,"display","none")}var l=0;if(8>=f)l=$.css(e,r);else{var u=!1;if(/^(width|height)$/.test(r)&&0===x.getPropertyValue(e,"display")&&(u=!0,x.setPropertyValue(e,"display",x.Values.getDisplayType(e))),!o){if("height"===r&&"border-box"!==x.getPropertyValue(e,"boxSizing").toString().toLowerCase()){var c=e.offsetHeight-(parseFloat(x.getPropertyValue(e,"borderTopWidth"))||0)-(parseFloat(x.getPropertyValue(e,"borderBottomWidth"))||0)-(parseFloat(x.getPropertyValue(e,"paddingTop"))||0)-(parseFloat(x.getPropertyValue(e,"paddingBottom"))||0);return n(),c}if("width"===r&&"border-box"!==x.getPropertyValue(e,"boxSizing").toString().toLowerCase()){var p=e.offsetWidth-(parseFloat(x.getPropertyValue(e,"borderLeftWidth"))||0)-(parseFloat(x.getPropertyValue(e,"borderRightWidth"))||0)-(parseFloat(x.getPropertyValue(e,"paddingLeft"))||0)-(parseFloat(x.getPropertyValue(e,"paddingRight"))||0);return n(),p}}var d;d=i(e)===a?t.getComputedStyle(e,null):i(e).computedStyle?i(e).computedStyle:i(e).computedStyle=t.getComputedStyle(e,null),"borderColor"===r&&(r="borderTopColor"),l=9===f&&"filter"===r?d.getPropertyValue(r):d[r],(""===l||null===l)&&(l=e.style[r]),n()}if("auto"===l&&/^(top|right|bottom|left)$/i.test(r)){var g=s(e,"position");("fixed"===g||"absolute"===g&&/top|left/i.test(r))&&(l=$(e).position()[r]+"px")}return l}var l;if(x.Hooks.registered[r]){var u=r,c=x.Hooks.getRoot(u);n===a&&(n=x.getPropertyValue(e,x.Names.prefixCheck(c)[0])),x.Normalizations.registered[c]&&(n=x.Normalizations.registered[c]("extract",e,n)),l=x.Hooks.extractValue(u,n)}else if(x.Normalizations.registered[r]){var p,d;p=x.Normalizations.registered[r]("name",e),"transform"!==p&&(d=s(e,x.Names.prefixCheck(p)[0]),x.Values.isCSSNullValue(d)&&x.Hooks.templates[r]&&(d=x.Hooks.templates[r][1])),l=x.Normalizations.registered[r]("extract",e,d)}if(!/^[\d-]/.test(l))if(i(e)&&i(e).isSVG&&x.Names.SVGAttribute(r))if(/^(height|width)$/i.test(r))try{l=e.getBBox()[r]}catch(g){l=0}else l=e.getAttribute(r);else l=s(e,x.Names.prefixCheck(r)[0]);return x.Values.isCSSNullValue(l)&&(l=0),v.debug>=2&&console.log("Get "+r+": "+l),l},setPropertyValue:function(e,r,a,n,o){var s=r;if("scroll"===r)o.container?o.container["scroll"+o.direction]=a:"Left"===o.direction?t.scrollTo(a,o.alternateValue):t.scrollTo(o.alternateValue,a);else if(x.Normalizations.registered[r]&&"transform"===x.Normalizations.registered[r]("name",e))x.Normalizations.registered[r]("inject",e,a),s="transform",a=i(e).transformCache[r];else{if(x.Hooks.registered[r]){var l=r,u=x.Hooks.getRoot(r);n=n||x.getPropertyValue(e,u),a=x.Hooks.injectValue(l,a,n),r=u}if(x.Normalizations.registered[r]&&(a=x.Normalizations.registered[r]("inject",e,a),r=x.Normalizations.registered[r]("name",e)),s=x.Names.prefixCheck(r)[0],8>=f)try{e.style[s]=a}catch(c){v.debug&&console.log("Browser does not support ["+a+"] for ["+s+"]")}else i(e)&&i(e).isSVG&&x.Names.SVGAttribute(r)?e.setAttribute(r,a):e.style[s]=a;v.debug>=2&&console.log("Set "+r+" ("+s+"): "+a)}return[s,a]},flushTransformCache:function(e){function t(t){return parseFloat(x.getPropertyValue(e,t))}var r="";if((f||v.State.isAndroid&&!v.State.isChrome)&&i(e).isSVG){var a={translate:[t("translateX"),t("translateY")],skewX:[t("skewX")],skewY:[t("skewY")],scale:1!==t("scale")?[t("scale"),t("scale")]:[t("scaleX"),t("scaleY")],rotate:[t("rotateZ"),0,0]};$.each(i(e).transformCache,function(e){/^translate/i.test(e)?e="translate":/^scale/i.test(e)?e="scale":/^rotate/i.test(e)&&(e="rotate"),a[e]&&(r+=e+"("+a[e].join(" ")+") ",delete a[e])})}else{var n,o;$.each(i(e).transformCache,function(t){return n=i(e).transformCache[t],"transformPerspective"===t?(o=n,!0):(9===f&&"rotateZ"===t&&(t="rotate"),void(r+=t+n+" "))}),o&&(r="perspective"+o+" "+r)}x.setPropertyValue(e,"transform",r)}};x.Hooks.register(),x.Normalizations.register(),v.hook=function(e,t,r){var n=a;return e=o(e),$.each(e,function(e,o){if(i(o)===a&&v.init(o),r===a)n===a&&(n=v.CSS.getPropertyValue(o,t));else{var s=v.CSS.setPropertyValue(o,t,r);"transform"===s[0]&&v.CSS.flushTransformCache(o),n=s}}),n};var S=function(){function e(){return l?T.promise||null:f}function n(){function e(e){function p(e,t){var r=a,i=a,s=a;return g.isArray(e)?(r=e[0],!g.isArray(e[1])&&/^[\d-]/.test(e[1])||g.isFunction(e[1])||x.RegEx.isHex.test(e[1])?s=e[1]:(g.isString(e[1])&&!x.RegEx.isHex.test(e[1])||g.isArray(e[1]))&&(i=t?e[1]:u(e[1],o.duration),e[2]!==a&&(s=e[2]))):r=e,t||(i=i||o.easing),g.isFunction(r)&&(r=r.call(n,w,P)),g.isFunction(s)&&(s=s.call(n,w,P)),[r||0,i,s]}function f(e,t){var r,a;return a=(t||"0").toString().toLowerCase().replace(/[%A-z]+$/,function(e){return r=e,""}),r||(r=x.Values.getUnitType(e)),[a,r]}function d(){var e={myParent:n.parentNode||r.body,position:x.getPropertyValue(n,"position"),fontSize:x.getPropertyValue(n,"fontSize")},a=e.position===N.lastPosition&&e.myParent===N.lastParent,o=e.fontSize===N.lastFontSize;N.lastParent=e.myParent,N.lastPosition=e.position,N.lastFontSize=e.fontSize;var s=100,l={};if(o&&a)l.emToPx=N.lastEmToPx,l.percentToPxWidth=N.lastPercentToPxWidth,l.percentToPxHeight=N.lastPercentToPxHeight;else{var u=i(n).isSVG?r.createElementNS("http://www.w3.org/2000/svg","rect"):r.createElement("div");v.init(u),e.myParent.appendChild(u),$.each(["overflow","overflowX","overflowY"],function(e,t){v.CSS.setPropertyValue(u,t,"hidden")}),v.CSS.setPropertyValue(u,"position",e.position),v.CSS.setPropertyValue(u,"fontSize",e.fontSize),v.CSS.setPropertyValue(u,"boxSizing","content-box"),$.each(["minWidth","maxWidth","width","minHeight","maxHeight","height"],function(e,t){v.CSS.setPropertyValue(u,t,s+"%")}),v.CSS.setPropertyValue(u,"paddingLeft",s+"em"),l.percentToPxWidth=N.lastPercentToPxWidth=(parseFloat(x.getPropertyValue(u,"width",null,!0))||1)/s,l.percentToPxHeight=N.lastPercentToPxHeight=(parseFloat(x.getPropertyValue(u,"height",null,!0))||1)/s,l.emToPx=N.lastEmToPx=(parseFloat(x.getPropertyValue(u,"paddingLeft"))||1)/s,e.myParent.removeChild(u)}return null===N.remToPx&&(N.remToPx=parseFloat(x.getPropertyValue(r.body,"fontSize"))||16),null===N.vwToPx&&(N.vwToPx=parseFloat(t.innerWidth)/100,N.vhToPx=parseFloat(t.innerHeight)/100),l.remToPx=N.remToPx,l.vwToPx=N.vwToPx,l.vhToPx=N.vhToPx,v.debug>=1&&console.log("Unit ratios: "+JSON.stringify(l),n),l}if(o.begin&&0===w)try{o.begin.call(m,m)}catch(y){setTimeout(function(){throw y},1)}if("scroll"===k){var S=/^x$/i.test(o.axis)?"Left":"Top",V=parseFloat(o.offset)||0,C,A,F;o.container?g.isWrapped(o.container)||g.isNode(o.container)?(o.container=o.container[0]||o.container,C=o.container["scroll"+S],F=C+$(n).position()[S.toLowerCase()]+V):o.container=null:(C=v.State.scrollAnchor[v.State["scrollProperty"+S]],A=v.State.scrollAnchor[v.State["scrollProperty"+("Left"===S?"Top":"Left")]],F=$(n).offset()[S.toLowerCase()]+V),s={scroll:{rootPropertyValue:!1,startValue:C,currentValue:C,endValue:F,unitType:"",easing:o.easing,scrollData:{container:o.container,direction:S,alternateValue:A}},element:n},v.debug&&console.log("tweensContainer (scroll): ",s.scroll,n)}else if("reverse"===k){if(!i(n).tweensContainer)return void $.dequeue(n,o.queue);"none"===i(n).opts.display&&(i(n).opts.display="auto"),"hidden"===i(n).opts.visibility&&(i(n).opts.visibility="visible"),i(n).opts.loop=!1,i(n).opts.begin=null,i(n).opts.complete=null,b.easing||delete o.easing,b.duration||delete o.duration,o=$.extend({},i(n).opts,o);var E=$.extend(!0,{},i(n).tweensContainer);for(var j in E)if("element"!==j){var H=E[j].startValue;E[j].startValue=E[j].currentValue=E[j].endValue,E[j].endValue=H,g.isEmptyObject(b)||(E[j].easing=o.easing),v.debug&&console.log("reverse tweensContainer ("+j+"): "+JSON.stringify(E[j]),n)}s=E}else if("start"===k){var E;i(n).tweensContainer&&i(n).isAnimating===!0&&(E=i(n).tweensContainer),$.each(h,function(e,t){if(RegExp("^"+x.Lists.colors.join("$|^")+"$").test(e)){var r=p(t,!0),n=r[0],o=r[1],i=r[2];if(x.RegEx.isHex.test(n)){for(var s=["Red","Green","Blue"],l=x.Values.hexToRgb(n),u=i?x.Values.hexToRgb(i):a,c=0;cO;O++){var z={delay:F.delay,progress:F.progress};O===R-1&&(z.display=F.display,z.visibility=F.visibility,z.complete=F.complete),S(m,"reverse",z)}return e()}};v=$.extend(S,v),v.animate=S;var P=t.requestAnimationFrame||d;return v.State.isMobile||r.hidden===a||r.addEventListener("visibilitychange",function(){r.hidden?(P=function(e){return setTimeout(function(){e(!0)},16)},c()):P=t.requestAnimationFrame||d}),e.Velocity=v,e!==t&&(e.fn.velocity=S,e.fn.velocity.defaults=v.defaults),$.each(["Down","Up"],function(e,t){v.Redirects["slide"+t]=function(e,r,n,o,i,s){var l=$.extend({},r),u=l.begin,c=l.complete,p={height:"",marginTop:"",marginBottom:"",paddingTop:"",paddingBottom:""},f={};l.display===a&&(l.display="Down"===t?"inline"===v.CSS.Values.getDisplayType(e)?"inline-block":"block":"none"),l.begin=function(){u&&u.call(i,i);for(var r in p){f[r]=e.style[r];var a=v.CSS.getPropertyValue(e,r);p[r]="Down"===t?[a,0]:[0,a]}f.overflow=e.style.overflow,e.style.overflow="hidden"},l.complete=function(){for(var t in f)e.style[t]=f[t];c&&c.call(i,i),s&&s.resolver(i)},v(e,p,l)}}),$.each(["In","Out"],function(e,t){v.Redirects["fade"+t]=function(e,r,n,o,i,s){var l=$.extend({},r),u={opacity:"In"===t?1:0},c=l.complete;l.complete=n!==o-1?l.begin=null:function(){c&&c.call(i,i),s&&s.resolver(i)},l.display===a&&(l.display="In"===t?"auto":"none"),v(this,u,l)}}),v}(window.jQuery||window.Zepto||window,window,document)}); \ No newline at end of file diff --git a/lib/velocity/velocity.ui.min.js b/lib/velocity/velocity.ui.min.js new file mode 100644 index 0000000000..870694530b --- /dev/null +++ b/lib/velocity/velocity.ui.min.js @@ -0,0 +1,2 @@ +/* VelocityJS.org UI Pack (5.0.4). (C) 2014 Julian Shapiro. MIT @license: en.wikipedia.org/wiki/MIT_License. Portions copyright Daniel Eden, Christian Pucci. */ +!function(t){"function"==typeof require&&"object"==typeof exports?module.exports=t():"function"==typeof define&&define.amd?define(["velocity"],t):t()}(function(){return function(t,a,e,r){function n(t,a){var e=[];return t&&a?($.each([t,a],function(t,a){var r=[];$.each(a,function(t,a){for(;a.toString().length<5;)a="0"+a;r.push(a)}),e.push(r.join(""))}),parseFloat(e[0])>parseFloat(e[1])):!1}if(!t.Velocity||!t.Velocity.Utilities)return void(a.console&&console.log("Velocity UI Pack: Velocity must be loaded first. Aborting."));var i=t.Velocity,$=i.Utilities,s=i.version,o={major:1,minor:1,patch:0};if(n(o,s)){var l="Velocity UI Pack: You need to update Velocity (jquery.velocity.js) to a newer version. Visit http://github.com/julianshapiro/velocity.";throw alert(l),new Error(l)}i.RegisterEffect=i.RegisterUI=function(t,a){function e(t,a,e,r){var n=0,s;$.each(t.nodeType?[t]:t,function(t,a){r&&(e+=t*r),s=a.parentNode,$.each(["height","paddingTop","paddingBottom","marginTop","marginBottom"],function(t,e){n+=parseFloat(i.CSS.getPropertyValue(a,e))})}),i.animate(s,{height:("In"===a?"+":"-")+"="+n},{queue:!1,easing:"ease-in-out",duration:e*("In"===a?.6:1)})}return i.Redirects[t]=function(n,s,o,l,c,u){function f(){s.display!==r&&"none"!==s.display||!/Out$/.test(t)||$.each(c.nodeType?[c]:c,function(t,a){i.CSS.setPropertyValue(a,"display","none")}),s.complete&&s.complete.call(c,c),u&&u.resolver(c||n)}var p=o===l-1;a.defaultDuration="function"==typeof a.defaultDuration?a.defaultDuration.call(c,c):parseFloat(a.defaultDuration);for(var d=0;d1&&($.each(a.reverse(),function(t,e){var r=a[t+1];if(r){var n=e.o||e.options,s=r.o||r.options,o=n&&n.sequenceQueue===!1?"begin":"complete",l=s&&s[o],c={};c[o]=function(){var t=r.e||r.elements,a=t.nodeType?[t]:t;l&&l.call(a,a),i(e)},r.o?r.o=$.extend({},s,c):r.options=$.extend({},s,c)}}),a.reverse()),i(a[0])}}(window.jQuery||window.Zepto||window,window,document)}); \ No newline at end of file diff --git a/page/10/index.html b/page/10/index.html new file mode 100644 index 0000000000..7baf788b18 --- /dev/null +++ b/page/10/index.html @@ -0,0 +1,415 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 贾攀的流水账 + + + + + + + + + + + + + + + +
    +
    + +
    +
    + + +
    + + + +

    贾攀的流水账

    + +
    +

    Panmax's Blog

    +
    + + +
    + + + + + + + + +
    + +
    + +
    +
    + + +
    + + 0% +
    + + +
    +
    +
    + + +
    + +
    +
    人会长大三次。
    +
    -> 第一次是在发现自己不是世界中心的时候。
    +
    -> 第二次是在发现即使再怎么努力,终究还是有些事令人无能为力的时候。
    +
    -> 第三次是在,明知道有些事可能会无能为力,但还是会尽力争取的时候。
    +
    +
    if you know me:
    + +
    else:
    +
    return 关于我
    +
    +
    +
    -------------------------------------------
    +
    +
    +
    # my daughter's blog.
    + +
    +
    + +
    + + + + +
    + + + + + + + + +
    +
    + +
    + +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/page/11/index.html b/page/11/index.html new file mode 100644 index 0000000000..156229a68f --- /dev/null +++ b/page/11/index.html @@ -0,0 +1,415 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 贾攀的流水账 + + + + + + + + + + + + + + + +
    +
    + +
    +
    + + +
    + + + +

    贾攀的流水账

    + +
    +

    Panmax's Blog

    +
    + + +
    + + + + + + + + +
    + +
    + +
    +
    + + +
    + + 0% +
    + + +
    +
    +
    + + +
    + +
    +
    人会长大三次。
    +
    -> 第一次是在发现自己不是世界中心的时候。
    +
    -> 第二次是在发现即使再怎么努力,终究还是有些事令人无能为力的时候。
    +
    -> 第三次是在,明知道有些事可能会无能为力,但还是会尽力争取的时候。
    +
    +
    if you know me:
    + +
    else:
    +
    return 关于我
    +
    +
    +
    -------------------------------------------
    +
    +
    +
    # my daughter's blog.
    + +
    +
    + +
    + + + + +
    + + + + + + + + +
    +
    + +
    + +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/page/12/index.html b/page/12/index.html new file mode 100644 index 0000000000..d804defece --- /dev/null +++ b/page/12/index.html @@ -0,0 +1,415 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 贾攀的流水账 + + + + + + + + + + + + + + + +
    +
    + +
    +
    + + +
    + + + +

    贾攀的流水账

    + +
    +

    Panmax's Blog

    +
    + + +
    + + + + + + + + +
    + +
    + +
    +
    + + +
    + + 0% +
    + + +
    +
    +
    + + +
    + +
    +
    人会长大三次。
    +
    -> 第一次是在发现自己不是世界中心的时候。
    +
    -> 第二次是在发现即使再怎么努力,终究还是有些事令人无能为力的时候。
    +
    -> 第三次是在,明知道有些事可能会无能为力,但还是会尽力争取的时候。
    +
    +
    if you know me:
    + +
    else:
    +
    return 关于我
    +
    +
    +
    -------------------------------------------
    +
    +
    +
    # my daughter's blog.
    + +
    +
    + +
    + + + + +
    + + + + + + + + +
    +
    + +
    + +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/page/13/index.html b/page/13/index.html new file mode 100644 index 0000000000..23ed2069ec --- /dev/null +++ b/page/13/index.html @@ -0,0 +1,415 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 贾攀的流水账 + + + + + + + + + + + + + + + +
    +
    + +
    +
    + + +
    + + + +

    贾攀的流水账

    + +
    +

    Panmax's Blog

    +
    + + +
    + + + + + + + + +
    + +
    + +
    +
    + + +
    + + 0% +
    + + +
    +
    +
    + + +
    + +
    +
    人会长大三次。
    +
    -> 第一次是在发现自己不是世界中心的时候。
    +
    -> 第二次是在发现即使再怎么努力,终究还是有些事令人无能为力的时候。
    +
    -> 第三次是在,明知道有些事可能会无能为力,但还是会尽力争取的时候。
    +
    +
    if you know me:
    + +
    else:
    +
    return 关于我
    +
    +
    +
    -------------------------------------------
    +
    +
    +
    # my daughter's blog.
    + +
    +
    + +
    + + + + +
    + + + + + + + + +
    +
    + +
    + +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/page/14/index.html b/page/14/index.html new file mode 100644 index 0000000000..cdf1c6fdf5 --- /dev/null +++ b/page/14/index.html @@ -0,0 +1,415 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 贾攀的流水账 + + + + + + + + + + + + + + + +
    +
    + +
    +
    + + +
    + + + +

    贾攀的流水账

    + +
    +

    Panmax's Blog

    +
    + + +
    + + + + + + + + +
    + +
    + +
    +
    + + +
    + + 0% +
    + + +
    +
    +
    + + +
    + +
    +
    人会长大三次。
    +
    -> 第一次是在发现自己不是世界中心的时候。
    +
    -> 第二次是在发现即使再怎么努力,终究还是有些事令人无能为力的时候。
    +
    -> 第三次是在,明知道有些事可能会无能为力,但还是会尽力争取的时候。
    +
    +
    if you know me:
    + +
    else:
    +
    return 关于我
    +
    +
    +
    -------------------------------------------
    +
    +
    +
    # my daughter's blog.
    + +
    +
    + +
    + + + + +
    + + + + + + + + +
    +
    + +
    + +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/page/15/index.html b/page/15/index.html new file mode 100644 index 0000000000..2af31e64cb --- /dev/null +++ b/page/15/index.html @@ -0,0 +1,415 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 贾攀的流水账 + + + + + + + + + + + + + + + +
    +
    + +
    +
    + + +
    + + + +

    贾攀的流水账

    + +
    +

    Panmax's Blog

    +
    + + +
    + + + + + + + + +
    + +
    + +
    +
    + + +
    + + 0% +
    + + +
    +
    +
    + + +
    + +
    +
    人会长大三次。
    +
    -> 第一次是在发现自己不是世界中心的时候。
    +
    -> 第二次是在发现即使再怎么努力,终究还是有些事令人无能为力的时候。
    +
    -> 第三次是在,明知道有些事可能会无能为力,但还是会尽力争取的时候。
    +
    +
    if you know me:
    + +
    else:
    +
    return 关于我
    +
    +
    +
    -------------------------------------------
    +
    +
    +
    # my daughter's blog.
    + +
    +
    + +
    + + + + +
    + + + + + + + + +
    +
    + +
    + +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/page/16/index.html b/page/16/index.html new file mode 100644 index 0000000000..4649bd181b --- /dev/null +++ b/page/16/index.html @@ -0,0 +1,415 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 贾攀的流水账 + + + + + + + + + + + + + + + +
    +
    + +
    +
    + + +
    + + + +

    贾攀的流水账

    + +
    +

    Panmax's Blog

    +
    + + +
    + + + + + + + + +
    + +
    + +
    +
    + + +
    + + 0% +
    + + +
    +
    +
    + + +
    + +
    +
    人会长大三次。
    +
    -> 第一次是在发现自己不是世界中心的时候。
    +
    -> 第二次是在发现即使再怎么努力,终究还是有些事令人无能为力的时候。
    +
    -> 第三次是在,明知道有些事可能会无能为力,但还是会尽力争取的时候。
    +
    +
    if you know me:
    + +
    else:
    +
    return 关于我
    +
    +
    +
    -------------------------------------------
    +
    +
    +
    # my daughter's blog.
    + +
    +
    + +
    + + + + +
    + + + + + + + + +
    +
    + +
    + +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/page/17/index.html b/page/17/index.html new file mode 100644 index 0000000000..9a9d8be185 --- /dev/null +++ b/page/17/index.html @@ -0,0 +1,415 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 贾攀的流水账 + + + + + + + + + + + + + + + +
    +
    + +
    +
    + + +
    + + + +

    贾攀的流水账

    + +
    +

    Panmax's Blog

    +
    + + +
    + + + + + + + + +
    + +
    + +
    +
    + + +
    + + 0% +
    + + +
    +
    +
    + + +
    + +
    +
    人会长大三次。
    +
    -> 第一次是在发现自己不是世界中心的时候。
    +
    -> 第二次是在发现即使再怎么努力,终究还是有些事令人无能为力的时候。
    +
    -> 第三次是在,明知道有些事可能会无能为力,但还是会尽力争取的时候。
    +
    +
    if you know me:
    + +
    else:
    +
    return 关于我
    +
    +
    +
    -------------------------------------------
    +
    +
    +
    # my daughter's blog.
    + +
    +
    + +
    + + + + +
    + + + + + + + + +
    +
    + +
    + +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/page/18/index.html b/page/18/index.html new file mode 100644 index 0000000000..36f0d09e26 --- /dev/null +++ b/page/18/index.html @@ -0,0 +1,415 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 贾攀的流水账 + + + + + + + + + + + + + + + +
    +
    + +
    +
    + + +
    + + + +

    贾攀的流水账

    + +
    +

    Panmax's Blog

    +
    + + +
    + + + + + + + + +
    + +
    + +
    +
    + + +
    + + 0% +
    + + +
    +
    +
    + + +
    + +
    +
    人会长大三次。
    +
    -> 第一次是在发现自己不是世界中心的时候。
    +
    -> 第二次是在发现即使再怎么努力,终究还是有些事令人无能为力的时候。
    +
    -> 第三次是在,明知道有些事可能会无能为力,但还是会尽力争取的时候。
    +
    +
    if you know me:
    + +
    else:
    +
    return 关于我
    +
    +
    +
    -------------------------------------------
    +
    +
    +
    # my daughter's blog.
    + +
    +
    + +
    + + + + +
    + + + + + + + + +
    +
    + +
    + +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/page/19/index.html b/page/19/index.html new file mode 100644 index 0000000000..9c279209da --- /dev/null +++ b/page/19/index.html @@ -0,0 +1,415 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 贾攀的流水账 + + + + + + + + + + + + + + + +
    +
    + +
    +
    + + +
    + + + +

    贾攀的流水账

    + +
    +

    Panmax's Blog

    +
    + + +
    + + + + + + + + +
    + +
    + +
    +
    + + +
    + + 0% +
    + + +
    +
    +
    + + +
    + +
    +
    人会长大三次。
    +
    -> 第一次是在发现自己不是世界中心的时候。
    +
    -> 第二次是在发现即使再怎么努力,终究还是有些事令人无能为力的时候。
    +
    -> 第三次是在,明知道有些事可能会无能为力,但还是会尽力争取的时候。
    +
    +
    if you know me:
    + +
    else:
    +
    return 关于我
    +
    +
    +
    -------------------------------------------
    +
    +
    +
    # my daughter's blog.
    + +
    +
    + +
    + + + + +
    + + + + + + + + +
    +
    + +
    + +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/page/2/index.html b/page/2/index.html new file mode 100644 index 0000000000..5fddbb2de1 --- /dev/null +++ b/page/2/index.html @@ -0,0 +1,415 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 贾攀的流水账 + + + + + + + + + + + + + + + +
    +
    + +
    +
    + + +
    + + + +

    贾攀的流水账

    + +
    +

    Panmax's Blog

    +
    + + +
    + + + + + + + + +
    + +
    + +
    +
    + + +
    + + 0% +
    + + +
    +
    +
    + + +
    + +
    +
    人会长大三次。
    +
    -> 第一次是在发现自己不是世界中心的时候。
    +
    -> 第二次是在发现即使再怎么努力,终究还是有些事令人无能为力的时候。
    +
    -> 第三次是在,明知道有些事可能会无能为力,但还是会尽力争取的时候。
    +
    +
    if you know me:
    + +
    else:
    +
    return 关于我
    +
    +
    +
    -------------------------------------------
    +
    +
    +
    # my daughter's blog.
    + +
    +
    + +
    + + + + +
    + + + + + + + + +
    +
    + +
    + +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/page/20/index.html b/page/20/index.html new file mode 100644 index 0000000000..6118c7fd55 --- /dev/null +++ b/page/20/index.html @@ -0,0 +1,415 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 贾攀的流水账 + + + + + + + + + + + + + + + +
    +
    + +
    +
    + + +
    + + + +

    贾攀的流水账

    + +
    +

    Panmax's Blog

    +
    + + +
    + + + + + + + + +
    + +
    + +
    +
    + + +
    + + 0% +
    + + +
    +
    +
    + + +
    + +
    +
    人会长大三次。
    +
    -> 第一次是在发现自己不是世界中心的时候。
    +
    -> 第二次是在发现即使再怎么努力,终究还是有些事令人无能为力的时候。
    +
    -> 第三次是在,明知道有些事可能会无能为力,但还是会尽力争取的时候。
    +
    +
    if you know me:
    + +
    else:
    +
    return 关于我
    +
    +
    +
    -------------------------------------------
    +
    +
    +
    # my daughter's blog.
    + +
    +
    + +
    + + + + +
    + + + + + + + + +
    +
    + +
    + +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/page/21/index.html b/page/21/index.html new file mode 100644 index 0000000000..c8a60140a0 --- /dev/null +++ b/page/21/index.html @@ -0,0 +1,415 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 贾攀的流水账 + + + + + + + + + + + + + + + +
    +
    + +
    +
    + + +
    + + + +

    贾攀的流水账

    + +
    +

    Panmax's Blog

    +
    + + +
    + + + + + + + + +
    + +
    + +
    +
    + + +
    + + 0% +
    + + +
    +
    +
    + + +
    + +
    +
    人会长大三次。
    +
    -> 第一次是在发现自己不是世界中心的时候。
    +
    -> 第二次是在发现即使再怎么努力,终究还是有些事令人无能为力的时候。
    +
    -> 第三次是在,明知道有些事可能会无能为力,但还是会尽力争取的时候。
    +
    +
    if you know me:
    + +
    else:
    +
    return 关于我
    +
    +
    +
    -------------------------------------------
    +
    +
    +
    # my daughter's blog.
    + +
    +
    + +
    + + + + +
    + + + + + + + + +
    +
    + +
    + +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/page/22/index.html b/page/22/index.html new file mode 100644 index 0000000000..063c14f8d8 --- /dev/null +++ b/page/22/index.html @@ -0,0 +1,415 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 贾攀的流水账 + + + + + + + + + + + + + + + +
    +
    + +
    +
    + + +
    + + + +

    贾攀的流水账

    + +
    +

    Panmax's Blog

    +
    + + +
    + + + + + + + + +
    + +
    + +
    +
    + + +
    + + 0% +
    + + +
    +
    +
    + + +
    + +
    +
    人会长大三次。
    +
    -> 第一次是在发现自己不是世界中心的时候。
    +
    -> 第二次是在发现即使再怎么努力,终究还是有些事令人无能为力的时候。
    +
    -> 第三次是在,明知道有些事可能会无能为力,但还是会尽力争取的时候。
    +
    +
    if you know me:
    + +
    else:
    +
    return 关于我
    +
    +
    +
    -------------------------------------------
    +
    +
    +
    # my daughter's blog.
    + +
    +
    + +
    + + + + +
    + + + + + + + + +
    +
    + +
    + +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/page/23/index.html b/page/23/index.html new file mode 100644 index 0000000000..c40836c932 --- /dev/null +++ b/page/23/index.html @@ -0,0 +1,415 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 贾攀的流水账 + + + + + + + + + + + + + + + +
    +
    + +
    +
    + + +
    + + + +

    贾攀的流水账

    + +
    +

    Panmax's Blog

    +
    + + +
    + + + + + + + + +
    + +
    + +
    +
    + + +
    + + 0% +
    + + +
    +
    +
    + + +
    + +
    +
    人会长大三次。
    +
    -> 第一次是在发现自己不是世界中心的时候。
    +
    -> 第二次是在发现即使再怎么努力,终究还是有些事令人无能为力的时候。
    +
    -> 第三次是在,明知道有些事可能会无能为力,但还是会尽力争取的时候。
    +
    +
    if you know me:
    + +
    else:
    +
    return 关于我
    +
    +
    +
    -------------------------------------------
    +
    +
    +
    # my daughter's blog.
    + +
    +
    + +
    + + + + +
    + + + + + + + + +
    +
    + +
    + +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/page/24/index.html b/page/24/index.html new file mode 100644 index 0000000000..736fda874c --- /dev/null +++ b/page/24/index.html @@ -0,0 +1,415 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 贾攀的流水账 + + + + + + + + + + + + + + + +
    +
    + +
    +
    + + +
    + + + +

    贾攀的流水账

    + +
    +

    Panmax's Blog

    +
    + + +
    + + + + + + + + +
    + +
    + +
    +
    + + +
    + + 0% +
    + + +
    +
    +
    + + +
    + +
    +
    人会长大三次。
    +
    -> 第一次是在发现自己不是世界中心的时候。
    +
    -> 第二次是在发现即使再怎么努力,终究还是有些事令人无能为力的时候。
    +
    -> 第三次是在,明知道有些事可能会无能为力,但还是会尽力争取的时候。
    +
    +
    if you know me:
    + +
    else:
    +
    return 关于我
    +
    +
    +
    -------------------------------------------
    +
    +
    +
    # my daughter's blog.
    + +
    +
    + +
    + + + + +
    + + + + + + + + +
    +
    + +
    + +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/page/25/index.html b/page/25/index.html new file mode 100644 index 0000000000..1c349da833 --- /dev/null +++ b/page/25/index.html @@ -0,0 +1,415 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 贾攀的流水账 + + + + + + + + + + + + + + + +
    +
    + +
    +
    + + +
    + + + +

    贾攀的流水账

    + +
    +

    Panmax's Blog

    +
    + + +
    + + + + + + + + +
    + +
    + +
    +
    + + +
    + + 0% +
    + + +
    +
    +
    + + +
    + +
    +
    人会长大三次。
    +
    -> 第一次是在发现自己不是世界中心的时候。
    +
    -> 第二次是在发现即使再怎么努力,终究还是有些事令人无能为力的时候。
    +
    -> 第三次是在,明知道有些事可能会无能为力,但还是会尽力争取的时候。
    +
    +
    if you know me:
    + +
    else:
    +
    return 关于我
    +
    +
    +
    -------------------------------------------
    +
    +
    +
    # my daughter's blog.
    + +
    +
    + +
    + + + + +
    + + + + + + + + +
    +
    + +
    + +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/page/26/index.html b/page/26/index.html new file mode 100644 index 0000000000..d841dfca28 --- /dev/null +++ b/page/26/index.html @@ -0,0 +1,415 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 贾攀的流水账 + + + + + + + + + + + + + + + +
    +
    + +
    +
    + + +
    + + + +

    贾攀的流水账

    + +
    +

    Panmax's Blog

    +
    + + +
    + + + + + + + + +
    + +
    + +
    +
    + + +
    + + 0% +
    + + +
    +
    +
    + + +
    + +
    +
    人会长大三次。
    +
    -> 第一次是在发现自己不是世界中心的时候。
    +
    -> 第二次是在发现即使再怎么努力,终究还是有些事令人无能为力的时候。
    +
    -> 第三次是在,明知道有些事可能会无能为力,但还是会尽力争取的时候。
    +
    +
    if you know me:
    + +
    else:
    +
    return 关于我
    +
    +
    +
    -------------------------------------------
    +
    +
    +
    # my daughter's blog.
    + +
    +
    + +
    + + + + +
    + + + + + + + + +
    +
    + +
    + +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/page/27/index.html b/page/27/index.html new file mode 100644 index 0000000000..8f1336b6ed --- /dev/null +++ b/page/27/index.html @@ -0,0 +1,415 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 贾攀的流水账 + + + + + + + + + + + + + + + +
    +
    + +
    +
    + + +
    + + + +

    贾攀的流水账

    + +
    +

    Panmax's Blog

    +
    + + +
    + + + + + + + + +
    + +
    + +
    +
    + + +
    + + 0% +
    + + +
    +
    +
    + + +
    + +
    +
    人会长大三次。
    +
    -> 第一次是在发现自己不是世界中心的时候。
    +
    -> 第二次是在发现即使再怎么努力,终究还是有些事令人无能为力的时候。
    +
    -> 第三次是在,明知道有些事可能会无能为力,但还是会尽力争取的时候。
    +
    +
    if you know me:
    + +
    else:
    +
    return 关于我
    +
    +
    +
    -------------------------------------------
    +
    +
    +
    # my daughter's blog.
    + +
    +
    + +
    + + + + +
    + + + + + + + + +
    +
    + +
    + +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/page/28/index.html b/page/28/index.html new file mode 100644 index 0000000000..beedae9602 --- /dev/null +++ b/page/28/index.html @@ -0,0 +1,415 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 贾攀的流水账 + + + + + + + + + + + + + + + +
    +
    + +
    +
    + + +
    + + + +

    贾攀的流水账

    + +
    +

    Panmax's Blog

    +
    + + +
    + + + + + + + + +
    + +
    + +
    +
    + + +
    + + 0% +
    + + +
    +
    +
    + + +
    + +
    +
    人会长大三次。
    +
    -> 第一次是在发现自己不是世界中心的时候。
    +
    -> 第二次是在发现即使再怎么努力,终究还是有些事令人无能为力的时候。
    +
    -> 第三次是在,明知道有些事可能会无能为力,但还是会尽力争取的时候。
    +
    +
    if you know me:
    + +
    else:
    +
    return 关于我
    +
    +
    +
    -------------------------------------------
    +
    +
    +
    # my daughter's blog.
    + +
    +
    + +
    + + + + +
    + + + + + + + + +
    +
    + +
    + +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/page/29/index.html b/page/29/index.html new file mode 100644 index 0000000000..26283310ce --- /dev/null +++ b/page/29/index.html @@ -0,0 +1,415 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 贾攀的流水账 + + + + + + + + + + + + + + + +
    +
    + +
    +
    + + +
    + + + +

    贾攀的流水账

    + +
    +

    Panmax's Blog

    +
    + + +
    + + + + + + + + +
    + +
    + +
    +
    + + +
    + + 0% +
    + + +
    +
    +
    + + +
    + +
    +
    人会长大三次。
    +
    -> 第一次是在发现自己不是世界中心的时候。
    +
    -> 第二次是在发现即使再怎么努力,终究还是有些事令人无能为力的时候。
    +
    -> 第三次是在,明知道有些事可能会无能为力,但还是会尽力争取的时候。
    +
    +
    if you know me:
    + +
    else:
    +
    return 关于我
    +
    +
    +
    -------------------------------------------
    +
    +
    +
    # my daughter's blog.
    + +
    +
    + +
    + + + + +
    + + + + + + + + +
    +
    + +
    + +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/page/3/index.html b/page/3/index.html new file mode 100644 index 0000000000..9e0352ad63 --- /dev/null +++ b/page/3/index.html @@ -0,0 +1,415 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 贾攀的流水账 + + + + + + + + + + + + + + + +
    +
    + +
    +
    + + +
    + + + +

    贾攀的流水账

    + +
    +

    Panmax's Blog

    +
    + + +
    + + + + + + + + +
    + +
    + +
    +
    + + +
    + + 0% +
    + + +
    +
    +
    + + +
    + +
    +
    人会长大三次。
    +
    -> 第一次是在发现自己不是世界中心的时候。
    +
    -> 第二次是在发现即使再怎么努力,终究还是有些事令人无能为力的时候。
    +
    -> 第三次是在,明知道有些事可能会无能为力,但还是会尽力争取的时候。
    +
    +
    if you know me:
    + +
    else:
    +
    return 关于我
    +
    +
    +
    -------------------------------------------
    +
    +
    +
    # my daughter's blog.
    + +
    +
    + +
    + + + + +
    + + + + + + + + +
    +
    + +
    + +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/page/30/index.html b/page/30/index.html new file mode 100644 index 0000000000..801a151eed --- /dev/null +++ b/page/30/index.html @@ -0,0 +1,415 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 贾攀的流水账 + + + + + + + + + + + + + + + +
    +
    + +
    +
    + + +
    + + + +

    贾攀的流水账

    + +
    +

    Panmax's Blog

    +
    + + +
    + + + + + + + + +
    + +
    + +
    +
    + + +
    + + 0% +
    + + +
    +
    +
    + + +
    + +
    +
    人会长大三次。
    +
    -> 第一次是在发现自己不是世界中心的时候。
    +
    -> 第二次是在发现即使再怎么努力,终究还是有些事令人无能为力的时候。
    +
    -> 第三次是在,明知道有些事可能会无能为力,但还是会尽力争取的时候。
    +
    +
    if you know me:
    + +
    else:
    +
    return 关于我
    +
    +
    +
    -------------------------------------------
    +
    +
    +
    # my daughter's blog.
    + +
    +
    + +
    + + + + +
    + + + + + + + + +
    +
    + +
    + +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/page/31/index.html b/page/31/index.html new file mode 100644 index 0000000000..7c4fe3a5f8 --- /dev/null +++ b/page/31/index.html @@ -0,0 +1,415 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 贾攀的流水账 + + + + + + + + + + + + + + + +
    +
    + +
    +
    + + +
    + + + +

    贾攀的流水账

    + +
    +

    Panmax's Blog

    +
    + + +
    + + + + + + + + +
    + +
    + +
    +
    + + +
    + + 0% +
    + + +
    +
    +
    + + +
    + +
    +
    人会长大三次。
    +
    -> 第一次是在发现自己不是世界中心的时候。
    +
    -> 第二次是在发现即使再怎么努力,终究还是有些事令人无能为力的时候。
    +
    -> 第三次是在,明知道有些事可能会无能为力,但还是会尽力争取的时候。
    +
    +
    if you know me:
    + +
    else:
    +
    return 关于我
    +
    +
    +
    -------------------------------------------
    +
    +
    +
    # my daughter's blog.
    + +
    +
    + +
    + + + + +
    + + + + + + + + +
    +
    + +
    + +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/page/32/index.html b/page/32/index.html new file mode 100644 index 0000000000..4bfb851541 --- /dev/null +++ b/page/32/index.html @@ -0,0 +1,415 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 贾攀的流水账 + + + + + + + + + + + + + + + +
    +
    + +
    +
    + + +
    + + + +

    贾攀的流水账

    + +
    +

    Panmax's Blog

    +
    + + +
    + + + + + + + + +
    + +
    + +
    +
    + + +
    + + 0% +
    + + +
    +
    +
    + + +
    + +
    +
    人会长大三次。
    +
    -> 第一次是在发现自己不是世界中心的时候。
    +
    -> 第二次是在发现即使再怎么努力,终究还是有些事令人无能为力的时候。
    +
    -> 第三次是在,明知道有些事可能会无能为力,但还是会尽力争取的时候。
    +
    +
    if you know me:
    + +
    else:
    +
    return 关于我
    +
    +
    +
    -------------------------------------------
    +
    +
    +
    # my daughter's blog.
    + +
    +
    + +
    + + + + +
    + + + + + + + + +
    +
    + +
    + +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/page/33/index.html b/page/33/index.html new file mode 100644 index 0000000000..6cccaf8630 --- /dev/null +++ b/page/33/index.html @@ -0,0 +1,415 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 贾攀的流水账 + + + + + + + + + + + + + + + +
    +
    + +
    +
    + + +
    + + + +

    贾攀的流水账

    + +
    +

    Panmax's Blog

    +
    + + +
    + + + + + + + + +
    + +
    + +
    +
    + + +
    + + 0% +
    + + +
    +
    +
    + + +
    + +
    +
    人会长大三次。
    +
    -> 第一次是在发现自己不是世界中心的时候。
    +
    -> 第二次是在发现即使再怎么努力,终究还是有些事令人无能为力的时候。
    +
    -> 第三次是在,明知道有些事可能会无能为力,但还是会尽力争取的时候。
    +
    +
    if you know me:
    + +
    else:
    +
    return 关于我
    +
    +
    +
    -------------------------------------------
    +
    +
    +
    # my daughter's blog.
    + +
    +
    + +
    + + + + +
    + + + + + + + + +
    +
    + +
    + +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/page/34/index.html b/page/34/index.html new file mode 100644 index 0000000000..cd13852c74 --- /dev/null +++ b/page/34/index.html @@ -0,0 +1,415 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 贾攀的流水账 + + + + + + + + + + + + + + + +
    +
    + +
    +
    + + +
    + + + +

    贾攀的流水账

    + +
    +

    Panmax's Blog

    +
    + + +
    + + + + + + + + +
    + +
    + +
    +
    + + +
    + + 0% +
    + + +
    +
    +
    + + +
    + +
    +
    人会长大三次。
    +
    -> 第一次是在发现自己不是世界中心的时候。
    +
    -> 第二次是在发现即使再怎么努力,终究还是有些事令人无能为力的时候。
    +
    -> 第三次是在,明知道有些事可能会无能为力,但还是会尽力争取的时候。
    +
    +
    if you know me:
    + +
    else:
    +
    return 关于我
    +
    +
    +
    -------------------------------------------
    +
    +
    +
    # my daughter's blog.
    + +
    +
    + +
    + + + + +
    + + + + + + + + +
    +
    + +
    + +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/page/35/index.html b/page/35/index.html new file mode 100644 index 0000000000..14d1e3e736 --- /dev/null +++ b/page/35/index.html @@ -0,0 +1,415 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 贾攀的流水账 + + + + + + + + + + + + + + + +
    +
    + +
    +
    + + +
    + + + +

    贾攀的流水账

    + +
    +

    Panmax's Blog

    +
    + + +
    + + + + + + + + +
    + +
    + +
    +
    + + +
    + + 0% +
    + + +
    +
    +
    + + +
    + +
    +
    人会长大三次。
    +
    -> 第一次是在发现自己不是世界中心的时候。
    +
    -> 第二次是在发现即使再怎么努力,终究还是有些事令人无能为力的时候。
    +
    -> 第三次是在,明知道有些事可能会无能为力,但还是会尽力争取的时候。
    +
    +
    if you know me:
    + +
    else:
    +
    return 关于我
    +
    +
    +
    -------------------------------------------
    +
    +
    +
    # my daughter's blog.
    + +
    +
    + +
    + + + + +
    + + + + + + + + +
    +
    + +
    + +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/page/36/index.html b/page/36/index.html new file mode 100644 index 0000000000..702214a02e --- /dev/null +++ b/page/36/index.html @@ -0,0 +1,415 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 贾攀的流水账 + + + + + + + + + + + + + + + +
    +
    + +
    +
    + + +
    + + + +

    贾攀的流水账

    + +
    +

    Panmax's Blog

    +
    + + +
    + + + + + + + + +
    + +
    + +
    +
    + + +
    + + 0% +
    + + +
    +
    +
    + + +
    + +
    +
    人会长大三次。
    +
    -> 第一次是在发现自己不是世界中心的时候。
    +
    -> 第二次是在发现即使再怎么努力,终究还是有些事令人无能为力的时候。
    +
    -> 第三次是在,明知道有些事可能会无能为力,但还是会尽力争取的时候。
    +
    +
    if you know me:
    + +
    else:
    +
    return 关于我
    +
    +
    +
    -------------------------------------------
    +
    +
    +
    # my daughter's blog.
    + +
    +
    + +
    + + + + +
    + + + + + + + + +
    +
    + +
    + +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/page/4/index.html b/page/4/index.html new file mode 100644 index 0000000000..b885534a9f --- /dev/null +++ b/page/4/index.html @@ -0,0 +1,415 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 贾攀的流水账 + + + + + + + + + + + + + + + +
    +
    + +
    +
    + + +
    + + + +

    贾攀的流水账

    + +
    +

    Panmax's Blog

    +
    + + +
    + + + + + + + + +
    + +
    + +
    +
    + + +
    + + 0% +
    + + +
    +
    +
    + + +
    + +
    +
    人会长大三次。
    +
    -> 第一次是在发现自己不是世界中心的时候。
    +
    -> 第二次是在发现即使再怎么努力,终究还是有些事令人无能为力的时候。
    +
    -> 第三次是在,明知道有些事可能会无能为力,但还是会尽力争取的时候。
    +
    +
    if you know me:
    + +
    else:
    +
    return 关于我
    +
    +
    +
    -------------------------------------------
    +
    +
    +
    # my daughter's blog.
    + +
    +
    + +
    + + + + +
    + + + + + + + + +
    +
    + +
    + +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/page/5/index.html b/page/5/index.html new file mode 100644 index 0000000000..16ad5b5b4d --- /dev/null +++ b/page/5/index.html @@ -0,0 +1,415 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 贾攀的流水账 + + + + + + + + + + + + + + + +
    +
    + +
    +
    + + +
    + + + +

    贾攀的流水账

    + +
    +

    Panmax's Blog

    +
    + + +
    + + + + + + + + +
    + +
    + +
    +
    + + +
    + + 0% +
    + + +
    +
    +
    + + +
    + +
    +
    人会长大三次。
    +
    -> 第一次是在发现自己不是世界中心的时候。
    +
    -> 第二次是在发现即使再怎么努力,终究还是有些事令人无能为力的时候。
    +
    -> 第三次是在,明知道有些事可能会无能为力,但还是会尽力争取的时候。
    +
    +
    if you know me:
    + +
    else:
    +
    return 关于我
    +
    +
    +
    -------------------------------------------
    +
    +
    +
    # my daughter's blog.
    + +
    +
    + +
    + + + + +
    + + + + + + + + +
    +
    + +
    + +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/page/6/index.html b/page/6/index.html new file mode 100644 index 0000000000..d79e72f471 --- /dev/null +++ b/page/6/index.html @@ -0,0 +1,415 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 贾攀的流水账 + + + + + + + + + + + + + + + +
    +
    + +
    +
    + + +
    + + + +

    贾攀的流水账

    + +
    +

    Panmax's Blog

    +
    + + +
    + + + + + + + + +
    + +
    + +
    +
    + + +
    + + 0% +
    + + +
    +
    +
    + + +
    + +
    +
    人会长大三次。
    +
    -> 第一次是在发现自己不是世界中心的时候。
    +
    -> 第二次是在发现即使再怎么努力,终究还是有些事令人无能为力的时候。
    +
    -> 第三次是在,明知道有些事可能会无能为力,但还是会尽力争取的时候。
    +
    +
    if you know me:
    + +
    else:
    +
    return 关于我
    +
    +
    +
    -------------------------------------------
    +
    +
    +
    # my daughter's blog.
    + +
    +
    + +
    + + + + +
    + + + + + + + + +
    +
    + +
    + +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/page/7/index.html b/page/7/index.html new file mode 100644 index 0000000000..f4f536294f --- /dev/null +++ b/page/7/index.html @@ -0,0 +1,415 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 贾攀的流水账 + + + + + + + + + + + + + + + +
    +
    + +
    +
    + + +
    + + + +

    贾攀的流水账

    + +
    +

    Panmax's Blog

    +
    + + +
    + + + + + + + + +
    + +
    + +
    +
    + + +
    + + 0% +
    + + +
    +
    +
    + + +
    + +
    +
    人会长大三次。
    +
    -> 第一次是在发现自己不是世界中心的时候。
    +
    -> 第二次是在发现即使再怎么努力,终究还是有些事令人无能为力的时候。
    +
    -> 第三次是在,明知道有些事可能会无能为力,但还是会尽力争取的时候。
    +
    +
    if you know me:
    + +
    else:
    +
    return 关于我
    +
    +
    +
    -------------------------------------------
    +
    +
    +
    # my daughter's blog.
    + +
    +
    + +
    + + + + +
    + + + + + + + + +
    +
    + +
    + +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/page/8/index.html b/page/8/index.html new file mode 100644 index 0000000000..53a78eb1e5 --- /dev/null +++ b/page/8/index.html @@ -0,0 +1,415 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 贾攀的流水账 + + + + + + + + + + + + + + + +
    +
    + +
    +
    + + +
    + + + +

    贾攀的流水账

    + +
    +

    Panmax's Blog

    +
    + + +
    + + + + + + + + +
    + +
    + +
    +
    + + +
    + + 0% +
    + + +
    +
    +
    + + +
    + +
    +
    人会长大三次。
    +
    -> 第一次是在发现自己不是世界中心的时候。
    +
    -> 第二次是在发现即使再怎么努力,终究还是有些事令人无能为力的时候。
    +
    -> 第三次是在,明知道有些事可能会无能为力,但还是会尽力争取的时候。
    +
    +
    if you know me:
    + +
    else:
    +
    return 关于我
    +
    +
    +
    -------------------------------------------
    +
    +
    +
    # my daughter's blog.
    + +
    +
    + +
    + + + + +
    + + + + + + + + +
    +
    + +
    + +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/page/9/index.html b/page/9/index.html new file mode 100644 index 0000000000..b323099c1e --- /dev/null +++ b/page/9/index.html @@ -0,0 +1,415 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 贾攀的流水账 + + + + + + + + + + + + + + + +
    +
    + +
    +
    + + +
    + + + +

    贾攀的流水账

    + +
    +

    Panmax's Blog

    +
    + + +
    + + + + + + + + +
    + +
    + +
    +
    + + +
    + + 0% +
    + + +
    +
    +
    + + +
    + +
    +
    人会长大三次。
    +
    -> 第一次是在发现自己不是世界中心的时候。
    +
    -> 第二次是在发现即使再怎么努力,终究还是有些事令人无能为力的时候。
    +
    -> 第三次是在,明知道有些事可能会无能为力,但还是会尽力争取的时候。
    +
    +
    if you know me:
    + +
    else:
    +
    return 关于我
    +
    +
    +
    -------------------------------------------
    +
    +
    +
    # my daughter's blog.
    + +
    +
    + +
    + + + + +
    + + + + + + + + +
    +
    + +
    + +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/search.json b/search.json new file mode 100644 index 0000000000..ab400bec7c --- /dev/null +++ b/search.json @@ -0,0 +1 @@ +[{"title":"2016心愿单","url":"/2015/2016%E5%BF%83%E6%84%BF%E5%8D%95/","content":"
      \n
    • 学习 IOS 开发(Swift)并做一款 APP 上架到Apple Store
    • \n
    • 学会尤克里里
    • \n
    • 每月至少读一本书
    • \n
    • Marry
    • \n
    \n","categories":["小攀说"],"tags":["2016"]},{"title":"2022 年上半年我使用的新工具盘点","url":"/2022/2022-first-half-app/","content":"

    最近刚好听到一句话「学霸两支笔,差生文具多」,我觉得里边的差生形容我再适合不过了。我总是喜欢以「磨刀不误砍柴工」、「工具善其事必先利其器」的“借口”去找寻那些(可能)有用、能提高我效率的五花八门的工具,我日常用到的工具太多了,下边盘点几个我在今年上半年入手并且用认为好用的几个新工具,包括 Native 工具、Web 工具和 Chrome 插件。

    \n

    飞书妙记

    我平时喜欢听一些内容,比如蒋勋讲解的红楼梦,有时候听到好的段落想把那些部分记录下来,但又不想一个字一个字地手敲,所以一直在找一款好用的语音转文字的工具,最后被一个叫《 组织进化论 》的播客节目安利了飞书 App 里的「飞书妙记」的功能,这里提一句「组织进化论」也是字节孵化的一档节目。

    \n

    我为了试用这个功能而下载了飞书,给我惊喜的是真的非常好用(包括飞书和飞书妙记),不仅支持导入音频文件转文字还支持实时录制的音频转文字,如果是多个人对话的话,可以自动帮我们把多个说话人区分出来。这就不止可以让我从有声书中提取内容了,它还可以让我更轻松地记录会议内容:好记性不如烂笔头,开会的时候打开录音模式,会后「妙记」就帮我把逐字稿生成了,参考着逐字稿写会议纪要就不会漏掉任何重要内容了。我还尝试使用在面试的场合,比如下图是我近期面的一个候选人,可以给我在后期写面评时提供抓手。

    \n

    \n

    妙记还可以把对话中的关键词帮我们提取出来:

    \n

    \n

    最最重要的是,它还是免费的,我之前用过几个收费的,比如科大讯飞,不管是价格还是可操作性方面都被飞书妙记吊打,但是有一说一正确性方面相比科大讯飞,妙记还是有一定提升空间的。

    \n

    MenubarX

    MenubarX 是一款非常实用的 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。

    \n

    Image Smith

    Image Smith 这是个图片压缩工具,压缩后的图片体积可以减少非常多,而且压缩后的图片质量我们肉眼看不出差别(反正我是看不出来)。

    \n

    我有时会把手机拍的照片放到博客中,比如这篇:https://jiapan.me/2022/da-guan-yuan/,原图一张十几 MB,压缩后可以到几 MB,这样能节省一半多的空间和带宽,而且效果上没有差别。

    \n

    \n

    下载地址:https://apps.apple.com/app/id1623828135

    \n

    这个工具也是我在作者刚上架限免时候下载的,现在卖 30RMB。类似的图片压缩工具还有一些在线版本,不想花钱买的可以试试:

    \n\n

    为了用户体验和性能,我在有 Native 版本的情况下就不用 Web 版的,所以上边那几个在线版本我没有深入使用和对比过,都是之前收藏的以备不时之需,我按照网站的颜值排了序,大家可以自己测评一下看看哪个更好用。

    \n

    Input Source Pro

    这个工具可以帮我在使用不同的软件时自动切换到不同的输入法,避免我们因切换输入法而打断思路,比如在用开发工具(如 IDEA、GoLand、WebStorm)时切换到英文,在使用钉钉、微信这类聊天工具时切换到百度输入法,甚至支持在浏览不同的网站时使用不同的输入法。还可以做到自动记录上一次在某个软件中使用的输入法,下次再切回这个软件时自动切换为上一次使用的输入法。

    \n

    \n

    每次从一个软件换到另一个软件时,Input Source Pro 都会贴心的提醒我当前在用(后这切换后)的输入法是哪个,但这个功能有个不好的地方,会在我要截图时给我带来困扰,每次我都要等 3 秒,等那个输入法悬浮提示消失后才能截图。

    \n

    这个工具的同样想法我之前也想到过,而且还和朋友讨论过,无奈吃了不会 macOS 开发的亏。既然有人做了,而且做的还不错,那咱也就没必要再惦记着造轮子了。

    \n

    官方地址:https://inputsource.pro/zh-CN

    \n

    ZenUML(Web 工具)

    ZenUML 是一款用伪代码画时序图的在线工具,画出来的时序图简洁漂亮,左边写代码右侧的时序图实时生成,所见即所得,而且可以直接导出 PNG 或者 JPG 格式的图片,登录后还可以将作品进行保存。我最近用它画了好多图,经常在做完方案介绍后被人问到图是用什么软件画的。

    \n

    \n

    而且这还个良心工具,除非你想为信仰充值升级到 Pro 版,它们的唯一区别是 Pro 版没有代码行数的限制。免费版代码限制多少行我并不是很清楚,我之前画过几个很复杂的流程图都没有触发限制,说明我完全没有升级 Pro 版的必要。

    \n

    \n

    官网地址:https://app.zenuml.com/

    \n

    类似工具还有:

    \n\n

    ripgrep(命令行工具)

    ripgrep 是一个支持递归和正则且性能超强的文本搜索命令行工具,类似于系统自带的 grep,但是甩 grep 几十条街。

    \n

    Mac 安装:

    \n
    brew install ripgrep
    \n

    虽然它叫 ribgrep,为了让用户更方便使用,它在命令行中输入 rg 就可以使用了。我会把所有公司的项目放到同一个目录下,有次一个同事问我是否知道某个类型的消息是哪个服务发出的,我通过这个工具快速给了他答案:

    \n

    \n

    ripgrep 是开源、用 Rust 编写的:https://github.com/BurntSushi/ripgrep

    \n

    tldr(命令行工具)

    tldr 是 too long; didn’t read 的缩写,tldr 这个工具也是出于同样的目的,告诉我们某个命令行的最常用、最实用的用法。我们在用某个命令时,如果看它的 help 可能会看到巨多无比的参数,从网上搜又会很费时,这时候 tldr 就派上用场了。

    \n

    比如以上边刚刚介绍的 rg 为例,我想知道它的常用功能都有那些,如果用 rg --help 参数会多到直接让我放弃,看下 tldr 的效果:

    \n
    ~ took 5s ➜ tldr rg

    rg

    Ripgrep is a recursive line-oriented CLI search tool.
    Aims to be a faster alternative to `grep`.
    More information: <https://github.com/BurntSushi/ripgrep>.

    - Recursively search the current directory for a regular expression:
    rg regular_expression

    - Search for regular expressions recursively in the current directory, including hidden files and files listed in `.gitignore`:
    rg --no-ignore --hidden regular_expression

    - Search for a regular expression only in a certain filetype (e.g. HTML, CSS, etc.):
    rg --type filetype regular_expression

    - Search for a regular expression only in a subset of directories:
    rg regular_expression set_of_subdirs

    - Search for a regular expression in files matching a glob (e.g. `README.*`):
    rg regular_expression --glob glob

    - Only list matched files (useful when piping to other commands):
    rg --files-with-matches regular_expression

    - Show lines that do not match the given regular expression:
    rg --invert-match regular_expression

    - Search a literal string pattern:
    rg --fixed-strings -- string
    \n

    tldr 用一句话给我们描述了 rg 的功能,并给出了官方地址。下边还列了一些常见用法,比如不加任何参数可以递归查询当前目录下所有文件。rg --type filetype regular_expression 可以指定要查询的文件类型,等等。是不是比官方手册实用很多而且省去了到网上查一圈的麻烦。

    \n

    还有很多小伙伴经常忘记 tar 的用法,也可以用 tldr 来做个快速回顾:

    \n
    ~ ➜ tldr tar

    tar

    Archiving utility.
    Often combined with a compression method, such as gzip or bzip2.
    More information: <https://www.gnu.org/software/tar>.

    - [c]reate an archive and write it to a [f]ile:
    tar cf target.tar file1 file2 file3

    - [c]reate a g[z]ipped archive and write it to a [f]ile:
    tar czf target.tar.gz file1 file2 file3

    - [c]reate a g[z]ipped archive from a directory using relative paths:
    tar czf target.tar.gz --directory=path/to/directory .

    - E[x]tract a (compressed) archive [f]ile into the current directory [v]erbosely:
    tar xvf source.tar[.gz|.bz2|.xz]

    - E[x]tract a (compressed) archive [f]ile into the target directory:
    tar xf source.tar[.gz|.bz2|.xz] --directory=directory

    - [c]reate a compressed archive and write it to a [f]ile, using [a]rchive suffix to determine the compression program:
    tar caf target.tar.xz file1 file2 file3

    - Lis[t] the contents of a tar [f]ile [v]erbosely:
    tar tvf source.tar

    - E[x]tract files matching a pattern from an archive [f]ile:
    tar xf source.tar --wildcards "*.html"
    \n

    Mac 安装:

    \n
    brew info tldr
    \n

    tldr 也是个开源项目,Github 地址:https://github.com/tldr-pages/tldr

    \n

    GitToolBox(JetBrains 插件)

    GitToolBox 是个 IDE 插件,这个插件可以直接让我们看到光标所在代码行的提交信息(提交人, 提交时间, CommitMessage),不用再通过侧边栏的 Annotate with Git Blame 来查看了,很方便。

    \n

    \n

    Relingo(Chrome 插件)

    Relingo 是个浏览器插件,可以在阅读英语文章时自动标记出那些我们可能生疏单词的解释,当我们认识某个单词后鼠标悬浮到对应单词然后在弹出框上打个勾,之后就不会再标记这个单词了,还可以对已经掌握的单词进行回顾,是个阅读英语文章和学习英语的不错工具。

    \n

    \n

    官方地址:https://relingo.net/zh/index

    \n

    Language Reactor(Chrome 插件)

    Language Reactor 也是个浏览器插件,可以让我们在 Youtube 看英语视频时通过字幕学习遇到的句子或单词。打开视频后会自动在视频右侧加载出字幕列表,可以直接点击某行字幕将视频进度跳转到我们想看的那句话,在视频中鼠标悬浮在某个单词上后视频会自动暂停播放,然后弹出这个单词的解释,鼠标移开后自动开始播放。

    \n

    \n

    Relingo 也带类似功能,但是术业有专攻,我觉得 Reactor 做的更好一些,所以如果两个插件都装了的话,需要手动把 Relingo 的字幕功能关闭。

    \n

    \n

    官方地址:https://www.languagereactor.com/
    Chrome 安装地址:https://chrome.google.com/webstore/detail/language-reactor/hoombieeljmmljlkjmnheibnpciblicm?hl=zh-CN

    \n

    我从哪里听说的这些新工具?

    上边的工具大部分都是我通过 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
    \n

    最后用AI帮我补充的句子来结尾:

    \n
    \n

    在这样的节日里,我们不仅要缅怀已故亲人,也要珍惜眼前人,因为生命短暂而珍贵。同时,也要引以为戒赌博的危害,要远离赌博,珍爱自己和家人的幸福生活。

    \n
    \n"},{"title":"关于成年人的30个生存技巧","url":"/2023/30-Survival-Tips-for-Adults/","content":"
      \n
    1. 手机上最好的生产力应用程序叫做飞行模式
    2. \n
    3. 让“不”成为你的默认选项。无论是新工作项目还是社交聚会,对非优先事项说“是”会破坏你的优先事项。如果这不是一个“肯定”,那么就是否定它。
    4. \n
    5. 将“我对此一无所知”规范化为成功答案。
    6. \n
    7. 如果书或电影很糟糕,你不必看完。
    8. \n
    9. 停止后悔过去的决定。在当时拥有自己所掌握的知识下做出了最佳选择。与之和平相处吧。
    10. \n
    11. 成功归结为一个简单的选择:1、确定自己想要什么;2、确定需要付出多少代价;3、选择是否愿意付出代价。
    12. \n
    13. 写下您的 3:3:3 计划:专注于您最重要项目 3 小时时间、完成 3 个较短任务和进行 3 次维护活动。定义一个“高效率”的一天至关重要。否则即使输出极佳也永远无法得到平静。
    14. \n
    15. 找到您的能量高峰状态(大多数人在早上)。在此期间时间块化 2-4 小时进行最重要的任务(参见 #7)。
    16. \n
    17. 避免早上第一件事就查看手机。幸福来源于精心设计,而不是靠运气。
    18. \n
    19. 为你的一天(就像电影一样)配上音乐。音乐是强大的情绪增强剂。几乎所有事情都可以通过音乐更加愉快地完成。
    20. \n
    21. 每天早上进行内部清洁。立刻喝一杯满满的水。你的身体60%是水,要及时补充。
    22. \n
    23. 遵循日本的80%法则:“吃到只有80%饱为止。”没有人有时间进入食物昏迷状态。
    24. \n
    25. 规范午间小睡。这样可以让你在一天中获得两倍的精力。
    26. \n
    27. 我们的祖先曾经狩猎长毛象,现在我们每天坐8个小时办公桌,然后平均再看3个小时电视。人类不适合这种生活方式。使用站立式办公桌、进行步行会议、每天运动身体等方法来改变习惯。
    28. \n
    29. 心理学认为,你如何谈论别人就是你如何对待自己(所以要友善)。
    30. \n
    31. 聚光效应(偏见):我们认为别人比实际更关注我们。残酷的事实是:当你意识到没有人在想着你时,你才真正拥有自由
    32. \n
    33. 停止循环思考,提出有效问题。如果和我一样总会反复回忆某些事情,请问自己:这有用吗?1年后我还会关心它吗?
    34. \n
    35. 磨练你的肢体语言(7-38-55法则)。人们会根据以下因素来喜欢/不喜欢你的交流:7%是用词,38%是语调和面部表情,55%是肢体语言。站直身子、挺胸抬头、眼神交流、微笑并握紧手臂。这样你就会变得更有魅力。
    36. \n
    37. 一个人最喜欢听到的声音就是自己的名字。他们第二个最喜欢听到的声音是他们所爱之人和宠物的名字。每当你听到其中一个被提及,请记下来。之后,通过“姓名”询问他们如何了解这些信息,这样可以让你脱颖而出。
    38. \n
    39. 花更多时间与给予你能量的人在一起,减少与夺走你能量的人相处时间。
    40. \n
    41. 掌握“告别的礼物”。你不欠一个贬低你的朋友、伴侣或雇主忠诚。成功和幸福的人只是简单地说再见。
    42. \n
    43. 每周做一些独自的事情(晚餐、电影等)。社会已经让我们认为独自做事很奇怪了。但如果你不习惯独处,就永远无法舒适地离开有毒关系。一个能够快乐独处的人是一个强大的人。
    44. \n
    45. 写下你的目标。提醒自己想成为什么样子。拥有目标的14% 的人比没有目标者成功率高10倍,而拥有书面目标计划并实施它们3% 的人比只是拥有目标者成功率高3倍。
    46. \n
    47. 避免告诉别人你的目标。这会释放廉价多巴胺,并欺骗你大脑以为已经实现了它们(降低动力)。悄悄行动吧。
    48. \n
    49. 简化您的财务:取消未使用订阅服务;自动支付账单、储蓄和投资;按50/30/20规则预算(50%用于需求,30%用于愿望,20%用于储蓄)。
    50. \n
    51. 购买能让你更健康、更富有或提供你自由时间的东西。这被称为实际唯物主义:产品可以在您生活质量上产生实质性影响。
    52. \n
    53. 使用1%规则来控制冲动购买。如果该物品超过您年收入的1%,请等待3天。如果3天后仍然想要该物品,请购买它。通常情况下,您会意识到自己并不真正需要那个东西。
    54. \n
    55. 如果购买了一件物品,则捐赠、丢弃或出售另一件物品。极简主义是一个双重学科:管理进入和离开的所有物品以保持平衡。
    56. \n
    57. 给你的大脑留下一个隔夜任务。闭上眼睛时给你的大脑一个任务。“我怎样才能每月多赚1000元?”不要试图当场解决它;只需将其释放到潜意识中(它会在晚上处理)。
    58. \n
    59. 让奇怪变得正常。“奇怪之处就是我们与众不同、被雇佣的原因所在。成为毫无歉意地独特的自己。事实上,变得奇怪甚至可能带给你终极幸福”
    60. \n
    \n"},{"title":"8 条荒谬的分布式假设","url":"/2019/8-ridiculous-distributed-assumptions/","content":"
      \n
    1. 网络是稳定的。
    2. \n
    3. 网络传输的延迟是零。
    4. \n
    5. 网络的带宽是无穷大。
    6. \n
    7. 网络是安全的。
    8. \n
    9. 网络的拓扑不会改变。
    10. \n
    11. 只有一个系统管理员。
    12. \n
    13. 传输数据的成本为零。
    14. \n
    15. 整个网络是同构的。
    16. \n
    \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

    支持它们的社会规范既不是人类自然的天性本能,也不是人际的交流关系,而是他们都相信着共同的虚构神话故事。

    \n
    \n

    人人生而平等,造物者赋予他们若干不可剥夺的权利,其中包括生命权、自由权和追求幸福的权利。

    \n
    \n

    演化的基础是差异,而不是平等。每个人身上带的基因码都有些许不同,而且从出生以后就接受着不同的环境影响,发展出不同的特质,导致不同的生存概率。“生而平等”其实该是“演化各有不同”。

    \n
    \n

    个体诞生的背后就只是盲目的演化过程,而没有任何目的。所以“造物者赋予”其实就只是“出生”。

    \n
    \n

    “自由”就像是“平等”、“权利”和“有限公司”,不过是人类发明的概念,也只存在于人类的想象之中。

    \n
    \n

    我们相信某种秩序,并非因为它是客观的现实,而是因为相信它可以让人提升合作效率、打造更美好的社会。

    \n
    \n

    伏尔泰就曾说:“世界上本来就没有神,但可别告诉我的仆人,免得他半夜偷偷把我宰了。”

    \n
    \n

    如果不是大多数中国人都相信仁义礼智信,儒家思想绝不可能持续了两千多年。如果不是大多数的美国总统和国会议员都相信人权,美国的民主也不可能持续了250年。如果不是广大的投资人和银行家都相信资本主义,现代经济体系连一天也不可能继续存在。

    \n
    \n

    而要怎样才能让人相信这些秩序?

    \n
      \n
    • 第一,对外的说法绝对要坚持它们千真万确
    • \n
    • 第二,在教育上也要彻底贯彻同一套原则。
    • \n
    \n
    \n

    有三大原因,让人类不会发现组织自己生活的种种秩序其实是想象:

    \n
      \n
    1. 想象建构的秩序深深与真实的世界结合。
    2. \n
    3. 想象建构的秩序塑造了我们的欲望。
    4. \n
    5. 想象建构的秩序存在于人和人之间思想的连接。
    6. \n
    \n
    \n

    旅游业真正卖的可不是机票和饭店房间,而是旅游中的经验。

    \n
    \n

    想象建构的秩序并非个人主观的想象,而是存在于主体之间(inter-subjective),存在于千千万万人共同的想象之中。

    \n
    \n

    “客观”、“主观”和“主体间”的不同:

    \n
      \n
    • “客观”事物的存在,不受人类意识及信念影响。
    • \n
    • “主观”事物的存在,靠的是某个单一个人的意识和信念
    • \n
    \n
    \n

    “主体间”事物的存在,靠的是许多个人主观意识之间的连接网络。

    \n
    \n

    为了改变现有由想象建构出的秩序,就得先用想象建构出另一套秩序才行。

    \n
    \n

    身为人类,我们不可能脱离想象所建构出的秩序。

    \n
    \n

    智人的社会秩序是通过想象建构,维持秩序所需的关键信息无法单纯靠DNA复制就传给后代,需要通过各种努力,才能维持种种法律、习俗、程序、礼仪,否则社会秩序很快就会崩溃。

    \n
    \n

    人类的大脑并不是个很好的储存设备,主要原因有三。

    \n
      \n
    • 第一,大脑的容量有限。
    • \n
    • 第二,人类总难免一死,而大脑也随之死亡。
    • \n
    • 第三,也是最重要的一点,在于人类的大脑经过演化,只习惯储存和处理特定类型的信息
    • \n
    \n
    \n

    演化压力让人类的大脑善于储存大量关于动植物、地形和社会的信息。

    \n
    \n

    然而在农业革命之后,社会开始变得格外复杂,另一种全新的信息类型也变得至关重要:数字。

    \n
    \n

    虽然这些符号现在被称为“阿拉伯数字”,但其实是印度人发明的。

    \n
    \n

    之所以现在我们会称“阿拉伯数字”,是因为阿拉伯人攻打印度时发现了这套实用的系统,再加以改良传到中东,进而传入欧洲。

    \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
    \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
    \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
    \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
    \n

    金钱制度有两大原则:

    \n
      \n
    1. 万物可换:钱就像是炼金术,可以让你把土地转为手下的忠诚,把正义转为健康,把暴力转为知识。
    2. \n
    3. 万众相信:有了金钱作为媒介,任何两个人都能合作各种计划。
    4. \n
    \n
    \n

    金钱还有更黑暗的一面。虽然金钱能建立起陌生人之间共通的信任,但人们信任的不是人类、社群或某些神圣的价值观,而只是金钱本身以及背后那套没有人性的系统。

    \n
    \n

    多数过去的文化,早晚都是遭到某些无情帝国军队的蹂躏,最后在历史上彻底遭到遗忘。

    \n
    \n

    在21世纪,几乎所有人的祖先都曾经属于某个帝国。

    \n
    \n

    帝国是一种政治秩序,有两项重要特征:

    \n
      \n
    • 第一,帝国必须统治着许多不同的民族,各自拥有不同的文化认同和独立的领土。
    • \n
    • 第二,帝国的特征是疆域可以灵活调整,而且可以几乎无限扩张。
    • \n
    \n
    \n

    这里要特别强调,帝国的定义就只在于文化多元性和疆界灵活性两项,至于起源、政府形式、领土范围或人口规模则并非重点。并不是一定要有军事征服才能有帝国。

    \n
    \n

    帝国正是造成民族多样性大幅减少的主因之一。

    \n
    \n

    帝国的标准配备,常常就包括战争、奴役、驱逐和种族屠杀。

    \n
    \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
    \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
    \n

    商业、帝国和全球性的宗教,最后终于将几乎每个智人都纳入了我们今天的全球世界。

    \n
    \n

    历史的铁则就是:事后看来无可避免的事,在当时看来总是毫不明显。

    \n
    \n

    混沌系统分成两级:

    \n
      \n
    • 一级混沌指的是“不会因为预测而改变”。
    • \n
    • 二级混沌系统,指的是“会受到预测的影响而改变”,因此就永远无法准确预测。
        \n
      • 例如市场就属于二级混沌系统。
      • \n
      • 历史是“二级”混沌系统
      • \n
      • 政治也属于二级混沌系统
      • \n
      \n
    • \n
    \n
    \n

    究竟为什么要学历史?历史不像是物理学或经济学,目的不在于做出准确预测。我们之所以研究历史,不是为了要知道未来,而是要拓展视野,要了解现在的种种绝非“自然”,也并非无可避免。未来的可能性远超过我们的想象。

    \n
    \n

    历史的选择绝不是为了人类的利益。

    \n
    \n

    迷因学假设,就像是生物演化是基于“基因”这种有机信息单位的复制,文化演化则是基于“迷因”(meme)这种文化信息单位的复制。

    \n
    \n

    模因,又译媒因、觅母、米姆、弥等。目前比较公认的定义是通过模仿在人与人之间传播的思想、行为或风格,通常是为了传达模因所代表的特定现象、主题或意义。

    \n
    \n
    \n

    现代科学与先前的知识体系有三大不同之处:

    \n
      \n
    1. 愿意承认自己的无知。
    2. \n
    3. 以观察和数学为中心。
    4. \n
    5. 取得新能力。
    6. \n
    \n
    \n

    科学革命并不是“知识的革命”,而是“无知的革命”。

    \n
    \n

    对“知识”的考验,不在于究竟是否真实,而在于是否能让人类得到力量或权力。

    \n
    \n

    许多的科学研究和科技发展,正是由军事所发起、资助及引导

    \n
    \n

    就目前所知,火药的发明其实是一场意外,原本的目的是道士想炼出长生不老药来。

    \n
    \n

    纵观历史,社会上有两种贫穷:

    \n
      \n
    1. 社会性的贫穷,指的是某些人掌握了机会,却不愿意释出给他人;
    2. \n
    3. 生物性的贫穷,指的是因为缺乏食物和住所,而使人的生存受到威胁。
    4. \n
    \n
    \n

    许多社会现在的问题是营养过剩,胖死比饿死的概率更高。

    \n
    \n

    人类所有看来无法解决的问题里,有一项最为令人烦恼、有趣且重要:死亡。

    \n
    \n

    当时最聪明的人才,想的是如何给死亡赋予意义,而不是逃避死亡。

    \n
    \n

    人之所以会死,可不是什么神的旨意,而是因为各种技术问题,像是心脏病,像是癌症,像是感染。而每个技术问题,都可以找到技术性的解决方案。

    \n
    \n

    现在所有最优秀的人才可不是浪费时间为死亡赋予意义,而是忙着研究各种与疾病及老化相关的生理、荷尔蒙和基因系统。

    \n
    \n

    科学革命的一大计划目标,就是要给予人类永恒的生命。

    \n
    \n

    唯一一个让死亡仍然占据核心的现代意识形态就是民族主义。在那些绝望到极点但又同时充满诗意的时刻,民族主义就会向人承诺,就算你牺牲了生命,但你会永远活在国家整体的永恒记忆里。只不过,这项承诺实在太虚无缥缈,恐怕大多数民族主义者也不知道这究竟说的是什么意思。

    \n
    \n

    科学活动并不是处于某个更高的道德和精神层面,而是也像其他的文化活动一样,受到经济、政治和宗教利益的影响。

    \n
    \n

    现代科学之所以能在过去500年间取得如同奇迹般的成果,有很大程度必须归功于政府、企业、基金会和私人捐助者愿意为此投入数十亿美元的经费。

    \n
    \n

    科学研究之所以能得到经费,多半是因为有人认为这些研究有助于达到某些政治、经济或宗教的目的。

    \n
    \n

    真正控制科学发展进度表的,也很少是科学家。

    \n
    \n

    科学并无力决定自己的优先级,也无法决定如何使用其发现。

    \n
    \n

    科学研究一定得和某些宗教或意识形态联手,才有蓬勃发展的可能。意识形态能够让研究所耗的成本合理化。

    \n
    \n

    在过去500年间,科学、帝国和资本之间的回馈循环无疑正是推动历史演进的主要引擎。

    \n
    \n

    虽然我们常常不愿意承认,但现在全球所有人的穿着、想法和品位几乎就都是欧洲人的穿着、想法和品位。

    \n
    \n

    中国和波斯其实并不缺乏制作蒸汽机的科技(当时要照抄或是购买都完全不成问题),他们缺少的是西方的价值观、故事、司法系统和社会政治结构,这些在西方花了数个世纪才形成及成熟,就算想要照抄,也无法在一夕之间内化。

    \n
    \n

    欧洲帝国主义之所以要前往遥远的彼岸,除了为了新领土,也是为了新知识。

    \n
    \n

    在15、16世纪,欧洲人的世界地图开始出现大片空白。从这点可以看出科学心态的发展,以及欧洲帝国主义的动机。

    \n
    \n

    误以为发现美洲的人是亚美利哥·韦斯普奇,因此为了向他致敬,这片大陆就被命名为“America”(美洲)。

    \n
    \n

    绝大多数的大帝国向外侵略只着眼于邻近地区,之所以最后幅员广大,只是因为帝国不断向邻近地区扩张而已。

    \n
    \n

    虽然偶尔会有某个雄心勃勃的统治者或冒险家,展开长途的征讨或探险,但通常都是顺着早已成形的帝国道路或商业路线。。

    \n
    \n

    郑和下西洋得以证明,当时欧洲并未占有科技上的优势。真正让欧洲人胜出的,是他们无与伦比而又贪得无厌、不断希望探索和征服的野心。

    \n
    \n

    所有的非欧洲政权中,第一个派出军事远征队前往美洲的是日本。

    \n
    \n

    现代科学和现代帝国背后的动力都是一种不满足,觉得在远方一定还有什么重要的事物,等着他们去探索、去掌握。

    \n
    \n

    科学能够从思想上让帝国合理化。

    \n
    \n

    正因为帝国与科学密切合作,就让它们有了如此强大的力量,能让整个世界大为改观;也是因为如此,我们很难简单断言它们究竟是善是恶。正是帝国创造了我们所认识的世界,而且,其中还包含我们用以判断世界的意识形态。

    \n
    \n

    最早的梵语母语民族是在大约3000年前、从中亚入侵印度,他们自称为“雅利亚”(Arya)。而最早的波斯语母语者则自称为“艾利亚”(Airiia)。

    \n
    \n

    对今日许多精英分子而言,要比较判断不同人群的优劣,几乎讲的总是历史上的文化差异,而不再是种族上的生物差异。

    \n
    \n

    不论是科学还是帝国,它们能够迅速崛起,背后都还潜藏着一股特别重要的力量:资本主义。

    \n
    \n

    真正让银行(以及整个经济)得以存活甚至大发利市的,其实是我们对未来的信任。“信任”就是世上绝大多数金钱的唯一后盾。

    \n
    \n

    人类发展出“信用”这种金钱概念,代表着目前还不存在、只存在于想象中的货品。

    \n
    \n

    现代经济的奇妙循环:

    \n
      \n
    • 正是这种信任创造了信贷;
    • \n
    • 而信贷带来了实实在在的经济成长;
    • \n
    • 正因为有成长,我们就更信任未来,也就愿意提供更多的信贷
    • \n
    \n
    \n

    民间企业的获利正是社会整体财富和繁荣的基础。

    \n
    \n

    所谓的“资本主义”(Capitalism),认为“资本”(capital)与“财富”(wealth)有所不同。

    \n
      \n
    • 资本指的是投入生产的各种金钱、物品和资源。
    • \n
    • 财富指的则是那些埋在地下或是浪费在非生产性活动的金钱、物品和资源。
    • \n
    \n
    \n

    资本主义的影响范围逐渐超越了单纯的经济领域,现在它还成了一套伦理,告诉我们该有怎样的行为,该如何教育孩子,甚至该如何思考问题。

    \n
    \n

    资本主义认为经济可以无穷无尽地发展下去,但这和我们日常生活观察到的宇宙现象完全背道而驰。

    \n
    \n

    印钞票的是银行和政府,但最后埋单的是科学家。

    \n
    \n

    欧洲人征服世界的过程中,所需资金来源从税收逐渐转为信贷,而且也逐渐改由资本家主导,一切的目标就是要让投资取得最高的报酬。

    \n
    \n

    为了掌控哈德孙河这个重要商业通道,西印度公司在河口的一座小岛上开拓了一个殖民地,名为“新阿姆斯特丹”(New Amsterdam)。这个殖民地不断遭受美国原住民威胁,英国人也多次入侵,最后在1664年落入英国手中。英国人将这个城市改名“纽约”(New York,即“新约克”,约克为英国郡名)。当时西印度公司曾在殖民地筑起一道墙,用来抵御英国人和美国原住民,这道墙的位置现在成了世界上最著名的街道:华尔街(Wall Street,直译为“墙街”)。

    \n
    \n

    在1717年,密西西比河下游河谷其实大约只有沼泽和鳄鱼,但密西西比公司却是撒着漫天大谎,把这个地方描述得金银遍地、无限商机。许多法国贵族、商人和城市里那些冷漠的中产阶级都信了这套谎言,于是密西西比公司股价一飞冲天。公司上市的股价是每股500里弗(livre)。1719年8月1日,股价涨到每股2750里弗。8月30日,股价已经飙升到每股4100里弗;9月4日升上每股5000里弗。等到12月2日,密西西比公司的股价每股超过10000里弗大关。当时,整个巴黎街头洋溢着一种幸福感。民众卖掉了自己所有的财产,借了大笔的金钱,只为了能够购买密西西比公司的股票。每个人都相信自己找到了快速致富的捷径。

    \n

    密西西比泡沫可以说是史上最惨烈的一次金融崩溃。法国王室的金融体系一直没能真正走出这场重大的打击。

    \n
    \n

    至于打下印度次大陆的,同样也不是英国官方,而是英国东印度公司的佣兵。这家公司的成就甚至比荷兰东印度公司更加辉煌

    \n

    一直要到1858年,英国王室才将印度及英国东印度公司的军队收编国有

    \n
    \n

    这世界上根本不可能有完全不受政治影响的市场。毕竟,经济最重要的资源就是“信任”,而信任这种东西总是得面对种种的坑蒙拐骗。光靠着市场本身,并无法避免诈欺、窃盗和暴力的行为。这些事得由政治系统下手,立法禁止欺诈,并用警察、法庭和监狱来执行法律。

    \n
    \n

    论听来十分完美,但实际上却是漏洞百出。如果真的是完全自由的市场,没有国王或神职人员来监督,贪婪的资本家就能够通过垄断或串通来打击劳工

    \n
    \n

    如果真的是完全自由的市场,没有国王或神职人员来监督,贪婪的资本家就能够通过垄断或串通来打击劳工。

    \n

    这是自由市场资本主义美中不足之处。它无法保证利润会以公平的方式取得或是以公平的方式分配。

    \n
    \n

    人类的历史从来不是洁白无邪,随着现代经济成长,全球各地还有无数的大小罪恶和灾难正在上演。

    \n

    就像农业革命一样,所谓的现代经济成长也可能只是个巨大的骗局。虽然人类和全球经济看来都在继续成长,但更多的人却活在饥饿和困乏之中。

    \n
    \n

    每次即将因为能源或原料短缺而使经济成长趋缓的时候,就会有资金投入科学研究,解决这项问题。这种做法屡屡奏效,有时候让人更有效利用现有资源,有时候找出了全新的能源和材料。

    \n
    \n

    过去可能会有人认为,像这样大规模使用资源,很快就会耗尽所有能源和原料,很快只能靠着回收垃圾撑下去了。然而,实际状况却正好相反。在1700年,全球运输工具使用的原料多半是木材和铁,但今天我们却有各式各样的新材料任君挑选,像是塑料、橡胶、铝和钛,这一切我们的祖先都完全一无所知。另外,1700年的马车主要是由木匠和铁匠手工人力制作,但在现在的丰田车厂和波音公司工厂里,我们靠的是燃油引擎和核电厂来推动生产。类似的革命在几乎所有产业领域无处不在。我们将它称为“工业革命”

    \n
    \n

    人类历史在过去一直是由两大周期来主导:植物的生长周期,以及太阳能的变化周期。

    \n
    \n

    工业革命的核心,其实就是能源转换的革命。

    \n
    \n

    我们能使用的能源其实无穷无尽。讲得更精确,唯一的限制只在于我们的无知。

    \n
    \n

    在地心引力下将一颗小苹果抬升一米,所需的能量就是一焦耳;

    \n
    \n

    工业革命最重要的一点,其实在于它就是第二次的农业革命。

    \n
    \n

    正是因为农业释放出了数十亿的人力,由工厂和办公室吸纳,才开始像雪崩一样有各种新产品倾泻而出。

    \n
    \n

    消费主义的美德就是消费更多的产品和服务,鼓励所有人应该善待自己、宠爱自己,就算因为过度消费而慢慢走上绝路,也是在所不惜。

    \n
    \n

    购物已成为人类最喜爱的消遣,而且消费性产品也成了家人、朋友、配偶之间不可或缺的中介。各种宗教节日(例如圣诞节)都已经成了购物节。

    \n
    \n

    肥胖这件事,可以说是消费主义的双重胜利。

    \n
      \n
    • 一方面,如果大家吃得太少,就会导致经济萎缩,这可不妙;
    • \n
    • 另一方面,大家吃多了之后,就得购买减肥产品,再次促进经济成长。
    • \n
    \n
    \n

    资本主义和消费主义的伦理可以说是一枚硬币的正反两面,将这两种秩序合而为一。

    \n
      \n
    • 有钱人的最高指导原则是——“投资!”
    • \n
    • 而我们这些其他人的最高指导原则则是——“购买!”
    • \n
    \n
    \n

    人类能用的资源其实不断增加,而且这个趋势很可能还会继续。

    \n
    \n

    与中世纪农民和鞋匠相比,现代工业对太阳或季节可说是完全不在乎,更重视的是要追求精确和一致。

    \n
    \n

    1847年,英国各家火车业者齐聚一堂,研拟同意统一协调所有火车时刻表,一概以格林尼治天文台的时间为准,而不再遵循利物浦、曼彻斯特、格拉斯哥或任何其他城市的当地时间。在火车业者开了头之后,越来越多机构跟进这股风潮。最后在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

    对快乐与否的影响,家庭和社群要比金钱和健康来得重要。

    \n
    \n

    多项重复研究发现,婚姻美好与感觉快乐,以及婚姻不协调与感觉痛苦,分别都呈现高度相关。

    \n
    \n

    快乐并不在于任何像是财富、健康甚至社群之类的客观条件,而在于客观条件和主观期望之间是否相符。

    \n
    \n

    重要的是要知足,而不是一直想要得到更多。

    \n
    \n

    在我们试着猜测或想象其他人有多快乐的时候(可能是现在或过去的人),我们总是想要设身处地去想想自己在那个情况下会如何感受。但这么一来,我们是把自己的期望放到了别人的物质条件上,结果当然就会失准。

    \n
    \n

    如果说快乐要由期望来决定,那么我们社会的两大支柱(大众媒体和广告业)很有可能正在不知不觉地让全球越来越不开心。

    \n
    \n

    有没有可能,第三世界国家之所以会对生活不满,不只是因为贫穷、疾病、腐败和政治压迫,也是因为他们看到了第一世界国家的生活标准?

    \n
    \n

    纵观历史,穷人和受压迫者之所以还能自我安慰,就是因为死亡是唯一完全公平的事。

    \n
    \n

    人类演化的结果,就是不会太快乐,也不会太痛苦。我们会短暂感受到快感,但不会永远持续。迟早快感会消退,让我们再次感受到痛苦。

    \n
    \n

    演化就把快感当成奖赏,鼓励男性和女性发生性行为、将自己的基因传下去。如果性交没有高潮,大概很多男性就不会那么热衷。但同时,演化也确保高潮得迅速退去。如果性高潮永续不退,可以想象男性会非常开心,但连觅食的动力都没了,最后死于饥饿,而且也不会有兴趣再去找下一位能够繁衍后代的女性。

    \n
    \n

    人类的生化机制就像是个恒温空调系统。

    \n
    \n

    已婚的人比单身和离婚的人更快乐,但这不一定代表是婚姻带来了快乐,也有可能是快乐带来了婚姻。

    \n
    \n

    那些生化机制天生开朗的人,一般来说都会是快乐和满足的。而这样的人会是比较理想的另一半,所以他们结婚的概率也比较高。

    \n
    \n

    快乐不只是“愉快的时刻多于痛苦的时刻”这么简单。相反,快乐要看的是某人生命的整体;生命整体有意义、有价值,就能得到快乐。

    \n
    \n

    只要有了活下去的理由,几乎什么都能够忍受。

    \n
    \n

    从我们所知的纯粹科学角度来看,人类的生命本来就完全没有意义。人类只是在没有特定目标的演化过程中,盲目产生的结果。

    \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":"解决由于 AWDL 导致 Mac 的断网问题","url":"/2023/AWDL-Mac-disconnected/","content":"

    我的电脑在公司使用无线网络时经常性断网,为了有稳定的网络我在工位时经常接根网线,使用网线连接。之前公司运维给了个叫 WiFriedX 的工具来解决这个问题,最近发现问题又出现了,开会时断网非常耽误事,所以就又着手开始排查。

    \n

    最后定位到是苹果搞的 AWDL 引起的,AWDL 全称:Apple Wireless Direct Link 苹果无线直连,用于 AirDrop、AirPlay 和其他服务的低延迟高速率 WIFI 点对点传输功能。苹果为它提供了独立的网络接口,可以通过 ifconfig awdl0 看到其状态。

    \n

    \n

    苹果的操作内核为1个 WiFi Broadcom 硬件芯片提供了多个 WiFi 接口:

    \n
      \n
    • en0:主要 WiFi 接口
    • \n
    • ap1:用于 WiFi 网络共享的接入点接口
    • \n
    • awdl0:苹果无线直接链接接口
    • \n
    \n

    通过拥有多个接口,我们的电脑就能够在 en0 上建立标准 WiFi 连接,同时在 awdl0 上广播、浏览和解析点对点连接。

    \n

    这导致的问题是信号不稳定,只要 AWDL 处于活动状态,它就会持续在后台探测附近的其他设备,在使用时会短暂干扰 WiFi 运作,在目前无线网络连接和 AWDL 频道直接来回切换。猜测在公司时问题更严重是因为公司的无线AP 比较多,导致的干扰也就更强。

    \n

    在网上查找解决方案的时候发现 Apple 芯片的 Mac 更容易出这个问题,比如 M1、M2。

    \n
    \n

    我前边提到的工具WiFriedX实际上就是通过关闭 AWDL 来解决网络不稳定的问题,但我发现它关闭的并不是那么彻底,关闭一段时间后,又在后台被其他进程开启。

    \n

    我通过手动的方式关闭 awdl0 网卡:

    \n
    sudo ifconfig awdl0 down
    \n

    在刚执行完后查询状态时,确实改为了 inactive,过了一会发现又变回了 active。查资料说的是如果本地启动了 AirDrop,AWDL 将立即重新启用;Bonjour discovery 还将每隔几分钟重新启用一次 AWDL。

    \n

    \n
    \n

    感谢开源社区,已经有其他人发现了这个 AWDL 的坑,并且也想长期关闭它,于是写了脚本来在后台持续监听这块网卡的状态并将其关闭。

    \n

    核心代码如下:

    \n
    #!/usr/bin/env bash

    set -euo pipefail

    while true; do
    if ifconfig awdl0 |grep -q \"<UP\"; then
    (set -x; ifconfig awdl0 down)
    fi

    sleep 1
    done
    \n

    这段逻辑会每秒钟检测一次 awdl0 网卡状态,如果是开启就进行关闭。

    \n

    运行这段代码可以达到永久关闭 awdl0 网卡的效果,但是如果是我们每次手动运行它会比较麻烦,每次重启电脑后还要记得再次运行。于是大神们继续封装,将这个代码在系统后代常驻运行,重启时也会自动启动。

    \n

    永久关闭 AWDL

    通过下边这个命令,可以把上边的脚本放在后台服务中一直执行,同时跟随系统启动:

    \n
    curl -sL https://raw.githubusercontent.com/meterup/awdl_wifi_scripts/main/awdl-daemon.sh | bash
    \n

    恢复 AWDL

    关闭后会影响 AirDrop 功能,如果想用手机给电脑投个文件或者照片之类的就很不方便。

    \n

    如果要恢复 AWDL 可以使用下边的命令:

    \n
    curl -s https://raw.githubusercontent.com/meterup/awdl_wifi_scripts/main/cleanup-and-reenable-awdl.sh | bash &> /dev/null
    \n

    快捷键

    在 shell 的 rc 文件中配置两个 alias,就可以实现快捷键一键开启和关闭 AWDL 功能了:

    \n
    alias awdldown='curl -sL https://raw.githubusercontent.com/meterup/awdl_wifi_scripts/main/awdl-daemon.sh | bash'
    alias awdlup='curl -s https://raw.githubusercontent.com/meterup/awdl_wifi_scripts/main/cleanup-and-reenable-awdl.sh | bash &> /dev/null'
    \n"},{"title":"AWS 搭建SS服务器","url":"/2016/AWS-%E6%90%AD%E5%BB%BASS%E6%9C%8D%E5%8A%A1%E5%99%A8/","content":"

    今天看到亚马逊云服务的广告,说现在开通AWS可以免费用一年。然后我爱占小便宜心又犯了,所以绑定信用卡,开通了AWS,在这过程中,不知道为什么扣了我两笔6.59的钱,想联系客服也找不到人。。

    \n

    进入AWS主页后看到有很多服务可以用,实在眼花缭乱,先一个一个来,我猜测EC2的意思就和阿里云主机是一个意思,所以就开通了一个,选择的是新加坡节点,果然开通时提示我可以免费用一年。既然这样的话,不如拿来搭一个ss服务器吧,哈哈哈~(因为其他暂时没想到做什么用,也许以后我会在这上边部署一个爬虫之类的)

    \n

    在此过程中,让我下载了一个pem格式的私钥文件,用来登录。

    \n

    登录命令:ssh -i "aws-for-panmax.pem" ubuntu@ec2-54-169-92-35.ap-southeast-1.compute.amazonaws.com 进来之后,我使用sudo adduser panmax创建了新的账户。

    \n

    我ping了一下twitter,延迟200+,有些略失望~

    \n\n

    更新和安装需要用到的包:

    \n

    sudo apt-get update

    \n

    sudo apt-get install nginx

    \n

    sudo apt-get install mysql-client-5.5 mysql-server-5.5

    \n

    sudo apt-get install php5 php5-fpm php5-cli php5-cgi php5-mysql php5-gd

    \n

    以上这些如果没有报错,就证明安装成功了。安装mysql过程中需要创建root密码。

    \n

    接下来创建数据库:

    \n

    mysql -u root -p 输入安装mysql时设置的root密码。

    \n

    创建shadowsocks数据库:

    \n

    create database shadowsocks

    \n

    然后建立一个名为ss,密码为ss的MySQL用户,因为这个用户只能本地登录,所以密码简单点也无所谓:

    \n

    grant all privileges on shadowsocks.* to ss@localhost identified by 'ss';

    \n

    到这步,我们的数据库已经完成了,,下面我们来安装shadowsocks ss-panel supervisor,一次执行下面的命令:

    \n

    sudo apt-get install python-pip git python-m2crypto

    \n

    sudo pip install cymysql

    \n

    git clone -b manyuser https://github.com/mengskysama/shadowsocks.git

    \n

    cd shadowsocks/shadowsocks/

    \n

    然后我们来修改配置文件/root/shadowsocks/shadowsocks/Config.py

    \n
    #Config
    MYSQL_HOST = 'localhost'
    MYSQL_PORT = 3306
    MYSQL_USER = 'ss'
    MYSQL_PASS = 'ss'
    MYSQL_DB = 'shadowsocks'
    MANAGE_PASS = 'ss233333333'
    #if you want manage in other server you should set this value to global ip
    MANAGE_BIND_IP = '127.0.0.1'
    #make sure this port is idle
    MANAGE_PORT = 23333
    \n

    然后我们还要修改这个文件/root/shadowsocks/shadowsocks/config.json

    \n
    {
    "server":"0.0.0.0",
    "server_ipv6": "[::]",
    "server_port":8388,
    "local_address": "127.0.0.1",
    "local_port":1080,
    "password":"m",
    "timeout":300,
    "method":"aes-256-cfb"
    }
    \n

    然后我们来导入数据库。进入MySQL:

    \n

    mysql -u root -p

    \n

    use shadowsocks;

    \n

    source ~/shadowsocks/shadowsocks/shadowsocks.sql;

    \n

    exit

    \n

    导入数据库之后,我们在shadowsocks目录下运行一下server.py,python server.py

    \n

    没有error的话,ctrl + c结束进程,我们进行下一步,安装守护进程,这样重启以后或者程序崩了还能自己重启。

    \n

    sudo apt-get install python-pip python-m2crypto supervisor

    \n

    然后我们需要新建两个文件,具体如下:

    \n

    sudo vim /etc/supervisor/conf.d/shadowsocks.conf

    \n

    内容:

    \n
    [program:shadowsocks]
    command=python /home/panmax/shadowsocks/shadowsocks/server.py -c /home/panmax/shadowsocks/shadowsocks/config.json
    autorestart=true
    user=root
    \n

    再创建一个文件:

    \n

    sudo vim /etc/supervisor/conf.d/cgi.conf

    \n

    内容:

    \n
    [program:cgi]
    command=php5-cgi -b localhost:9000
    autorestart=true
    user=root
    \n

    然后命令:

    \n

    cd shadowsocks/shadowsocks

    \n

    service supervisor start

    \n

    supervisorctl reload

    \n

    在以下两个文件/etc/profile和 /etc/default/supervisor结尾添加如下代码(/etc/default/supervisor不存在,直接sudo vi /etc/default/supervisor 即可):

    \n
    ulimit -n 51200  
    ulimit -Sn 4096
    ulimit -Hn 8192
    \n

    至此ss的后端服务已经搞定了,现在我们来整前端界面:

    \n

    cd /usr/share/nginx/

    \n

    wget -b v2 https://github.com/orvice/ss-panel/archive/master.zip

    \n

    安装解压软件:

    \n

    sudo apt-get install unzip

    \n

    解压文件:

    \n

    sudo unzip master.zip

    \n

    然后重命名文件夹,

    \n

    mv ss-panel-master ss

    \n

    现在来修改文件夹权限,

    \n

    cd /usr/share/nginx/

    \n

    sudo chmod 777 * -R /usr/share/nginx/html

    \n

    sudo chmod 777 * -R /usr/share/nginx/ss

    \n

    sudo chown -R www-data:www-data /usr/share/nginx/html

    \n

    sudo chown -R www-data:www-data /usr/share/nginx/ss

    \n

    然后我们需要将ss-pane中的数据库导入我们刚刚创建的数据库中,还是进入MySQL:

    \n

    mysql -u root -p

    \n

    use shadowsocks;

    \n

    source /usr/share/nginx/ss/sql/invite_code.sql;

    \n

    然后我们需要将ss-pane中的数据库导入我们刚刚创建的数据库中,查看/usr/share/nginx/ss/sql下的内容,把里边的文件导入:

    \n

    例如:

    \n
    use shadowsocks;
    source /usr/share/nginx/ss/sql/invite_code.sql;
    ...
    \n

    然后我们来修改配置文件

    \n
    cd /usr/share/nginx 
    mv /usr/share/nginx/ss/lib/config-simple.php /usr/share/nginx/ss/lib/config.php
    \n

    修改congfig.php里边的数据库相关配置信息

    \n

    到此,ss-panel前端界面也安装完毕,然后我们需要修改一下Nginx配置文件

    \n
    cd /etc/nginx/sites-available/
    sudo vim default
    \n

    修改为

    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;
    }
    }

    \n

    然后重启一下

    \n
    root@ubuntu-512mb-sfo1-01:~# shutdown -r now
    \n

    完成。

    \n

    /admin 进入管理员界面。

    \n

    默认帐号:first@blood.com

    \n

    默认密码:1993

    \n

    绑定域名:

    \n

    新增CNAME,主机记录ss,记录值为AWS EC2的公有 DNS

    \n","categories":["折腾"],"tags":["AWS","EC2","亚马逊云服务","Linux"]},{"title":"Airbnb React 编码规范","url":"/2016/Airbnb-React-%E7%BC%96%E7%A0%81%E8%A7%84%E8%8C%83/","content":"

    英文原文地址: Airbnb React/JSX Style Guide

    \n

    Airbnb React/JSX Style Guide

    \n

    用更合理的方式书写 React 和 JSX

    \n
    \n

    基本规则

      \n
    • 一个文件内只包含一个 React 组件。

      \n\n
    • \n
    • 总是使用 JSX 语法。

      \n
    • \n
    • 不要使用 React.createElement,除非你从一个不是 JSX 的文件初始化你的应用。
    • \n
    \n

    Class vs React.createClass vs stateless

    \n
    // bad
    const Listing = React.createClass({
    // ...
    render() {
    return <div>{this.state.hello}</div>;
    }
    });

    // good
    class Listing extends React.Component {
    // ...
    render() {
    return <div>{this.state.hello}</div>;
    }
    }
    \n
      \n
    • 如果没有内部 state 或者 refs,那么普通函数 (非箭头函数) 比类的写法更好:
    • \n
    \n
    // bad
    class Listing extends React.Component {
    render() {
    return <div>{this.props.hello}</div>;
    }
    }

    // bad (since arrow functions do not have a "name" property)
    const Listing = ({ hello }) => (
    <div>{hello}</div>
    );

    // good
    function Listing({ hello }) {
    return <div>{hello}</div>;
    }
    \n

    命名

      \n
    • 扩展名:React 组件使用.jsx扩展名
    • \n
    • 文件名:文件名使用帕斯卡命名。 例如: ReservationCard.jsx
    • \n
    • 引用命名:React 组件使用帕斯卡命名,引用实例采用骆驼命名。 eslint: react/jsx-pascal-case
    • \n
    \n
    // bad
    import reservationCard from './ReservationCard';

    // good
    import ReservationCard from './ReservationCard';

    // bad
    const ReservationItem = <ReservationCard />;

    // good
    const reservationItem = <ReservationCard />;
    \n

    命名

      \n
    • 扩展: 使用 .jsx React 组件的扩展名。
    • \n
    • 文件名: 为文件使用帕斯卡命名方式(PascalCase)。 例如: ReservationCard.jsx
    • \n
    • 引用命名:为 React组件 使用帕斯卡命名方式(PascalCase),为他们的实例使用驼峰方式命名(camelCase)。eslint: react/jsx-pascal-case
    • \n
    \n
    // bad
    import reservationCard from './ReservationCard';

    // good
    import ReservationCard from './ReservationCard';

    // bad
    const ReservationItem = <ReservationCard />;

    // good
    const reservationItem = <ReservationCard />;
    \n
      \n
    • 组件命名:组件名称应该和文件名一致。例如: ReservationCard.jsx 应该有一个 ReservationCard 的引用名称。 然而,如果是在目录中的组件, 应该使用 index.jsx 作为文件名并且使用目录名称作为组件名:
    • \n
    \n
    // bad
    import Footer from './Footer/Footer';

    // bad
    import Footer from './Footer/index';

    // good
    import Footer from './Footer';
    \n

    声明

      \n
    • 不要使用 displayName 属性来命名组件,应该使用类的引用名称。
    • \n
    \n
    // bad
    export default React.createClass({
    displayName: 'ReservationCard',
    // stuff goes here
    });

    // good
    export default class ReservationCard extends React.Component {
    }
    \n

    对齐

    \n
    // bad
    <Foo superLongParam="bar"
    anotherSuperLongParam="baz" />

    // good
    <Foo
    superLongParam="bar"
    anotherSuperLongParam="baz"
    />

    // if props fit in one line then keep it on the same line
    <Foo bar="bar" />

    // children get indented normally
    <Foo
    superLongParam="bar"
    anotherSuperLongParam="baz"
    >
    <Quux />
    </Foo>
    \n

    引号

      \n
    • JSX 的属性都采用双引号("),其他的 JS 都使用单引号。eslint: jsx-quotes
    • \n
    \n
    \n

    为什么这样做?JSX 属性 不能包含转义的引号, 所以当输入 "don't" 这类的缩写的时候用双引号会更方便。标准的 HTML 属性通常也会使用双引号替代单引号,所以 JSX 属性也会遵守这样的约定。

    \n
    \n
    // bad
    <Foo bar='bar' />

    // good
    <Foo bar="bar" />

    // bad
    <Foo style={{ left: "20px" }} />

    // good
    <Foo style={{ left: '20px' }} />
    \n

    空格

      \n
    • 总是在你的自闭标签内包含一个空格。
    • \n
    \n
    // bad
    <Foo/>

    // very bad
    <Foo />

    // bad
    <Foo
    />

    // good
    <Foo />
    \n

    属性

      \n
    • 总是为你的属性名使用驼峰命名(camelCase)。
    • \n
    \n
    // bad
    <Foo
    UserName="hello"
    phone_number={12345678}
    />

    // good
    <Foo
    userName="hello"
    phoneNumber={12345678}
    />
    \n\n
    // bad
    <Foo
    hidden={true}
    />

    // good
    <Foo
    hidden
    />
    \n

    大括号

    \n
    // bad
    render() {
    return <MyComponent className="long body" foo="bar">
    <MyChild />
    </MyComponent>;
    }

    // good
    render() {
    return (
    <MyComponent className="long body" foo="bar">
    <MyChild />
    </MyComponent>
    );
    }

    // good, when single line
    render() {
    const body = <div>hello</div>;
    return <MyComponent>{body}</MyComponent>;
    }
    \n

    标签

    \n
    // bad
    <Foo className="stuff"></Foo>

    // good
    <Foo className="stuff" />
    \n\n
    // bad
    <Foo
    bar="bar"
    baz="baz" />

    // good
    <Foo
    bar="bar"
    baz="baz"
    />
    \n

    方法

      \n
    • 使用箭头函数关闭本地变量。
    • \n
    \n
    function ItemList(props) {
    return (
    <ul>
    {props.items.map((item, index) => (
    <Item
    key={item.key}
    onClick={() => doSomethingWith(item.name, index)}
    />
    ))}
    </ul>
    );
    }
    \n
      \n
    • 为 render 方法的处理事件在构造函数中进行绑定。 eslint: react/jsx-no-bind
    • \n
    \n
    \n

    为什么这样做? 在 render 方法中的 bind 调用每次调用 render 的时候都会创建一个全新的函数。

    \n
    \n
    // bad
    class extends React.Component {
    onClickDiv() {
    // do stuff
    }

    render() {
    return <div onClick={this.onClickDiv.bind(this)} />
    }
    }

    // good
    class extends React.Component {
    constructor(props) {
    super(props);

    this.onClickDiv = this.onClickDiv.bind(this);
    }

    onClickDiv() {
    // do stuff
    }

    render() {
    return <div onClick={this.onClickDiv} />
    }
    }
    \n
      \n
    • 不要使用下划线前缀为 React 组件的内部方法命名。
    • \n
    \n
    // bad
    React.createClass({
    _onClickSubmit() {
    // do stuff
    },

    // other stuff
    });

    // good
    class extends React.Component {
    onClickSubmit() {
    // do stuff
    }

    // other stuff
    }
    \n

    排序

      \n
    • class extends React.Component 的顺序:
    • \n
    \n
      \n
    1. 可选的 static 方法
    2. \n
    3. constructor
    4. \n
    5. getChildContext
    6. \n
    7. componentWillMount
    8. \n
    9. componentDidMount
    10. \n
    11. componentWillReceiveProps
    12. \n
    13. shouldComponentUpdate
    14. \n
    15. componentWillUpdate
    16. \n
    17. componentDidUpdate
    18. \n
    19. componentWillUnmount
    20. \n
    21. 点击回调或者事件回调 比如 onClickSubmit() 或者 onChangeDescription()
    22. \n
    23. render 函数中的 getter 方法 比如 getSelectReason() 或者 getFooterContent()
    24. \n
    25. 可选的 render 方法 比如 renderNavigation() 或者 renderProfilePicture()
    26. \n
    27. render
    28. \n
    \n
      \n
    • 怎样定义 propTypes, defaultProps, contextTypes等……
    • \n
    \n
    import React, { PropTypes } from 'react';

    const propTypes = {
    id: PropTypes.number.isRequired,
    url: PropTypes.string.isRequired,
    text: PropTypes.string,
    };

    const defaultProps = {
    text: 'Hello World',
    };

    class Link extends React.Component {
    static methodsAreOk() {
    return true;
    }

    render() {
    return <a href={this.props.url} data-id={this.props.id}>{this.props.text}</a>
    }
    }

    Link.propTypes = propTypes;
    Link.defaultProps = defaultProps;

    export default Link;
    \n

    React.createClass的排序:eslint: react/sort-comp

    \n
      \n
    1. displayName
    2. \n
    3. propTypes
    4. \n
    5. contextTypes
    6. \n
    7. childContextTypes
    8. \n
    9. mixins
    10. \n
    11. statics
    12. \n
    13. defaultProps
    14. \n
    15. getDefaultProps
    16. \n
    17. getInitialState
    18. \n
    19. getChildContext
    20. \n
    21. componentWillMount
    22. \n
    23. componentDidMount
    24. \n
    25. componentWillReceiveProps
    26. \n
    27. shouldComponentUpdate
    28. \n
    29. componentWillUpdate
    30. \n
    31. componentDidUpdate
    32. \n
    33. componentWillUnmount
    34. \n
    35. 点击回调或者事件回调 比如 onClickSubmit() or onChangeDescription()
    36. \n
    37. getter methods for render like getSelectReason() or getFooterContent()
    38. \n
    39. Optional render methods like renderNavigation()or renderProfilePicture()
    40. \n
    41. render
    42. \n
    \n

    isMounted

    \n
    \n

    为什么? isMounted是一种反模式,当使用 ES6 类风格声明 React 组件时该属性不可用,并且即将被官方弃用。

    \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":"

    原文地址:http://keeganlee.me/post/architecture/20160107

    \n

    App与服务器的通信接口如何设计得好,需要考虑的地方挺多的,在此根据我的一些经验做一些总结分享,旨在抛砖引玉。

    \n

    安全机制的设计

    现在,大部分App的接口都采用RESTful架构,RESTFul最重要的一个设计原则就是,客户端与服务器的交互在请求之间是无状态的,也就是说,当涉及到用户状态时,每次请求都要带上身份验证信息。实现上,大部分都采用token的认证方式,一般流程是:

    \n
      \n
    1. 用户用密码登录成功后,服务器返回token给客户端;
    2. \n
    3. 客户端将token保存在本地,发起后续的相关请求时,将token发回给服务器;
    4. \n
    5. 服务器检查token的有效性,有效则返回数据,若无效,分两种情况:
        \n
      • token错误,这时需要用户重新登录,获取正确的token
      • \n
      • token过期,这时客户端需要再发起一次认证请求,获取新的token
      • \n
      \n
    6. \n
    \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
      \n
    1. 不需要注册,不需要修改密码,也不需要因为忘记密码而重置密码的操作了;
    2. \n
    3. 用户不再需要记住密码了,也不怕密码泄露的问题了;
    4. \n
    5. 相对于密码登录其安全性明显提高了。
    6. \n
    \n

    接口数据的设计

    接口的数据一般都采用JSON格式进行传输,不过,需要注意的是,JSON的值只有六种数据类型:

    \n
      \n
    • Number:整数或浮点数
    • \n
    • String:字符串
    • \n
    • Boolean:true 或 false
    • \n
    • Array:数组包含在方括号[]中
    • \n
    • Object:对象包含在大括号{}中
    • \n
    • Null:空类型
    • \n
    \n

    所以,传输的数据类型不能超过这六种数据类型。以前,我们曾经试过传输Date类型,它会转为类似于”2016年1月7日 09时17分42秒 GMT+08:00”这样的字符串,这在转换时会产生问题,不同的解析库解析方式可能不同,有的可能会转乱,有的可能直接异常了。要避免出错,必须做特殊处理,自己手动去做解析。为了根除这种问题,最好的解决方案是用毫秒数表示日期。

    \n

    另外,以前的项目中还出现过字符串的”true”和”false”,或者字符串的数字,甚至还出现过字符串的”null”,导致解析错误,尤其是”null”,导致App奔溃,后来查了好久才查出来是该问题导致的。这都是因为服务端对数据没处理好,导致有些数据转为了字符串。所以,在客户端,也不能完全信任服务端传回的数据都是对的,需要对所有异常情况都做相应处理。

    \n

    服务器返回的数据结构,一般为:

    \n
    {
    code:0
    message: "success"
    data: { key1: value1, key2: value2, ... }
    }
    \n
      \n
    • code: 状态码,0表示成功,非0表示各种不同的错误
    • \n
    • message: 描述信息,成功时为”success”,错误时则是错误信息
    • \n
    • data: 成功时返回的数据,类型为对象或数组
    • \n
    \n

    不同错误需要定义不同的状态码,属于客户端的错误和服务端的错误也要区分,比如1XX表示客户端的错误,2XX表示服务端的错误。这里举几个例子:

    \n
      \n
    • 0:成功
    • \n
    • 100:请求错误
    • \n
    • 101:缺少appKey
    • \n
    • 102:缺少签名
    • \n
    • 103:缺少参数
    • \n
    • 200:服务器出错
    • \n
    • 201:服务不可用
    • \n
    • 202:服务器正在重启
    • \n
    \n

    错误信息一般有两种用途:一是客户端开发人员调试时看具体是什么错误;二是作为App错误提示直接展示给用户看。主要还是作为App错误提示,直接展示给用户看的。所以,大部分都是简短的提示信息。

    \n

    data字段只在请求成功时才会有数据返回的。数据类型限定为对象或数组,当请求需要的数据为单个对象时则传回对象,当请求需要的数据是列表时,则为某个对象的数组。这里需要注意的就是,不要将data传入字符串或数字,即使请求需要的数据只有一个,比如token,那返回的data应该为:

    \n
    // 正确
    data: { token: 123456 }

    // 错误
    data: 123456
    \n

    接口版本的设计

    接口不可能一成不变,在不停迭代中,总会发生变化。接口的变化一般会有几种:

    \n
      \n
    • 数据的变化,比如增加了旧版本不支持的数据类型
    • \n
    • 参数的变化,比如新增了参数
    • \n
    • 接口的废弃,不再使用该接口了
    • \n
    \n

    为了适应这些变化,必须得做接口版本的设计。实现上,一般有两种做法:

    \n
      \n
    1. 每个接口有各自的版本,一般为接口添加个version的参数。
    2. \n
    3. 整个接口系统有统一的版本,一般在URL中添加版本号,比如http://api.domain.com/v2。
    4. \n
    \n

    大部分情况下会采用第一种方式,当某一个接口有变动时,在这个接口上叠加版本号,并兼容旧版本。App的新版本开发传参时则将传入新版本的version。

    \n

    如果整个接口系统的根基都发生变动的话,比如微博API,从OAuth1.0升级到OAuth2.0,整个API都进行了升级。

    \n

    有时候,一个接口的变动还会影响到其他接口,但做的时候不一定能发现。因此,最好还要有一套完善的测试机制保证每次接口变更都能测试到所有相关层面。

    \n","categories":["转载"],"tags":["接口"]},{"title":"后端开发学习路径","url":"/2020/Backend-Developer-RoadMap/","content":"

    \"\"

    \n
    互联网
    \t互联网如何工作?
    \t什么是 HTTP?
    \t浏览器如何工作?
    \tDNS 如何工作?
    \t什么是域名?
    \t什么是主机?

    前端基础知识
    \tHTML
    \tCSS
    \tJavaScript

    操作系统和通用技能
    \t终端的使用
    \t操作系统工作原理
    \t进程管理
    \t内存管理
    \t进程间通信
    \tI/O 管理
    \tPOSIX 基础
    \t\tstdin
    \t\tstdout
    \t\tstderr
    \t\tpipes
    \t基础网络知识
    \t线程和并发
    \t基本命令
    \t\tgrep
    \t\tawk
    \t\tlsof
    \t\tcurl
    \t\twget
    \t\ttail
    \t\thead
    \t\tless
    \t\tfind
    \t\tssh
    \t\tkill

    开发语言
    \tJava
    \tPython
    \tGo
    \tJavaScritp
    \tRuby
    \tRust
    \tC#
    \tPHP(虽然是最好的语言)

    版本管理
    \tGit 的基本用法
    \t仓库托管服务
    \t\tGitHub
    \t\tGitLab
    \t\tBitbucket

    关系型数据库
    \tPostgreSQL
    \tMySQL
    \tMariaDB
    \tMS SQL
    \tOracle

    NoSQL Database
    \tHBase
    \tMongoDB
    \tCouchDB
    \tRethinkDB
    \tDynamoDB

    数据库周边
    \tORM框架
    \tACID 特性
    \tBASE 特性
    \t\tBasic Availability:基本可用。
    \t\tSoft-state:软状态。
    \t\tEventual Consistency:最终一致性
    \t事务
    \tN+1问题
    \t数据库范式
    \t索引是什么、工作原理
    \t数据副本
    \t分片策略
    \tCAP 定理

    API设计
    \tREST
    \tJson API
    \tSOAP 协议
    \t认证
    \t\t基于 Cookies
    \t\tOAuth
    \t\tBasic 认证
    \t\tToken 认证
    \t\tJWT
    \t\tOpenID
    \t\tSAML
    \tOpen API 规范 和 Swagger

    缓存
    \tCDN
    \tServer 端缓存
    \t\tRedis
    \t\tMemcached
    \tClient 端缓存

    Web 安全知识
    \tHash 算法
    \t\t何为MD5,为什么不要使用MD5来加密?
    \t\tSHA 家族
    \t\tSCrypt
    \t\tBCrypt
    \tHTTPS
    \tCORS
    \tSSL/TLS
    \t内容安全政策

    测试
    \t集成测试
    \t单元测试
    \t功能测试

    CI / CD

    设计模式和开发原则
    \tSOLID
    \tKISS
    \tYAGNI
    \tDRY
    \tGOF 设计模式
    \t领域驱动设计
    \t测试驱动设计

    架构模式
    \t单体架构
    \t微服务架构
    \tService Mesh
    \tSOA
    \tCQRS 和事件源
    \tServerless

    搜索引擎
    \tElasticSearch
    \tSolr

    消息中介
    \tRabbitMQ
    \tKafka

    容器化与虚拟化
    \tDocker
    \trkt
    \tLXC

    GraphQL
    \tApollo
    \tRelay Modern

    图数据库
    \tNeo4j

    WebSocket

    Web 服务器
    \tNginx
    \tApache
    \tCaddy
    \tMS IIS

    大规模建设
    \t容错策略
    \t\t降级
    \t\t限流
    \t\t负载转移
    \t\t断路器
    \t迁移策略
    \t水平与垂直伸缩
    \t可观察性建设
    \t8 条谬论
    \t\t网络是稳定的
    \t\t网络传输的延迟是零
    \t\t网络的带宽是无穷大
    \t\t网络是安全的
    \t\t网络的拓扑不会改变
    \t\t只有一个系统管理员
    \t\t传输数据的成本是零
    \t\t整个网络是同构的

    标记说明
    \t个人推荐
    \t替代方案
    \t不紧急,用到再学
    \t不推荐

    持续学习
    \n"},{"title":"CAS 禁用SSL 的方式","url":"/2017/CAS-%E7%A6%81%E7%94%A8SSL-%E7%9A%84%E6%96%B9%E5%BC%8F/","content":"

    由于要内部使用,所以不需要配置 https 链接。

    \n

    本文是基于 CAS 5.0.X 下进行的修改,修改方式如下:

    \n

    我使用 overlay 的方式进行的部署,只需在 etc/cas/config/cas.properties 中配置如下三项即可

    \n
    server.ssl.enabled=false
    cas.tgc.secure=false
    cas.warningCookie.secure=false
    \n

    但是现在还有一个问题时,几遍将 server.port 改为其他端口,http 的端口号也还是 8080,也就是说这里修改的 server.port 是修改的 https 的方式。

    \n"},{"title":"CPU 性能指标工具脑图","url":"/2020/CPU-performance-mind-map/","content":"

    \"\"

    \n
    CPU 使用率
    \t用户 CPU 使用率
    \t\tuser - 用户态 CPU 使用率
    \t\tnice - 低优先级用户态 CPU 使用率
    \t系统 CPU 使用率
    \t软中断和硬中断 CPU 使用率
    \t其他
    \t\tsteal - 虚拟化环境中会用到的窃取 CPU 利用率:被其他虚拟机占用的 CPU 时间百分比
    \t\tguest - 客户 CPU 使用率:运行客户虚拟机的 CPU 时间百分比

    上下文切换
    \t自愿上下文切换变多了,说明进程都在等待资源,有可能发生了 I/O 等其他问题
    \t非自愿上下文切换变多了,说明进程都在被强制调度,也就是都在争抢 CPU,说明 CPU 的确成了瓶颈;
    \t中断次数变多了,说明 CPU 被中断处理程序占用,还需要通过查看 /proc/interrupts 文件来分析具体的中断类型。

    平均负载
    \t 如果 1 分钟、5 分钟、15 分钟的三个值基本相同,或者相差不大,那就说明系统负载很平稳。
    \t如果 1 分钟的值远小于 15 分钟的值,就说明系统最近 1 分钟的负载在减少,而过去 15 分钟内却有很大的负载。
    \t如果 1 分钟的值远大于 15 分钟的值,就说明最近 1 分钟的负载在增加
    \t\t这种增加有可能只是临时性的,也有可能还会持续增加下去,所以就需要持续观察。
    \t\t一旦 1 分钟的平均负载接近或超过了 CPU 的个数,就意味着系统正在发生过载的问题,这时就得分析调查是哪里导致的问题,并要想办法优化了。

    CPU 缓存命中率

    工具
    \t平均负载
    \t\tuptime
    \t\ttop
    \t系统整体 CPU 使用率
    \t\tvmstat
    \t\tmpstat
    \t\t\t运行 mpstat 查看 CPU 使用率的变化情况:
    # -P ALL 表示监控所有CPU,后面数字5表示间隔5秒后输出一组数据
    $ mpstat -P ALL 5
    \t\ttop
    \t\tsar
    \t\t/proc/stat
    \t\t\t其他性能工具的数据来源
    \t进程 CPU 使用率
    \t\ttop
    \t\tpidstat
    \t\t\t# 间隔5秒后输出一组数据
    $ pidstat -u 5 1
    \t\t\t# 每隔1秒输出1组数据(需要 Ctrl+C 才结束)
    # -w参数表示输出进程切换指标,而-u参数则表示输出CPU使用指标
    $ pidstat -w -u 1
    \t\t\tpidstat 默认显示进程的指标数据,加上 -t 参数后,才会输出线程的指标。
    # 每隔1秒输出一组数据(需要 Ctrl+C 才结束)
    # -wt 参数表示输出线程的上下文切换指标
    $ pidstat -wt 1
    \t\tps
    \t\thtop
    \t\tatop
    \t系统上下文切换
    \t\tvmstat
    \t\t\t# 每隔5秒输出1组数据
    # vmstat 5
    \t\t\tcs(context switch)是每秒上下文切换的次数。
    \t\t\tin(interrupt)则是每秒中断的次数。
    \t\t\tr(Running or Runnable)是就绪队列的长度,也就是正在运行和等待 CPU 的进程数。
    \t\t\tb(Blocked)则是处于不可中断睡眠状态的进程数。
    \t进程上下文切换
    \t\tpidstat
    \t\t\t给它加上 -w 选项,你就可以查看每个进程上下文切换的情况了
    $ pidstat -w 5 1
    \t\t\tcswch ,表示每秒自愿上下文切换(voluntary context switches)的次数
    \t\t\tnvcswch ,表示每秒非自愿上下文切换(non voluntary context switches)的次数
    \t软中断
    \t\ttop
    \t\t/proc/softirqs
    \t\tmpstat
    \t网络
    \t\tdstat
    \t\tsar
    \t\t\t# -n DEV 表示显示网络收发的报告,间隔1秒输出一组数据
    $ sar -n DEV 1
    \t\ttcpdump
    \t\t\t# -i eth0 只抓取eth0网卡,-n不解析协议名和主机名
    # tcp port 80表示只抓取tcp协议并且端口号为80的网络帧
    $ tcpdump -i eth0 -n tcp port 80
    \tI/O
    \t\tdstat
    \t\t\tdstat 的好处是,可以同时查看 CPU 和 I/O 这两种资源的使用情况,便于对比分析。
    \t\t\t# 间隔1秒输出10组数据
    $ dstat 1 10
    \t\tipstat
    \t\t\t# -d 展示 I/O 统计数据,-p 指定进程号,间隔 1 秒输出 3 组数据
    $ pidstat -d -p 4344 1 3
    \t\tsar
    \tCPU 个数
    \t\t/proc/cpuinfo
    \t\tlscpu
    \t事件剖析
    \t\tperf
    \t\t\tperf top:类似于 top,它能够实时显示占用 CPU 时钟最多的函数或者指令,因此可以用来查找热点函数
    \t\t\t\t第一行包含三个数据,分别是采样数(Samples)、事件类型(event)和事件总数量(Event count)。
    \t\t\t\t再往下看是一个表格式样的数据,每一行包含四列,分别是:
    \t\t\t\t\t Overhead ,是该符号的性能事件在所有采样中的比例,用百分比来表示。
    \t\t\t\t\tShared ,是该函数或指令所在的动态共享对象(Dynamic Shared Object),如内核、进程名、动态链接库名、内核模块名等。
    \t\t\t\t\tObject ,是动态共享对象的类型。比如 [.] 表示用户空间的可执行程序、或者动态链接库,而 [k] 则表示内核空间。
    \t\t\t\t\tSymbol 是符号名,也就是函数名。当函数名未知时,用十六进制的地址来表示。
    \t\t\tperf record 和 perf report
    \t\t\t\t$ perf record # 按Ctrl+C终止采样
    \t\t\t\t§ $ perf report # 展示类似于perf top的报告
    \t\t\t# -g开启调用关系分析,-p指进程号21515
    $ perf top -g -p 21515
    \t\texecsnoop
    \n"},{"title":"Counter的elements()源码阅读笔记","url":"/2016/Counter%E7%9A%84elements-%E6%BA%90%E7%A0%81%E9%98%85%E8%AF%BB%E7%AC%94%E8%AE%B0/","content":"

    Counterelements() 方法返回一个迭代器。元素被重复了多少次,在该迭代器中就包含多少个该元素。所有元素按照字母序排序,个数小于1的元素不被包含。

    \n

    举例:

    \n
    >>> c = Counter('ABCABC')
    >>> sorted(c.elements())
    ['A', 'A', 'B', 'B', 'C', 'C']
    \n

    源码如下:

    \n
    def elements(self):
    return _chain.from_iterable(_starmap(_repeat, self.iteritems()))
    \n

    好!!!精!!!简!!!

    \n

    从里往外看这行代码吧:

    \n
    _starmap(_repeat, self.iteritems())
    \n

    _starmapitertools 模块中的一个实现了 __iter__ 方法的类,构造器接收两个参数:一个函数(function)和一个序列(sequence),作用是创建一个迭代器,生成值function(*item),其中item来自sequence,只有当sequence生成的项适用于这种调用函数的方式时,此函数才有效。

    \n

    itertools.starmap(function, iterable) 等价于:

    \n
    def starmap(function, iterable):
    for args in iterable:
    yield function(*args)
    \n

    举例:

    \n
    from itertools import starmap

    values = [(0, 5), (1, 6), (2, 7), (3, 8), (4, 9)]
    for i in starmap(lambda x,y:(x, y, x*y), values):
    print '%d * %d = %d' % i

    0 * 5 = 0
    1 * 6 = 6
    2 * 7 = 14
    3 * 8 = 24
    4 * 9 = 36
    \n

    所以 _starmap(_repeat, self.iteritems()) 等价于下边的代码:

    \n
    for item in self.iteritems():
    yield _repeat(*item)
    \n

    也就是返回一个迭代器,迭代器的每一项是使用item 解包作为参数来调用 _repeat 的结果。

    \n

    下边再来看_repeatitertools.repeat(object[, times]),同样也是实现了__iter__方法的类,作用是创建一个迭代器,重复生成object,times(如果已提供)指定重复计数,如果未提供times,将无止尽返回该对象。

    \n

    等价于:

    \n
    def repeat(object, times=None):
    if times is None:
    while True:
    yield object
    else:
    for i in xrange(times):
    yield object
    \n

    举例:

    \n
    from itertools import *

    for i in repeat('over-and-over', 5):
    print i

    over-and-over
    over-and-over
    over-and-over
    over-and-over
    over-and-over
    \n

    repeat 很容易理解就不用解释了。

    \n

    下边我们返回去看_starmap(_repeat, self.iteritems()), 这些完这些,得到的结果是一个迭代器里边每一项依然是个迭代器,每个内层迭代器迭代出的结果是重复生成的项。

    \n

    可以想象成这样:

    \n
    >>> _starmap(_repeat, [{'A': 2, 'B': 3, 'C': 4}])

    ['AA', 'BBB', 'CCCC']
    \n

    再来看一下chainitertools.chain(*iterables), 将多个迭代器作为参数, 但只返回单个迭代器, 它产生所有参数迭代器的内容, 就好像他们是来自于一个单一的序列。

    \n

    等价于:

    \n
    def chain(*iterables):
    for it in iterables:
    for element in it:
    yield element
    \n

    举例:

    \n
    for i in chain([1, 2, 3], ['a', 'b', 'c']):
    print i
    1
    2
    3
    a
    b
    c
    \n

    chain 的 类函数 from_iterable 可以理解成接收一个参数,然后将这个参数解包后调用构造器。

    \n

    以上例子也可以写成:

    \n
    for i in chain.from_iterable([[1, 2, 3], ['a', 'b', 'c']]):
    print i
    1
    2
    3
    a
    b
    c
    \n

    所以,用 chain 来合并 _starmap(_repeat, self.iteritems()) 得到的嵌套迭代器后得到的就是我们需要的结果了!

    \n

    最后再次感叹下Python代码的精简!

    \n
    \n

    更正前几篇中的出现过的一个错误:

    \n

    字典调用 iteritems 方法得到的并不是一个列表,而是一个迭代器。

    \n

    之前把 iteritems 一直当成 items 了。

    \n
    >>> x = {'title':'python web site','url':'www.iplaypython.com'}
    >>> x.items()

    [('url', 'www.iplaypython.com'), ('title', 'python web site')]
    >>> a
    [('url', 'www.iplaypython.com'), ('title', 'python web site')]
    >>> type(a)
    <type 'list'>

    >>> f = x.iteritems()
    >>> f
    <dictionary-itemiterator object at 0xb74d5e3c>
    >>> type(f)
    <type 'dictionary-itemiterator'> #字典项的迭代器
    >>> list(f)
    [('url', 'www.iplaypython.com'), ('title', 'python web site')]
    ","categories":["源码"],"tags":["源码","Python"]},{"title":"Counter的most_common()源码阅读笔记","url":"/2016/Counter%E7%9A%84most-common-%E5%92%8Celements-%E6%BA%90%E7%A0%81/","content":"

    这个方法可以传一个可选参数 n, 代表获取数量最多的前 n 个元素。如果不传参数,则返回所有结果。

    \n

    反回的结果是一个列表,里边的元素是一个元组,元组第0位是被计数的具体元素,元组第1位是出现的次数。如:[('a', 5), ('b', 4), ('c', 3)],当多个元素计数值相同时,按照字母序排列。

    \n

    下边是 most_common 的源码:

    \n
    def most_common(self, n=None):
    if n is None:
    return sorted(self.iteritems(), key=_itemgetter(1), reverse=True)
    return _heapq.nlargest(n, self.iteritems(), key=_itemgetter(1))
    \n

    先来看n是None的情况,因为Counter类继承自dict,所以 self.iteritems 得到的是键值对元组的列表,用 sorted对这个列表进行排序,因为是要按照元组的第1位的数字从大到小的顺序来排序,所以key应该是元组的第1位。代码中用 _itemgetter(1)来取出元组的第1位,_itemgetteroperator 模块里的 itemgetter 类,这个类重写了 __call__ 方法,所以这个类的实例可以当做函数来调用。 具体用法如下:

    \n
    After f = itemgetter(2), the call f(r) returns r[2].
    After g = itemgetter(2, 5, 3), the call g(r) returns (r[2], r[5], r[3])
    \n

    现在 key=itemgetter(1), 即 key(r) 就是 r[1] 这样就可以取到我们想要的那个值了,如果换作之前,我可能会重新定义一个函数,然后赋值给key,最多写一个lambda表达式: lambda x:x[1]赋值给key,这些都是重造轮子的例子。。。不好不好。。。

    \n

    此时我们实际要进行的是整数之间的比较,就不用再给 sortedcmp 参数赋值了,因为我们要得到一个从大到小排列的结果,所以最后 reverse=True

    \n

    如果 n 不为 None ,调用了 heapq(最上边导入时将heapq as 重命名成了 _heapq) 模块中的 nlargest 函数,这个函数的实现有些略微复杂,等以后有时间再去看,直接看下函数的介绍:

    \n
    Find the n largest elements in a dataset.
    Equivalent to: sorted(iterable, key=key, reverse=True)[:n]
    \n

    这个函数的调用结果和用 sorted 排序后再取出前n个结果等价。

    \n

    也就是 sorted(self.iteritems(), key=_itemgetter(1), reverse=True)[:n]

    \n

    下一篇写Counter的elements()方法

    \n","categories":["源码"],"tags":["源码","Python"]},{"title":"《关键对话》脑图","url":"/2020/Critical-Conversation-mind-map/","content":"

    \"\"

    \n

    为了更好的 SEO,把大纲放在下边。读者也可根据大纲自行绘制自己的脑图。

    \n
    从「心」开始
    \t对话技巧
    \t\t关注你的真正目的
    \t\t拒绝做出「傻瓜式选择」
    \t关键问题
    \t\t我让人感觉自己的目的是什么
    \t\t我的真正目的是什么
    \t\t\t关于自己的
    \t\t\t关于他人的
    \t\t\t关于我们之间关系的
    \t\t怎样做才能实现这些真正目的
    \t\t我不希望怎样
    \t\t怎样才能真正实现希望的目的,避免不希望实现的目的

    注意观察
    \t对话技巧
    \t\t关注交谈何时会变成关键对话
    \t\t关注安全问题
    \t\t关注你的压力应对方式
    \t关键问题
    \t\t我正在陷入沉默或暴力状态吗
    \t\t对方正在陷入沉默或暴力状态吗

    保证安全
    \t对话技巧
    \t\t在必要时道歉
    \t\t利用对比法消除误解
    \t\t利用四步法创建共同目的
    \t关键问题
    \t\t安全感为什么会出现危机
    \t\t\t我是否建立了共目的
    \t\t\t我是否保持了彼此尊重
    \t\t怎样做才能重建安全感

    控制想法
    \t对话技巧
    \t\t行为模式回顾
    \t\t区分事实和想法
    \t\t留意三种「小聪明」
    \t\t改变主观臆断
    \t关键问题
    \t\t我的想法是什么
    \t\t我是否故意忽略自己在这个问题中的责任
    \t\t一个理智而正常的人为什么会这样做
    \t\t要想实现真正的目的应该怎么做

    陈述观点
    \t对话技巧
    \t\t分享事实经过
    \t\t说出你的想法
    \t\t征询对方观点
    \t\t做出试探表述
    \t\t鼓励做出尝试
    \t关键问题
    \t\t我是否对对方观点完全开放
    \t\t我讨论的是不是真正的问题
    \t\t我是否自信地表达自己的观点

    了解动机
    \t对话技巧
    \t\t询问观点
    \t\t确认感受
    \t\t重新描述
    \t\t主动引导
    \t\t赞同
    \t\t补充
    \t\t比较
    \t关键问题
    \t\t我是否积极了解对方的看法
    \t\t我是否努力避免不必要的不合

    开始行动
    \t对话技巧
    \t\t决定如何决策
    \t\t记录决策并进行监督检查
    \t关键问题
    \t\t我们应当怎样决策
    \t\t何人何时完成何种任务
    \t\t如何对任务实施检查评估
    \n"},{"title":"Datetime相减计算总秒数遇到的坑","url":"/2015/Datetime%E7%9B%B8%E5%87%8F%E8%AE%A1%E7%AE%97%E6%80%BB%E7%A7%92%E6%95%B0%E9%81%87%E5%88%B0%E7%9A%84%E5%9D%91/","content":"

    一直以为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()

    \n

    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

    \n

    在Python2.7中加入了timedelta.total_seconds()方法:

    \n

    timedelta.total_seconds()

    \n
    \n

    Return 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
    \n

    Note that for very large time intervals (greater than 270 years on most platforms) this method will lose microsecond accuracy.

    \n
    \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

    \n","categories":["Code"],"tags":["Python","Datetime","坑"]},{"title":"好书推荐:《数据密集型应用系统设计》","url":"/2019/Designing-Data-Intensive-Application/","content":"

    今天推荐一本我近期读到的质量很高的技术书(也可以说是我今年读到的最好的一本技术类书籍):《数据密集型应用系统设计》,属于「动物书」系列,封面是一只野猪。这本书我从上个月 18 号开始读,每天拿出一个半小时左右阅读,于昨天(12月28号)读完,刚好用了 40 天,全书 500 多页,也算是一本大部头了。

    \n

    \"\"

    \n

    本书作者 Martin Kleppmann 是英国剑桥大学分布式系统方向的研究员。之前在 LinkedIn 和 Rapportive 等互联网公司做过软件工程师,负责大规模数据基础设施建设。在阅读过程中,我多次惊叹作者的知识面简直广得惊人,也善于举一反三,知识之间互相关联。

    \n

    \"\"

    \n

    全书脉络清晰,分为三个部分:

    \n

    第一部分介绍数据相关的基本思想,包括如何评价一个数据库(第一章),数据在逻辑上如何组织(第二章),在磁盘中如何分布(第三章),在表现上如何编码(第四章)。这些思想是一个数据系统的基本,无论它是单机的,还是分布式的。

    \n

    第二部分介绍分布式环境下的技术,包括复制(第五章)、分区(第六章)、分布式事务与共识(第七、八、九章)。这些技术大多是基于同构系统的,分布式事务虽然也能在异构系统中应用,但是复杂度要高很多。

    \n

    第三部分介绍异构系统中数据的处理技术,包括批处理(第十章)和流处理(第十一章),最后提出一种以流处理为主的异步数据处理方案,有可能在日后成为构建应用的主流方案(第十二章)。

    \n

    作者在最后一小节还讨论了大数据的伦理问题,尽管在现实世界中、在金钱利益面前,可能无人理会这些事情,但是这些夫子自道,还是很体现作者情怀,可以说这也是全书升华的地方,同时让我对作者肃然起敬再次 +1。

    \n

    书中把软件开发中(以后端为主)常用的技术本质、来龙去脉、使用场景、优点劣势都讲得非常清楚,并且讲解得深入浅出,把复杂的东西简单化,可见作者文笔之深厚。这一本书中囊括了几乎所有数据处理相关工作中可能遇到的内容,而且还提供了非常好的实操性。书中很多问题我在实际场景中也都遇到过,读起来使我醍醐灌顶、击节扼腕,每每读到我之前踩坑的地方都会想:如果我能早点读到这本书能少走很多弯路。

    \n

    书中的配图也很到位,大部分是流程图,有时候文字读不懂的地方,看到配图就会明白,我贴几张图感受一下:

    \n

    ETL 介绍:

    \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 的关键字并没有发现什么有用的信息。

    \n

    这个接口的 URL 是:/api/admin/banners,我修改了接口对应的 URL 后再次尝试,发现又正常了,然后经过各种尝试,发现只要路径中带有 admin/banners 就无法访问,我把浏览器发送的请求转为 cURL 命令,使用终端发送也是正常的,所以确定问题应该出在浏览器身上。

    \n

    这时候想起来我在上家公司工作时,写的一个广告接口,URL 中带有 advertisement,刚开始也是无法请求,最后发现是装了屏蔽广告的插件导致的,这次一定也是因为这个问题,所以我尝试把 AdBlock 关掉,再次测试,一切 ok。

    \n

    这中间差不多花费了20多分钟来排查问题,没有很快找到问题的一个重要原因是,我的火狐浏览器恰好也装了 AdBlock,所以早早的就把浏览器的问题排除了。

    \n

    最后兜了个圈又回到原点。

    \n"},{"title":"关注当下与松驰感","url":"/2023/Focus-on-the-present-and-relaxation/","content":"

    这篇博客是由我、飞书妙记、ChatGPT和NotionAI共同完成。

    \n
      \n
    1. 我用飞书妙记口述零碎的内容,录了6分钟。
    2. \n
    3. 飞书妙记将口述内容转为文字。
    4. \n
    5. 我进行了简单的校对和删除无用口语词汇。
    6. \n
    7. ChatGPT将这些零碎的内容重新组织成一篇连贯的文章,并进行了适当的补充。
    8. \n
    9. 我和NotionAI进行了最终的编辑和润色。
    10. \n
    \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时一直报错:

    \n
    Permission denied (publickey).
    fatal: Could not read from remote repository.

    Please make sure you have the correct access rights
    and the repository exists.
    \n

    因为我在/home/www下进行的操作,这个目录当前用户是没有写权限的,所以需要在操作git时前边加sudo,所以才会导致这个问题。

    \n

    官方说明:

    \n

    You 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.

    \n

    解决方法,将www目录权限改为777。

    \n","categories":["GitHub"],"tags":["Linux","GitHub"]},{"title":"Gradle 笔记","url":"/2018/Gradle-%E7%AC%94%E8%AE%B0/","content":"

    Gradle的构建生命周期

    \"\"

    \n

    Gradle 项目可以使用 Maven Plugin 将构建上传到 Maven 仓库中:

    apply plugin: 'maven'
    ...
    uploadArchives {
    repositories.mavenDeployer {
    repository(url: "http://localhost:8088/nexus/content/repositories/snapshots/") {
    authentication(userName: "admin", password: "admin123")
    pom.groupId = "com.juvenxu"
    pom.artifactId = "account-captcha"
    }
    }
    }
    \n

    想在 build 时发布版本导 Maven 仓库中,只需要添加一行任务依赖配置即可:

    build.dependsOn 'uploadArchives'
    \n

    通过 gradle -q tasks 显示所有的任务

    任务名称缩写

    Gradle提高效率的一个办法就是能够在命令行输入任务名的驼峰简写,当你的任务名称非常长的时候这很有用

    \n

    命令行选项

      \n
    • -i:Gradle默认不会输出很多信息,你可以使用-i选项改变日志级别为INFO
    • \n
    • -s:如果运行时错误发生打印堆栈信息
    • \n
    • -q:只打印错误信息
    • \n
    • -?-h,–help:打印所有的命令行选项
    • \n
    • -b,–build-file:Gradle默认执行build.gradle脚本,如果想执行其他脚本可以使用这个命令,比如gradle -b test.gradle
    • \n
    • –offline:在离线模式运行build,Gradle只检查本地缓存中的依赖
    • \n
    • -D, –system-prop:Gradle作为JVM进程运行,你可以提供一个系统属性比如:-Dmyprop=myValue
    • \n
    • -P,–project-prop:项目属性可以作为你构建脚本的一个变量,你可以传递一个属性值给build脚本,比如:-Pmyprop=myValue
    • \n
    • tasks:显示项目中所有可运行的任务
    • \n
    • properties:打印你项目中所有的属性值
    • \n
    \n

    指定 Main-Class

    jar {
    \tmanifest {
    \t attributes 'Main-Class': 'com.jpanj.hello.Hello'
    \t}
    }
    \n

    gradle wrapper 的目录结构

    \"\"

    \n

    所以,gradle目录gradlewgradlew.bat 都应该放在版本控制内。

    \n

    包装器可以根据需求自定义,如访问外网受限时:

    task wrapper(type: Wrapper) {
    //Requested Gradle version
    gradleVersion = '1.2'
    //Target URL to retrieve Gradle wrapper distribution
    distributionUrl = 'http://myenterprise.com/gradle/dists'
    //Path where wrapper will be unzipped relative to Gradle home directory
    distributionPath = 'gradle-dists'
    }
    \n

    更多包装器的特性查看:http://gradle.org/docs/current/dsl/org.gradle.api.tasks.wrapper.Wrapper.html

    \n

    Gradle允许通过外部属性来定义自己的变量

    外部属性一般存储在键值对中,要添加一个属性,需要使用ext命名空间:
    //Only initial declaration of extra property requires you to use ext namespace
    project.ext.myProp = 'myValue'
    ext {
    someOtherProp = 123
    }

    //Using ext namespace to access extra property is optional
    assert myProp == 'myValue'
    println project.someOtherProp
    ext.someOtherProp = 567
    \n
    外部属性可以定义在一个属性文件中: 通过在 /.gradle路径 或者项目根目录下的 gradle.properties 文件来定义属性,可以直接注入到项目中:

    假设在 gradle.properties 文件中定义了下面的属性:

    \n
    exampleProp = myValue
    someOtherProp = 455
    \n

    可以在项目中访问这两个变量:

    \n
    assert project.exampleProp == 'myValue'

    task printGradleProperty << {
    println "Second property: $someOtherProp"
    }
    \n
    定义属性的其他方法
      \n
    • 通过 -P 命令行选项来定义项目属性
    • \n
    • 通过 -D 命令行选项来定义系统属性
    • \n
    • 环境属性遵循这个模式:ORG_GRADLE_PROJECT_propertyName=someValue
    • \n
    \n

    当任务创建的时候你可以添加任意多个动作,每一个任务都有一个动作清单,他们在运行的时候是执行的

    task printVersion {
    \t//任务的初始声明可以添加first和last动作
    doFirst {
    println "Before reading the project version"
    }

    doLast {
    println "Version: $version"
    }
    }

    printVersion.doFirst { println "11111" }
    printVersion.doLast { println "22222" }
    \n

    输出

    \n
    > Task :printVersion
    11111
    Before reading the project version
    Version: 0.1
    22222
    \n
    \n

    当你想添加动作的那个任务不是你自己写的时候这会非常有用,你可以添加一些自定义的逻辑,比如你可以添加 doFirst 动作到 compile-Java 任务来检查项目是否包含至少一个 source 文件。

    \n
    \n

    dependsOn方法用来声明一个任务依赖于一个或者多个任务

    task first << { println "first" }
    task second << { println "second" }

    //声明多个依赖
    task printVersion(dependsOn: [second, first]) << {
    logger.quiet "Version: $version"
    }

    task third << { println "third" }
    //通过任务名称来声明依赖
    third.dependsOn('printVersion')
    \n

    输出

    \n
    first
    second
    Version: 0.1
    third
    \n
    \n

    Gradle 并不保证依赖的任务能够按顺序执行,dependsOn方法只是定义这些任务应该在这个任务之前执行,但是这些依赖的任务具体怎么执行它并不关心(也就是和 [second, first] 顺序 无关),因为任务不是顺序执行的,就可以并发的执行来提高性能。

    \n
    \n

    可以使用 finalizedBy 使一个任务结束后自动触发另一个

    task first << { println "first" }
    task second << { println "second" }
    //声明first结束后执行second任务
    first.finalizedBy second
    \n

    first 结束后自动触发任务 second

    \n

    > Task :first
    first

    > Task :second
    second
    \n

    allprojects 和 subprojects

    你可以用 allprojects 方法给所有的项目添加 groupversion 属性,由于根项目不需要 Java 插件,你可以使用 subprojects 给所有子项目添加Java插件:

    \n
    allprojects {
    group = 'com.manning.gia'
    version = '0.1'
    }

    subprojects {
    apply plugin: 'java'
    }
    \n"},{"title":"《黑客与画家》摘抄","url":"/2021/HackersPainters/","content":"

    译者序

    自由软件基金会创始人理查德·斯托尔曼说:“出于兴趣而解决某个难题,不管它有没有用,这就是黑客。”

    \n

    黑客的六条价值观:

    \n
      \n
    1. 使用计算机以及所有有助于了解这个世界本质的事物都不应受到任何限制。任何事情都应该亲手尝试。
    2. \n
    3. 信息应该全部免费。
    4. \n
    5. 不信任权威,提倡去中心化。
    6. \n
    7. 判断一名黑客的水平应该看他的技术能力,而不是看他的学历、年龄或地位等其他标准。
    8. \n
    9. 你可以用计算机创造美和艺术。
    10. \n
    11. 计算机使生活更美好。
    12. \n
    \n

    前言

    人们区分程序员甚至不是看他们写了什么程序,而是看他们使用什么语言。

    \n

    1. 为什么书呆子不受欢迎

    受父母的影响,书呆子被教导追求正确答案,而受欢迎的小孩被教导讨人喜欢。

    \n

    青少年在心理上还没有摆脱儿童状态,许多人都会残忍地对待他人。他们折磨书呆子的原因就像拔掉一条蜘蛛腿一样,觉得很好玩。在一个人产生良知之前,折磨就是一种娱乐。

    \n

    在任何社会等级制度中,那些对自己没自信的人就会通过虐待他们眼中的下等人来突显自己的身份。我已经意识到,正是因为这个原因,在美国社会中底层白人是对待黑人最残酷的群体。

    \n

    不受欢迎是一种传染病,虽然善良的孩子不会去欺负书呆子,但是为了保护自己,也依然会与书呆子保持距离。难怪聪明的小孩读中学时往往是不快乐的。

    \n

    公立学校的老师很像监狱的狱卒。看管监狱的人主要关心的是犯人都待在自己应该待的位置。然后,让犯人有东西吃,尽可能不要发生斗殴和伤害事件,这就可以了。除此以外,他们一件事也不愿多管,没必要自找麻烦。所以,他们就听任犯人内部形成各种各样的小集团。

    \n

    真实世界的关键并非在于它是由成年人组成的,而在于它的庞大规模使得你做的每件事都能产生真正意义上的效果。

    \n

    表面上,学校的使命是教育儿童。事实上,学校的真正目的是把儿童都关在同一个地方,以便大人们白天可以腾出手来把事情做完。

    \n

    人类喜欢工作,在世界上大多数地方,你的工作就是你的身份证明。

    \n

    以前的青少年似乎也更尊敬成年人,因为成年人都是看得见的专家,会传授他们所要学习的技能。如今的大多数青少年,对他们的家长在遥远的办公室所从事的工作几乎一无所知。他们看不到学校作业与未来走上社会后从事的工作有何联系。

    \n

    校园生活的两大恐怖之处——残忍和无聊

    \n

    校园生活的真正问题是空虚。

    \n

    2. 黑客与画家

    黑客与画家的共同之处,在于他们都是创作者。与作曲家、建筑师、作家一样,黑客和画家都是试图创作出优秀的作品

    \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

    3. 不能说的话

    所谓“流行”(传统观念也是一种流行),本质上就是自己看不见自己的样子。

    \n

    历史的常态似乎就是,任何一个年代的人们,都会对一些荒谬的东西深信不疑。

    \n

    最令人暴跳如雷的言论,就是被认为说出了真相的言论。

    \n

    最先从你头脑中跳出来的想法,往往就是最困扰你、很可能为真的想法。你已经注意到它们,但还没有认真思考过。

    \n

    如果某个观点在大部分时空都是不受禁止的,只有我们这个社会才把它当作禁忌,那么很可能是我们出错了。

    \n

    孩子眼里的世界是不真实的,是一个被灌输进他们头脑的假想世界。将来当孩子长大以后接触社会,就会发现小时候以为真实的事情,在现实世界中是荒唐可笑的。

    \n

    找出不能说的话的四种方式:

    \n
      \n
    1. 判断言论的真伪
    2. \n
    3. 关注“异端邪说”
    4. \n
    5. 回顾过去
    6. \n
    7. 寻找那些一本正经的卫道者,看看他们到底在捍卫者什么
    8. \n
    \n

    流行的时尚产生于某个有影响力的人物,他突发奇想,接着其他人纷纷模仿。

    \n

    但是,流行的道德观念不是这样,它们往往不是偶然产生的,而是被刻意创造出来的。如果有些观点我们不能说出口,原因很可能是某些团体不允许我们说。

    \n

    如果一个团体强大到无比自信,它根本不会在乎别人的抨击。美国人或者英国人对外国媒体的诋毁就毫不在意。

    \n

    大多数的斗争,不管它们实际上争的是什么,都会以思想斗争的形式表现出来。

    \n

    优秀作品往往来自于其他人忽视的想法,而最被忽视的想法就是那些被禁止的思想观点。

    \n

    智力越高的人,越愿意去思考那些惊世骇俗的思想观点。这不仅仅因为聪明人本身很积极地寻找传统观念的漏洞,还因为传统观念对他们的束缚力很小,很容易摆脱。从他们的衣着上你就可以看出这一点:不受传统观念束缚的人,往往也不会穿流行的衣服

    \n

    做一个异端是有回报的,不仅是在科学领域,在任何有竞争的地方,只要你能看到别人看不到或不敢看的东西,你就有很大的优势。

    \n

    训练自己去想那些不能想的事情,你获得的好处会超过所得到的想法本身。

    \n

    与笨蛋辩论,你也会变成笨蛋。

    \n

    自由思考比畅所欲言更重要……。在思想和言论之间划一条明确的界线。在心里无所不想,但是不一定要说出来……。你的思想是一个地下组织,绝不要把那里发生的事情一股脑说给外人听。

    \n

    讨论一个观点会产生更多的观点,不讨论就什么观点也没有。

    \n

    如果可能的话,你最好找一些信得过的知己,只与他们畅所欲言、无所不谈。这样不仅可以获得新观点,还可以用来选择朋友。能够一起谈论“异端邪说”并且不会因此气急败坏的人,就是你最应该认识的朋友。

    \n

    人们喜欢讨论的许多问题实际上都是很复杂的,马上说出你的想法对你并没有什么好处。

    \n

    如果你的思想很保守,你自己不会知道,而且你很可能还会持有相反的看法。

    \n

    如果一个命题不是错的,却被加上各种标签,进行压制和批判,那就有问题。

    \n

    4. 良好的坏习惯

    在程序员眼里,“黑客”指的是优秀程序员……。对于程序员来说,“黑客”这个词的字面意思主要就是“精通”,也就是他可以随心所欲地支配计算机。

    \n

    警方总是从犯罪动机开始调查。常见的犯罪动机不外乎毒品、金钱、性、仇恨等。满足智力上的好奇心并不在FBI的犯罪动机清单之上。

    \n

    在计算机工业的历史上,新技术往往是由外部人员开发的,而且所占的比例可能要高于内部人员。

    \n

    一个人们拥有言论自由和行动自由的社会,往往最有可能采纳最优方案,而不是采纳最有权势的人提出的方案。专制国家会变成腐败国家,腐败国家会变成贫穷国家,贫穷国家会变成弱小国家。

    \n

    5. 另一条路

    “你的电脑”这个概念正慢慢成为过去时,取而代之的是“你的数据”。

    \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
    • 迅速地做出1.0版,并且不断以改进,整个过程中密切倾听用户的反馈。
    • \n
    \n

    如果竞争对手的产品很糟糕,你也不要自鸣得意。比较软件的标准应该是看对手的软件将来会有什么功能,而不是现在有什么功能。

    \n

    6. 如何创造财富

    如果你想致富,应该怎么做?我认为最好的办法就是自己创业,或者加入创业公司。

    \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

    7. 关注贫富分化

    有三个原因使得我们对赚钱另眼相看。

    \n
      \n
    • 第一,我们从小被误导的对财富的看法;
    • \n
    • 第二,历史上积累财富的方式大多名声不好;
    • \n
    • 第三,担心收入差距拉大将对社会产生不利影响。
    • \n
    \n

    财富与金钱是两个概念。金钱只是用来交易财富的一种手段,财富才是有价值的东西,我们购买的商品和服务都属于财富。

    \n

    由于每个人创造财富的能力和欲望强烈程度都不一样,所以每个人创造财富的数量很不平等。

    \n

    每个人的技能不同,导致收入不同,这才是贫富分化的主要原因,正如逻辑学的“奥卡姆剃刀”原则所说,简单的解释就是最好的解释。

    \n

    如果说某种工作的报酬过低,那就相当于说人们的需求不正确。

    \n

    封建社会只有两个阶级:贵族与农奴(为贵族服务的人)。中产阶级是一个新的第三类团体,他们出现在城镇中,以制造业和贸易为生。

    \n

    创造财富真正取代掠夺和贪污成为致富的最佳方式,并不是发生在中世纪,而是发生在工业革命时代。

    \n

    双重误解:对一个已经过时的情况持有错误的看法。

    \n
      \n
    • 举例:由于人类历史上主要的致富方式长期以来都是偷窃,所以我们依然对有钱人抱有一种怀疑态度。理想主义的大学生从小受到历史上知名作家的影响,长大后不知不觉保留了孩提时对财富的看法。
    • \n
    \n

    技术应该会引起收入差距的扩大,但是似乎能缩小其他差距。一百年前,富人过着与普通人截然不同的生活。……由于技术的发展,富人的生活与普通人的差距缩小了。

    \n

    技术无法使其变得更便宜的唯一东西,就是品牌。

    \n

    只要存在对某种商品的需求,技术就会发挥作用,将这种商品的价格变得很低,从而可以大量销售。

    \n

    无论在物质上,还是在社会地位上,技术好像都缩小了富人与穷人之间的差距,而不是让这种差距扩大了。

    \n

    一个社会需要有富人,这主要不是因为你需要富人的支出创造就业机会,而是因为他们在致富过程做出的事情。……如果你让他致富,他就会造出一台拖拉机,使你不再需要使用马匹耕田了。

    \n

    8. 防止垃圾邮件的一种方法

    每个用户应该都分别有自己的概率分布表,这是根据他收到的邮件对每一个词进行统计后得出的。这样做可以:

    \n
      \n
    1. 使得过滤器更有效;
    2. \n
    3. 让每个用户自己定义,什么是他眼中的垃圾邮件;
    4. \n
    5. 使得垃圾邮件的发送者无法针对过滤器做出调整(这可能是最大的好处)。
    6. \n
    \n

    9. 设计者的品味

    人类的思想就是没有经过整理的无数杂念的混合。

    \n

    优秀设计的原则:

    \n
      \n
    • 好设计是简单的设计。……当你被迫把东西做得很简单时,你就被迫直接面对真正的问题。当你不能用表面的装饰交差时,你就不得不做好真正的本质部分。
    • \n
    • 好设计是永不过时的设计。……如果解决方法是丑陋的,那就肯定还有更好的解决方法,只是还没有发现而已。……如果你希望自己的作品对未来的人们有吸引力,方法之一就是让你的作品对上几代人有吸引力。
    • \n
    • 好设计是解决主要问题的设计。
    • \n
    • 好设计是启发性的设计。……在软件业中,这条原则意味着,你应该为用户提供一些基本模块,使得他们可以随心所欲自由组合,就像玩乐高积木那样。
    • \n
    • 好设计通常是有点趣味性的设计。……幽默感是强壮的一种表现,始终拥有幽默感就代表你对厄运一笑了之,而丧失幽默感则表示你被厄运深深伤到。
    • \n
    • 好设计是艰苦的设计。……人们常常觉得野生动物非常优美,原因就是它们的生活非常艰苦,在外形上不可能有多余的部分了。
    • \n
    • 好设计是看似容易的设计。……在大多数领域,看上去容易的事情,背后都需要大量的练习。练习的作用也许是训练你把刻意为之的事情变成一种自觉的行为。
    • \n
    • 好设计是对称的设计。……对称有两种:重复性对称和递归性对称。……在软件中,能用递归解决的问题通常代表已经找到了最佳解法。
    • \n
    • 好设计是模仿大自然的设计。……我能想象五十年后,小型的无人侦察飞机可以做得完全像鸟一样。
    • \n
    • 好设计是一种再设计。……专家的做法是先完成一个早期原型,然后提出修改计划,最后把早期原型扔掉。……你应该培养对自己的不满。……犯错误是很正常的事情。你不要把犯错看成灾难,要勇于承认、勇于改正。……开源软件因为公开承认自己会有bug,反而使得代码的bug比较少。
    • \n
    • 好设计是能够复制的设计。……把事情做对比原创更重要。
    • \n
    • 好设计常常是奇特的设计。……唯一达到“奇特”的方法,就是追求做出好作品,完成之后再回过头看。
    • \n
    • 好设计是成批出现的。……推动人才成批涌现的最大因素就是,让有天赋的人聚在一起,共同解决某个难题。互相激励比天赋更重要。
    • \n
    • 好设计常常是大胆的设计。……今天的实验性错误就是明天的新理论。如果你想做出伟大的新成果,那就不能对常识与真理不相吻合之处视而不见,反而应该特别注意才对。……伟大成果的出现常常来源于某人看到一样东西后,心想我能做得比这更好。
    • \n
    \n

    10. 编程语言解析

    编程语言的一个重要特点:一个操作所需的代码越多,就越难避免bug,也越难发现它们。

    \n

    编译器处理的高级语言代码又叫做源码。它经过翻译以后产生的机器码就叫做目标码

    \n

    开源软件就像一篇经受同行评议的论文。

    \n

    程序员的时间要比计算机的时间昂贵得多,后者已经变得很便宜了,所以几乎不值得非常麻烦地用汇编语言开发软件。

    \n

    如果你长期使用某种语言,你就会慢慢按照这种语言的思维模式进行思考。所以,后来当你遇到其他任何一种有重大差异的语言,即使那种语言本身并没有任何不对的地方,你也会觉得它极其难用。缺乏经验的程序员对于各种语言优缺点的判断经常被这种心态误导。

    \n

    语言设计者之间的最大分歧也许就在于,有些人认为编程语言应该防止程序员干蠢事,另一些人则认为程序员应该可以用编程语言干一切他们想干的事。

    \n

    事实上有两种程度的面向对象编程:某些语言允许你以这种风格编程,另一些语言则强迫你一定要这样编程。

    \n

    允许你做某事的语言肯定不差于强迫你做某事的语言。所以,至少在这方面我们可以得到明确的结论:你应该使用允许你面向对象编程的语言。至于你最后到底用不用则是另外一个问题了。

    \n

    11. 百年后的编程语言

    无论何时,选择进化的主干可能都是最佳方案。

    \n

    那些内核最小、最干净的编程语言才会存在于进化的主干上。

    \n

    编程语言进化缓慢的原因在于它们并不是真正的技术。

    \n

    随着技术的发展,每一代人都在做上一代人觉得很浪费的事情。

    \n

    我觉得一些最好的软件就像论文一样,也就是说,当作者真正开始动手写这些软件的时候,他们其实不知道最后会写出什么结果。

    \n

    浪费程序员的时间而不是浪费机器的时间才是真正的无效率。

    \n

    另一种消耗硬件性能的方法就是,在应用软件与硬件之间设置很多的软件层。

    \n

    除了某些特定的应用软件,一百年后,并行计算不会很流行。如果应用软件真的大量使用并行计算,这就属于过早优化了。

    \n

    应用软件运行速度提升的关键在于有一个好的性能分析器帮助指导程序开发。

    \n

    新语言更多地以开源项目的形式出现,而不是以研究性项目的形式出现。

    \n

    新语言的设计者更多的是本身就需要使用它们的应用软件作者,而不是编译器作者。

    \n

    设计新语言的方法之一就是直接写下你想写的程序,不管编译器是否存在,也不管有没有支持它的硬件。……随便什么,只要能让你最省力地写出来就行。

    \n

    12. 拒绝平庸

    真正非常严肃地把黑客作为人生目标的人,应该考虑学习Lisp。

    \n

    Lisp没有得到广泛使用的原因就是因为编程语言不仅仅是技术,也是一种习惯性思维,非常难于改变。

    \n

    程序员关心的那种强大也许很难正式定义,但是有一个办法可以解释,那就是有一些功能在一种语言中是内置的,但是在另一种语言中需要修改解释器才能做到,那么前者就比后者更强大。

    \n

    编程语言的特点之一就是它会使得大多数使用它的人满足于现状,不想改用其他语言。……人类天性变化的速度大大慢于计算机硬件变化的速度,所以编程语言的发展通常比CPU的发展落后一二十年。

    \n

    编程语言不一样,与其说它是技术,还不如说是程序员的思考模式。

    \n

    13. 书呆子的复仇

    各种编程语言的编程能力是不相同的。

    \n

    编程语言现在的发展不过刚刚赶上1958年Lisp语言的水平。

    \n

    使用一种不常见的语言会出现的问题我想到了三个:

    \n
      \n
    1. 你的程序可能无法很好地与使用其他语言写的程序协同工作;
    2. \n
    3. 你可能找不到很多函数库;
    4. \n
    5. 你可能不容易雇到程序员
    6. \n
    \n

    把软件运行在服务器端就可以没有顾忌地使用最先进的技术。

    \n

    到目前为止,大家公认少于10个人的团队最适合开发软件。

    \n

    选择更强大的编程语言会减少所需要的开发人员数量。因为:

    \n
      \n
    1. 如果你使用的语言很强大,可能会减少一些编程的工作量,也就不需要那么多黑客了;
    2. \n
    3. 使用更高级语言的黑客可能比别的程序员更聪明
    4. \n
    \n

    如果你创业的话,千万不要为了取悦风险投资商或潜在并购方而设计你的产品。

    \n

    衡量语言的编程能力的最简单方法可能就是看代码数量。……语言的编程能力越强大,写出来的程序就越短。

    \n

    代码的数量很重要,因为开发一个程序所耗费的时间主要取决于程序的长度。

    \n

    当团队规模超过某个门槛时,再增加人手只会带来净损失。

    \n

    一种出色的工具到了真正优秀的黑客手里,可以发挥出更大的威力。

    \n

    程序员使用某种语言能做到的事情是有极限的。

    \n

    你的经理其实不关心公司是否真的能获得成功,他真正关心的是不承担决策失败的责任。所以对他个人来说,最安全的做法就是跟随大多数人的选择。……既然我选择的是“业界最佳实践”,如果不成功,项目失败了,那么你也无法指责我,因为做出选择的人不是我,而是整个“业界”。

    \n

    编程语言的所谓“业界最佳实践”,实际上不会让你变成最佳,只会让你变得很平常。

    \n

    如果你想在软件业获得成功,就使用你知道的最强大的语言,用它解决你知道的最难的问题,并且等待竞争对手的经理做出自甘平庸的选择。

    \n

    14. 梦寐以求的编程语言

    编程语言本来就是为了满足黑客的需要而产生的,当且仅当黑客喜欢一种语言时,这种语言才能成为合格的编程语言。

    \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

    15. 设计与研究

    设计与研究的区别看来就在于,前者追求“好”(good),后者追求“新”(new)。

    \n

    艺术的各个领域有着巨大的差别,但是我觉得任何一个领域的最佳作品都不可能由对用户言听计从的人做出来。

    \n

    让用户满意并不等于迎合用户的一切要求。用户不了解所有可能的选择,也经常弄错自己真正想要的东西。做一个好的设计师就像做一个好医生一样。你不能头痛医头,脚痛医脚。病人告诉你症状,你必须找出他生病的真正原因,然后针对病因进行治疗。

    \n

    除非设定目标用户,否则一种设计的好坏根本无从谈起。

    \n

    如果你正在设计某种新东西,就应该尽快拿出原型,听取用户的意见。

    \n

    软件功能的增加并不必然带来质量的提高。

    \n"},{"title":"使用 Hoverfly 虚拟化服务","url":"/2020/Hoverfly-API-simulation/","content":"
    \"\"
    \n\n

    在说明什么是服务虚拟化前,先介绍另外两个常见的概念:

    \n

    Mock

    Mock 代指那些仅记录它们的调用信息的对象,在测试断言中我们需要验证 Mock 被进行了符合期望的调用。当我们并不希望真的调用生产环境下的代码或者在测试中难于验证真实代码执行效果的时候,我们会用 Mock 来替代那些真实的对象。

    \n

    Mock 典型的例子即是对邮件发送服务的测试,我们并不希望每次进行测试的时候都发送一封邮件,毕竟我们很难去验证邮件是否真的被发出了或者被接收了,我们更多地关注于邮件服务是否按照我们的预期在合适的业务流中被调用。

    \n

    Stub

    Stub 代指那些包含了预定义好的数据并且在测试时返回给调用者的对象。Stub 常被用于我们不希望返回真实数据或者会造成其他副作用的场景。

    \n

    Stub 的典型应用场景即是当某个对象需要从数据库获取数据时,我们并不需要真正地与数据库进行交互,而是直接返回预定义好的数据。

    \n

    服务虚拟化

    服务虚拟化技术可以让你创建一个模拟外部服务行为的应用程序,但是无需实际运行或者连接到外部服务。与 mock 或者 stub 相比,服务虚拟化技术可以管理更复杂的服务行为。

    \n

    服务虚拟化更像一种智能的 mock 或者 stub,非常适合内部逻辑复杂但接口定义良好,数据响应简单的服务。

    \n

    Hoverfly

    Hoverfly 是一款比较新的服务虚拟化工具,可以模拟遗留系统复杂的响应,以及支持许多服务相互依赖的微服务架构。Hoverfly 使用 Go 语言编写,轻量、高性能。

    \n

    Hoverfly 有非常丰富的功能和使用场景,今天我们仅介绍 Hoverfly 作为代理服务器时的两种常用模式:Capture 模式 和 Simulate 模式。

    \n

    安装

    ➜ brew install SpectoLabs/tap/hoverfly
    \n

    启动

    Hoverfly 带有一个称为 hoverctl 的命令行工具。

    \n
    ➜ hoverctl start
    Hoverfly is now running

    +------------+------+
    | admin-port | 8888 |
    | proxy-port | 8500 |
    +------------+------+
    \n

    可以看到默认情况下, Hoverfly 的代理服务运行在 8500 端口,后台页面运行在 8888 断口。

    \n

    状态检查

    ➜ hoverctl status

    +------------+----------+
    | Hoverfly | running |
    | Admin port | 8888 |
    | Proxy port | 8500 |
    | Proxy type | forward |
    | Mode | capture |
    | Middleware | disabled |
    | CORS | disabled |
    +------------+----------+
    \n

    Hoverfly 作为代理服务器

    \"\"

    \n

    如图所示,Hoverfly 最常见的使用场景是作为一个代理服务器在客户端和服务器之间传递请求。默认情况下,Hoverfly 作为代理服务器启动。

    \n

    代理服务器 VS Web服务器

    Hoverfly 还可以作为 Web服务器启动,这个不作为本文的重点,在这里简单对代理和Web服务器做个区分:

    \n

    代理服务器是一种特殊的Web服务器,二者主要区别在于:当Web服务器接收到来自客户端的请求时,它以预期的响应内容(例如HTML页面)进行响应。 通常,响应的数据存放在该服务器上或同一网络中。

    \n

    代理服务器应将传入的请求转发到另一台服务器(目标),同时,它还需要设置一些适当的头信息,如 X-Forwarded-ForX-Real-IPX-Forwarded-Proto 等。一旦代理服务器从目标接收到响应,再由它回传给客户端。

    \n

    捕获(Capture)模式

    捕获模式用于创建 API 模拟数据。

    \n

    \"\"

    \n

    在捕获模式下,Hoverfly 拦截客户端和外部服务之间的通信,并透明地记录来自客户端的传出请求和来自服务 API 的传入响应(类似于一个中间人)。

    \n

    通常,捕获模式被用作创建 API 模拟过程的起点, 然后将捕获的数据导出并修改,再重新导入到 Hoverfly 中以用作后续的模拟。

    \n

    默认情况下,如果请求未更改,Hoverfly 将忽略重复的请求。 所以尝试捕获有状态的端点可能会出现问题,因为每次发出请求时,该端点可能会返回不同的响应。当然,也可以通过配置禁用重复请求检查,所捕获的重复请求在仿真模式时会被循序使用。

    \n

    仿真(Simulate)模式

    在仿真模式下,Hoverfly 使用其模拟数据来仿真外部API。 每次 Hoverfly 收到请求时,它都会使用之前捕获到的数据进行响应(而不是将其转发到真实的服务)。 没有网络流量会到达真正的外部服务。

    \n

    \"\"

    \n

    仿真数据可以通过在捕获模式下运行 Hoverfly 自动生成,也可以手动创建。

    \n

    示例

    我们通过 curl 命令来做一个完整的演示。

    \n

    http://time.jsontest.com/ 是一个可以提供当前时间的 API 端点:

    \n
    ➜ curl -s http://time.jsontest.com/ | jq
    {
    "date": "06-27-2020",
    "milliseconds_since_epoch": 1593247079638,
    "time": "08:37:59 AM"
    }
    \n

    通过 hoverctl 将 Hoverfly 切换到捕获模式:

    ➜ hoverctl mode capture

    Hoverfly has been set to capture mode
    \n

    进入捕获模式后,我们再次想上边的 API 发出一个请求,不过这次指定 Hoverfly 作为代理:

    \n
    ➜ curl -s --proxy http://localhost:8500 http://time.jsontest.com | jq
    {
    "date": "06-27-2020",
    "milliseconds_since_epoch": 1593247241163,
    "time": "08:40:41 AM"
    }
    \n

    通过指定 proxy 参数,请求会首先转到代理,也就是 Hoverfly,然后再被转发到真正的 API 服务。响应的接收过程也是类似,这是 Hoverfly 拦截网络流量的方式。可以通过 Hoverfly 的日志了解具体发生了什么。

    \n
    ➜ hoverctl logs
    ……
    ……
    INFO[2020-06-27T16:39:31+08:00] Mode has been changed mode=capture
    INFO[2020-06-27T16:40:41+08:00] request and response captured mode=capture request="&map[body: destination:time.jsontest.com headers:map[Accept:[*/*] Proxy-Connection:[Keep-Alive] User-Agent:[curl/7.64.1]] method:GET path:/ query:map[] scheme:http]" response="&map[error:nil response]"
    \n

    切换到仿真模式:

    ➜ hoverctl mode simulate
    Hoverfly has been set to simulate mode with a matching strategy of 'strongest'
    \n

    在仿真模式下,Hoverfly 会使用我们之前记录下来的请求来对客户端进行响应,而不是将流量转发到真正的 API。

    \n

    现在我们继续以 Hoverfly 为代理,重复发起之前的请求:

    ➜ curl -s --proxy http://localhost:8500 http://time.jsontest.com | jq
    {
    "date": "06-27-2020",
    "milliseconds_since_epoch": 1593247241163,
    "time": "08:40:41 AM"
    }
    \n

    返回的数据与我们在捕获模式下请求的结果相同。

    \n

    接下来,如果我们想在客户端中模拟一个 100 年之后的响应,可以之前将捕获到的数据进行导出,重新编辑后导入 Hoverfly:

    \n
    ➜ hoverctl export simulation.json
    Successfully exported simulation to simulation.json
    \n

    查看 simulation.json` 的内容:

    \n
    {
    \t"data": {
    \t\t"pairs": [
    \t\t\t{
    \t\t\t\t"request": {
    \t\t\t\t\t"path": [
    \t\t\t\t\t\t{
    \t\t\t\t\t\t\t"matcher": "exact",
    \t\t\t\t\t\t\t"value": "/"
    \t\t\t\t\t\t}
    \t\t\t\t\t],
    \t\t\t\t\t"method": [
    \t\t\t\t\t\t{
    \t\t\t\t\t\t\t"matcher": "exact",
    \t\t\t\t\t\t\t"value": "GET"
    \t\t\t\t\t\t}
    \t\t\t\t\t],
    \t\t\t\t\t"destination": [
    \t\t\t\t\t\t{
    \t\t\t\t\t\t\t"matcher": "exact",
    \t\t\t\t\t\t\t"value": "time.jsontest.com"
    \t\t\t\t\t\t}
    \t\t\t\t\t],
    \t\t\t\t\t"scheme": [
    \t\t\t\t\t\t{
    \t\t\t\t\t\t\t"matcher": "exact",
    \t\t\t\t\t\t\t"value": "http"
    \t\t\t\t\t\t}
    \t\t\t\t\t],
    \t\t\t\t\t"body": [
    \t\t\t\t\t\t{
    \t\t\t\t\t\t\t"matcher": "exact",
    \t\t\t\t\t\t\t"value": ""
    \t\t\t\t\t\t}
    \t\t\t\t\t]
    \t\t\t\t},
    \t\t\t\t"response": {
    \t\t\t\t\t"status": 200,
    \t\t\t\t\t"body": "{\\n \\"date\\": \\"06-27-2020\\",\\n \\"milliseconds_since_epoch\\": 1593247241163,\\n \\"time\\": \\"08:40:41 AM\\"\\n}\\n",
    \t\t\t\t\t"encodedBody": false,
    \t\t\t\t\t"headers": {
    \t\t\t\t\t\t"Access-Control-Allow-Origin": [
    \t\t\t\t\t\t\t"*"
    \t\t\t\t\t\t],
    \t\t\t\t\t\t"Content-Length": [
    \t\t\t\t\t\t\t"100"
    \t\t\t\t\t\t],
    \t\t\t\t\t\t"Content-Type": [
    \t\t\t\t\t\t\t"application/json"
    \t\t\t\t\t\t],
    \t\t\t\t\t\t"Date": [
    \t\t\t\t\t\t\t"Sat, 27 Jun 2020 08:40:41 GMT"
    \t\t\t\t\t\t],
    \t\t\t\t\t\t"Hoverfly": [
    \t\t\t\t\t\t\t"Was-Here"
    \t\t\t\t\t\t],
    \t\t\t\t\t\t"Server": [
    \t\t\t\t\t\t\t"Google Frontend"
    \t\t\t\t\t\t],
    \t\t\t\t\t\t"X-Cloud-Trace-Context": [
    \t\t\t\t\t\t\t"50e02f0f588bb1a559fcb071b8da1344;o=1"
    \t\t\t\t\t\t]
    \t\t\t\t\t},
    \t\t\t\t\t"templated": false
    \t\t\t\t}
    \t\t\t}
    \t\t],
    \t\t"globalActions": {
    \t\t\t"delays": [],
    \t\t\t"delaysLogNormal": []
    \t\t}
    \t},
    \t"meta": {
    \t\t"schemaVersion": "v5.1",
    \t\t"hoverflyVersion": "v1.3.0",
    \t\t"timeExported": "2020-06-27T16:51:04+08:00"
    \t}
    }
    \n

    找到其中的body 字段,将其修改为:

    "body": "{\\n   \\"date\\": \\"06-27-2120\\",\\n   \\"milliseconds_since_epoch\\": 1593247241163,\\n   \\"time\\": \\"08:40:41 AM\\"\\n}\\n",
    \n

    将仿真数据导入 Hoverfly:

    ➜ hoverctl import simulation.json
    Successfully imported simulation from simulation.json
    \n

    再次发送请求:

    ➜ curl -s --proxy http://localhost:8500 http://time.jsontest.com | jq
    {
    "date": "06-27-2120",
    "milliseconds_since_epoch": 1593247241163,
    "time": "08:40:41 AM"
    }
    \n

    时间快进到了 100 年后。

    \n

    到这里,我们已经成功模拟了一个API端点,虽然我们这次演示了 curl 命令,但是在时间的测试中,应该由正在测试的应用程序向 Hoverfly 发起请求。一旦 Hoverfly 存储了请求和响应的数据,我们就不再需要访问真正的服务了,可以控制 Hoverfly 返回准确的响应。

    \n

    最后

    Hoverfly 还有一个可视化的管理页面,可以访问: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
      \n
    1. 客户端时钟存在较大不稳定因素,用户可以随时调整时钟导致序号回退等问题。
    2. \n
    3. 客户端本地序号如果重装应用会导致序号清零,也会导致序号回退的问题。
    4. \n
    5. 类似「群聊」和 「多点登录」这种多客户端场景,存在:物理世界中的同一时间点,不同客户端同时发消息给同一个接收方。
    6. \n
    \n

    第3点不太容易理解,用一个例子解释一下:比如同一个群里,多个用户同时发言,多客户端间由于存在时钟不同步的问题,并不能保证客户端带上来的时间是准确的,可能存在群里的用户 A 先发言,B 后发言,但由于用户 A 的手机时钟比用户 B 的慢了半分钟,如果以这个时间作为「时序基准」来进行排序,可能反而导致用户 A 的发言被认为是晚于用户 B 的。

    \n

    IM服务器生成

    客户端把消息提交给 IM 服务器后,IM 服务器依据自身服务器的时钟生成一个时间戳,再把消息推送给接收方时携带这个时间戳,接收方依据这个时间戳来进行消息的排序。

    \n

    在实际环境中,IM 服务都是集群化部署,集群化部署也就是许多服务器同时提供服务。

    \n

    虽然多台服务器通过 NTP 时间同步服务,能降低服务集群机器间的时钟差异到毫秒级别,但仍然还是存在一定的时钟误差,而且 IM 服务器规模相对比较大,时钟的统一性维护上也比较有挑战,整体时钟很难保持极低误差,因此一般也不能用 IM 服务器的本地时钟来作为消息的「时序基准」。

    \n

    全局序号生成器

    如果有一个全局递增的序号生成器,就能避免多服务器时钟不同步的问题了。IM 服务端就能通过这个序号生成器发出的序号,来作为消息排序的「时序基准」。

    \n

    这种「全局序号生成器」可以通过多种方式来实现,常见的比如 Redis 的原子自增命令 incr,DB 自带的自增 id,或者类似 Twitter 的 snowflake 算法、「时间相关」的分布式序号生成服务等。

    \n

    TT 在用的发号器

    TT 没有搭建独立的全局序号生成服务,而是利用 PostgreSQL 强大的 function 能力来实现的。TT 本身也在结构化存储上大规模使用了PostgreSQL,基建相对来说是比较完善。

    \n

    我们自己在 PostgreSQL 内实现了发号器函数,可以根据自己的ID、对方 ID、当前时间、shard 等条件生成集群间毫秒级唯一、保证递增但不保证连续的ID。

    \n

    性能

    我们 IM 使用的PostgreSQL集群分了8192个逻辑shard,每个shard每毫秒可生成1024个序号,理论上整个集群每秒最多了生成 (1024 * 1000 * 8192)=83亿个序号,性能上完完全全是够用的。

    \n

    可用性

    PostgreSQL 有自身的高可用架构,另外我们还用了 PostgreSQL 强大的逻辑 shard 能力,两个用户间的消息ID通过哈希规则,固定选择其中一个的shard 来生成,即使某个shard真的出了故障也只会影响8192 / 2 =4096分之1的用户。

    \n

    两个用户间的一致性

    考虑一个问题,如果不同的数据库实例的时间不一致,两个用户间的聊天顺序是否会有影响?答案是没有影响。

    \n

    两个用户之间的消息ID始终通过一个固定的实例生成的。具体shard选取规则为:

    \n
    (uid + other_uid) % shard_num
    \n

    通过以上规则,可以保证无论是用户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
    • 发送方
    • \n
    • 接收方
    • \n
    \n

    收发双方的历史消息是相互独立的。我用个例子解释一下:

    \n

    女神给你发消息说:「今天七夕,给我发个7777红包,我要截图发朋友圈,一会还你」。你毫不犹豫给女神发了过去。女神在截图前将「今天七夕,给我发个7777红包……」这句话进行了删除。

    \n

    相互独立的意思是说,一方删除消息不影响另一方的展示,女神那一侧删了,你这一侧是不受影响的,如果女神赖账不还,你可以把你这边的完整对话记录拿出来和她对峙。

    \n

    基于以上逻辑,在设计数据库表结构时我们需要为收发双方维护各自的索引记录。

    \n

    由于收发双方看到的消息内容实际是一致的,我们没有必要将内容存储两次,所以可以有一个表来独立存储消息内容。

    \n

    「消息内容表」存储消息纬度的基本信息,如:

    \n
      \n
    • 消息ID
    • \n
    • 消息内容
    • \n
    • 消息类型
    • \n
    • 消息时间
    • \n
    \n

    收发双方的「消息索引表」通过唯一的消息 ID 来和消息内容进行关联,同时还要有一个枚举字段来记录这是条发送消息还是接收消息。

    \n

    假设用户123给用户456发送一条消息,消息存储在关系型数据库中,上边涉及的两张表大致如下:

    \n

    \n

    \n

    123给456发了一条「你好」的消息,这个动作会在消息内容表中存储一条消息,这条消息的 ID 为1024。

    \n

    同时往索引表里存储两条数据:

    \n
      \n
    1. 用户ID 为123,另一方用户 ID 为456,这是条发出消息,消息 ID 为1024
    2. \n
    3. 用户ID 为456,另一方用户 ID 为123,这是条接收消息,消息 ID 为1024
    4. \n
    \n

    业界也常将消息的发出和接收这两个纬度抽象为发件箱和收件箱。

    \n

    联系人列表

    一般 IM 系统还需要一个最近联系人列表,来让互动双方快速查找需要聊天的对象,联系人列表一般还会携带两人最近一条聊天消息用于展示。

    \n

    \n

    继续以 123 给 456 发消息为例,除了在内容表和索引表插入记录,还会更新各自的最近联系人表。上图中我们将 用户 ID=123 && 另一方用户 ID=456用户 ID=456 && 另一方用户 ID=123 的两行数据中的最后一条消息 ID 字段更新为 1024。为了便于客户端排序和展示,很多时候我们还会在最近聊系人表中冗余其他信息,如最后一条消息时间。

    \n

    在大部分业务场景中,如果 123 是第一次给 456 发消息,会在发送消息的时候通过其他数据(如互关、好友等)校验双方好友状态,校验通过后给双方创建出联系人记录,这一点 TT 和常规做法略有区别,后文会做介绍。

    \n

    联系人列表和消息索引表的区别如下:

      \n
    • 联系人列表只更新存储收发双方的最新一条消息,不存储两人所有的历史消息
    • \n
    • 联系人表的使用场景用于查询某一个人最近的所有联系人,是用户全局维度
    • \n
    • 消息索引表的使用场景一般用于查询收发双方的历史聊天记录,是聊天会话维度
    • \n
    • 收发一条消息时
        \n
      • 联系人列表为更新操作
      • \n
      • 消息索引表为插入操作
      • \n
      \n
    • \n
    \n

    TT 中的 IM 存储设计

    消息表

    TT 中将索引表和内容表进行了合并成了一张消息表,同时通讯录表承担了更多的工作。

    \n

    \n

    要查询 123 和 321 之间的聊天记录时,使用 WHERE ((user_id=123 AND other_user_id=456) OR (user_id=456 AND other_user_id=123)) 条件来进行查询。

    \n

    这样的好处是要维护的表和冗余的数据更少一些,之前一个 索引表+一个内容表 的形式,每发送一条消息,不算联系人表更新的话,要有3次插入操作:1次内容表插入,2次索引表插入。

    \n

    为什么索引表是2次插入,而不是用1次插入来同时写两条数据?考虑到数据量,大部分情况下,索引表会根据主态的用户 ID 进行分片存储,收发双方的数据大概率不在同一分片上,进而导致无法通过一条语句写入两条数据。将索引和内容表合并后,只需要插入1条数据,对于单方可见的消息,我们在消息的一个额外字段中进行描述即可。

    \n

    架构本质上是一个需要权衡的过程,这种模式有优点的同时也有缺点。最大的缺点就是使用不够灵活,索引效率不够高效。另一个缺点是发送系统提示类的消息时,只能通过关联双方 ID 来展示在双方的聊天列表中,且同一条系统消息无法复用。

    \n

    使用合并后这种方式,在底层消息分片存储上也相对更复杂一些。有索引表的情况下,我们拉取一个用户和另一个用户的记录,不管是收发消息,只需要根据 userID + otherUserID 进行查询就够了。刚刚也提到,这种情况下我们可以将索引数据按照 userID 进行分片,一个用户所有索引数据落在同一个分片上,消息内容表根据消息ID 进行分片。获取到消息索引数据后,根据数据中的消息 ID 进行点查询来获取消息内容,效率很高。

    \n

    将索引表和内容表合并后,考虑到收发双法都会使用同一份数据,所以不建议使用用户 ID 进行分片,而是依然用消息 ID 分片,然后将发送方 ID 和接收方 ID 做一个联合索引。在查询双方聊天记录时,需要业务并行查询所有分片节点,然后在内存中进行排序。在我们的场景中,公司将 RocketDB 进行了二次封装,实现了一个自研的高性能关系数据库:TTDB,此类查询在 DB 中完成,不需要在业务代码内处理这个情况,查询效率也很高。TTDB 涉及很多 DB 方面的底层架构,超出了我的知识边界,就不过多进行介绍了。

    \n

    会话表

    上文中介绍的联系人表在 TT 中叫 Conversation:会话,两个用户能否发消息就判断两个用户之间有没有会话记录。

    \n

    会话是在用户形成互关、配对等可以聊天的关系时,由内部服务提前创建好的。与常规方式不同,常规方式是在用户建第一次发消息时创建联系人。

    \n

    当一个用户给另一个用户发消息时,我们只需要校验有没有会话,没有会话就认为这是一个无效请求,直接拒绝掉。

    \n

    联系人表的必要性

    最后思考一个问题,在有消息索引表或者 TT 中的消息表的情况下,为什么还需要联系人表(或会话表)?

    \n

    联系人表或者会话表的必要性主要考虑以下几点:

    \n

    1. 方便按最后聊天时间列出所有最近联系过的人

    回想一下微信中的聊天列表页,列表中的顺序按照两个用户间最后一条消息时间进行倒排,同时展示最后一条消息内容。如果没有联系人表,我们需要通过消息索引表按照 userID 和 otherUserID 进行分组(group by),然后按时间倒排取(order by)第一条,性能会非常差。

    \n

    2. 消息未读数维护

    正常来说每条消息是有已读、未读字段的,如果要统计未读消息的数量,确实可以通过 SQL 进行未读消息的 count 来得到,但这样也是效率很差,通常的做法是在联系人表上冗余一个未读数字段。

    \n

    3. 聊天 cell 的单独控制

    举个例子,在微信中我可以将对方置为隐藏或删除,在没有联系人表的情况下很难实现。

    \n"},{"title":"JavaScript 查漏补缺","url":"/2016/JavaScript-%E6%9F%A5%E6%BC%8F%E8%A1%A5%E7%BC%BA/","content":"

    实际上,JavaScript允许对任意数据类型做比较:

    \n
    false == 0; // true
    false === 0; // false
    \n

    要特别注意相等运算符==。JavaScript在设计时,有两种比较运算符:

    \n

    第一种是==比较,它会自动转换数据类型再比较,很多时候,会得到非常诡异的结果;

    \n

    第二种是===比较,它不会自动转换数据类型,如果数据类型不一致,返回false,如果一致,再比较。

    \n

    由于JavaScript这个设计缺陷,不要使用==比较,始终坚持使用===比较。

    \n

    另一个例外是NaN这个特殊的Number与所有其他值都不相等,包括它自己:

    \n
    NaN === NaN; // false
    \n

    唯一能判断NaN的方法是通过isNaN()函数:

    \n
    isNaN(NaN); // true
    \n
    \n

    在其他语言中,也有类似JavaScript的null的表示,例如Java也用null,Swift用nil,Python用None表示。但是,在JavaScript中,还有一个和null类似的undefined,它表示“未定义”。

    \n

    JavaScript的设计者希望用null表示一个空的值,而undefined表示值未定义。事实证明,这并没有什么卵用,区分两者的意义不大。大多数情况下,我们都应该用nullundefined仅仅在判断函数参数是否传递的情况下有用。

    \n
    \n

    strict模式

    \n

    JavaScript在设计之初,为了方便初学者学习,并不强制要求用var申明变量。这个设计错误带来了严重的后果:如果一个变量没有通过var申明就被使用,那么该变量就自动被申明为全局变量:

    \n
    i = 10; // i现在是全局变量
    \n

    在同一个页面的不同的JavaScript文件中,如果都不用var申明,恰好都使用了变量i,将造成变量i互相影响,产生难以调试的错误结果。

    \n

    使用var申明的变量则不是全局变量,它的范围被限制在该变量被申明的函数体内(函数的概念将稍后讲解),同名变量在不同的函数体内互不冲突。

    \n

    为了修补JavaScript这一严重设计缺陷,ECMA在后续规范中推出了strict模式,在strict模式下运行的JavaScript代码,强制通过var申明变量,未使用var申明变量就使用的,将导致运行错误。

    \n

    启用strict模式的方法是在JavaScript代码的第一行写上:

    \n
    'use strict';
    \n

    这是一个字符串,不支持strict模式的浏览器会把它当做一个字符串语句执行,支持strict模式的浏览器将开启strict模式运行JavaScript。

    \n\n
    \n

    unshiftshift

    \n

    如果要往Array的头部添加若干元素,使用unshift()方法,shift()方法则把Array的第一个元素删掉:

    \n
    var arr = [1, 2];
    arr.unshift('A', 'B'); // 返回Array新的长度: 4
    arr; // ['A', 'B', 1, 2]
    arr.shift(); // 'A'
    arr; // ['B', 1, 2]
    arr.shift(); arr.shift(); arr.shift(); // 连续shift 3次
    arr; // []
    arr.shift(); // 空数组继续shift不会报错,而是返回undefined
    arr; // []
    \n
    \n

    splice()方法是修改Array的“万能方法”,它可以从指定的索引开始删除若干元素,然后再从该位置添加若干元素:

    \n
    var arr = ['Microsoft', 'Apple', 'Yahoo', 'AOL', 'Excite', 'Oracle'];
    // 从索引2开始删除3个元素,然后再添加两个元素:
    arr.splice(2, 3, 'Google', 'Facebook'); // 返回删除的元素 ['Yahoo', 'AOL', 'Excite']
    arr; // ['Microsoft', 'Apple', 'Google', 'Facebook', 'Oracle']
    // 只删除,不添加:
    arr.splice(2, 2); // ['Google', 'Facebook']
    arr; // ['Microsoft', 'Apple', 'Oracle']
    // 只添加,不删除:
    arr.splice(2, 0, 'Google', 'Facebook'); // 返回[],因为没有删除任何元素
    arr; // ['Microsoft', 'Apple', 'Google', 'Facebook', 'Oracle']
    \n
    \n

    由于多行字符串用\\n写起来比较费事,所以最新的ES6标准新增了一种多行字符串的表示方法,用...表示:

    \n
    `这是一个
    多行
    字符串`;
    \n
    \n

    for … in

    \n

    for循环的一个变体是for ... in循环,它可以把一个对象的所有属性依次循环出来:

    \n
    var o = {
    name: 'Jack',
    age: 20,
    city: 'Beijing'
    };
    for (var key in o) {
    alert(key); // 'name', 'age', 'city'
    }
    \n

    要过滤掉对象继承的属性,用hasOwnProperty()来实现:

    \n
    var o = {
    name: 'Jack',
    age: 20,
    city: 'Beijing'
    };
    for (var key in o) {
    if (o.hasOwnProperty(key)) {
    alert(key); // 'name', 'age', 'city'
    }
    }
    \n

    由于Array也是对象,而它的每个元素的索引被视为对象的属性,因此,for ... in循环可以直接循环出Array的索引:

    \n
    var a = ['A', 'B', 'C'];
    for (var i in a) {
    alert(i); // '0', '1', '2'
    alert(a[i]); // 'A', 'B', 'C'
    }
    \n

    请注意,for ... inArray的循环得到的是String而不是Number

    \n
    \n

    iterable内置的forEach方法,它接收一个函数,每次迭代就自动回调该函数。以Array为例:

    \n
    var a = ['A', 'B', 'C'];
    a.forEach(function (element, index, array) {
    // element: 指向当前元素的值
    // index: 指向当前索引
    // array: 指向Array对象本身
    alert(element);
    });
    \n

    SetArray类似,但Set没有索引,因此回调函数的前两个参数都是元素本身:

    \n
    var s = new Set(['A', 'B', 'C']);
    s.forEach(function (element, sameElement, set) {
    alert(element);
    });
    \n

    Map的回调函数参数依次为valuekeymap本身:

    \n
    var m = new Map([[1, 'x'], [2, 'y'], [3, 'z']]);
    m.forEach(function (value, key, map) {
    alert(value);
    });
    \n

    如果对某些参数不感兴趣,由于JavaScript的函数调用不要求参数必须一致,因此可以忽略它们。例如,只需要获得Arrayelement

    \n
    var a = ['A', 'B', 'C'];
    a.forEach(function (element) {
    alert(element);
    });
    \n

    待续

    ","categories":["笔记"],"tags":["JavaScript"]},{"title":"免登陆查看 Kibana Dashboard","url":"/2020/Kibana-dashboard-auto-authenticating/","content":"
    \"\"
    \n\n

    7.x 中,elastic 公司开放了 x-pack 的认证功能,所以我们可以对 Kibana 的使用也进行登陆认证,保障了系统的安全性。

    \n
    \"\"
    \n\n

    这样导致的问题是,我们将 Kibana 中创建好的报表通过 iFrame 的方式嵌入到其他系统中后,运营人员在查看报表时也需要进行登陆,有没有什么办法可以不登陆就查看报表呢?

    \n

    可以通过 Nginx,将拼好的 Authorization 请求头传递给 Kibana 服务。

    \n

    请求头的生成策略是:

    \n
    base64(用户名:密码)
    \n

    Nginx 示例配置:

    \n
    server {
    listen 15601;
    server_name 127.0.0.1;

    location / {
    proxy_pass http://<实际的 kibana IP>:5601;
    proxy_set_header Host $host;
    proxy_set_header X-Real-IP $remote_addr;
    proxy_set_header REMOTE-HOST $remote_addr;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    # 将 base64 编码的 <用户名:密码> 传过去
    proxy_set_header Authorization "Basic cmVhZC1vbmx5OnRtIypkNCZyJXA0aGM=";
    }

    }
    \n

    建议新建一个只有 read-only 权限的角色和用户,用他的 token 来进行免登陆查看报表。

    \n

    通过 kibana 生成 iFrame 的嵌入代码后,只需将将里边的正常 kibana url 前缀部分替换为这个 Nginx 的地址和对应的端口号就可以了。

    \n

    TIP

    可以使用如下命令在 Linux 中生成 base64 编码:

    \n
    echo -n username:password | base64
    \n"},{"title":"Local Storage vs Session Storage","url":"/2017/Local-Storage-vs-Session-Storage/","content":"

    localStoragesessionStorage 都继承自 Storage。除了 sessionStorage 是为了做非持久化的目的,两者没有区别。

    \n

    意思是,数据会一直存储在 localStorage 中直到被明确删除。所做的修改将被保存并且对于当前和未来所有的访问都是有效的。

    \n

    对于 sessionStorage, 修改在每个窗口(或者 Chrome 和 Firefox 的 tab 中)下有效。所做的修改将被保存并且在当前页下有效,以及未来可以在当前窗口下访问。当窗口被关闭,存储就会被删除。

    \n"},{"title":"沟通的艺术第三章《沟通和认同:自我的塑造与展现》脑图","url":"/2020/Look-out-Look-in-ch3-mind-map/","content":"

    \"自我的塑造与展现\"

    \n

    为了更好的 SEO,把大纲放在下边。读者也可根据大纲自行绘制自己的脑图。

    \n
    沟通和自我
    \t自我概念与自尊
    \t\t定义
    \t\t\t自我概念是指你对自己所持有的相对稳定的知觉
    \t\t\t自尊是你对自我价值的评估
    \t\t举例
    \t\t\t安静
    \t\t\t\t「我简直是个懦夫,所以才会说不出话来」
    \t\t\t\t「我享受倾听甚于讲话」
    \t\t\t好辩
    \t\t\t\t「我太强势了,一定很惹人厌」
    \t\t\t\t「我在自己的信念上坚定不移」
    \t\t\t自律
    \t\t\t\t「我太小心翼翼了」
    \t\t\t\t「在我开口说或动手之前,我总会深思熟虑」
    \t\t影响
    \t\t\t高自尊的人倾向于认为别人是好的,并且期待被他们接受
    \t\t\t低自尊的人认为所有人都一直用批判的眼光看待他们
    \t自我的生物性和社会性根源
    \t\t说明
    \t\t\t生物性:人格在很大程度上是由基因决定的。
    \t\t\t社会化:从我们相关的人那里得到的信息在塑造我们的自我概念时扮演了重要的角色
    \t\t自我概念的形成
    \t\t\t反应评价:我们每个人得出的自我概念反应的是我们认为别人看待我们的方式。
    \t\t\t重要他人:你的自我评价或评价标准可能因为他人对待你的方式而受到影响。
    \t\t\t社会比较:依据他人的对照方式评估自己
    \t自我概念的特征
    \t\t特征
    \t\t\t主观:我们倾向于相信我们的自我概念是准确的,但事实上它很可能被歪曲了。
    \t\t\t抗拒改变:我们倾向于坚持一个现存的自我概念,即使有证据显示它是过时的。
    \t\t接受正面自我的建议
    \t\t\t对自己有真实的认知
    \t\t\t有切合实际的期望
    \t\t\t有改变的意愿
    \t\t\t有改变的技巧
    \t文化、性别和认同
    \t\t文化:孕育我们长大的文化以不着痕迹的方式塑造着我们对自我的理解
    \t\t\t大部分西方文化都是高度个人主义的
    \t\t\t大部分的亚洲人是倾向于集体主义的
    \t\t性征和性别:性别角色和性别标签对男人和女人如何看待自己以及如何沟通有着深刻影响。
    \t自我应验预言和沟通
    \t\t定义:指如果个体对事件的发生有所预期,并且他接下来的行为是建立在这些预期上的,那么这件事的发生会比没有预期更可能成真。
    \t\t类型
    \t\t\t自我强加的预言:指的是你的自我期待对你的行为产生影响
    \t\t\t他人强加的预言

    沟通作为印象管理
    \t公开自我和隐私自我
    \t\t觉知的自我是指你在真诚的自省过程中所相信的自己。(隐私的)
    \t\t展现的自我是我们想要别人如何看待我们。(公开的)
    \t印象管理的特征
    \t\t多重身份
    \t\t合作式的:某一刻发生的事,是由沟通双方及其在长期的交往中积累的经验共同导致的。
    \t\t深思熟虑或不知不觉:大部分人都在有意识或无意识地,以一种有助于建立在自己和他人眼中的理想身份的方式进行沟通。
    \t为什么要印象管理?
    \t\t为了开始和经营关系
    \t\t为了获得别人的顺从
    \t\t为了保住别人的颜面
    \t\t为了探索新的自我
    \t面对面印象管理
    \t\t举止,由一个沟通者的预言和非语言行为组成
    \t\t外貌,是人们用来塑造印象的个人化方式
    \t\t配置,即我们用来影响别人如何看待我们的物理工具
    \t网络印象管理
    \t印象管理和诚实
    \t\t印象管理意味着选择自己的哪个角色或者哪个部分加以展现

    在关系中的自我坦露
    \t自我坦露的模式
    \t\t社会穿透
    \t\t\t广度
    \t\t\t深度
    \t\t乔哈里视窗
    \t\t\t开放区
    \t\t\t盲视区
    \t\t\t隐藏区
    \t\t\t未知区
    \t自我坦露的好处与风险
    \t\t好处
    \t\t\t宣泄:可以提供心理上和情感上的双重慰藉
    \t\t\t互惠:一个自我袒露的行为会引发另一个自我袒露行为
    \t\t\t自我澄清:通过和他人谈论你的信念、意见、想法、态度和感觉,可以理清你对于这些话题的看法。
    \t\t\t自我确认:旨在确认你自我概念中的重要组成部分
    \t\t\t关系的建立和维持:开始一段关系是需要一定程度的自我袒露的
    \t\t\t社会控制:袒露个人信息会增加你对他人的控制,有时也会增加你对情境的控制
    \t\t风险
    \t\t\t拒绝
    \t\t\t负面印象
    \t\t\t降低关系满意度
    \t\t\t丧失影响力
    \t\t\t伤害别人
    \t自我坦露的指导原则
    \t\t这个人对你而言重要吗?
    \t\t坦露的方式合适吗?
    \t\t坦露的风险合理吗?
    \t\t有建设性影响吗?
    \t\t你的自我坦露是互惠的吗?
    \t\t你在道德上有义务坦露吗?

    自我坦露的替代选择
    \t沉默:将自己的想法与感受保留在心中
    \t说谎:善意的谎言被定义为对被告知的人来说是没有恶意的,甚至是有帮助的
    \t模棱两可:当面对是说谎还是说出一个令人不愉快的真相的困境时,沟通者常常会选择一种模棱两可的回答
    \t暗示:暗示要比模棱两可更直接。这是因为模棱两可的说法不要求改变他人的行为,而暗示确实旨在从他人那里得到期待的回应。

    指有意透漏与自己相关信息的过程,而且这些信息通常是重要的、不为人所知的。

    四个步骤
    \t1. 持有某种期待
    \t2. 表现出与期望一致的行为
    \t3. 期待如是发生
    \t4. 强化起初的期待

    原因
    \t过期的信息
    \t歪曲的回馈
    \t完美主义
    \t社会期待

    @Panmax
    \n","tags":["阅读"]},{"title":"沟通的艺术第八章《倾听:不止是听见》脑图","url":"/2020/Look-out-Look-in-ch8-mind-map/","content":"

    \"倾听\"

    \n

    为了更好的 SEO,把大纲放在下边。读者也可根据大纲自行绘制自己的脑图。

    \n
    定义
    \t听与倾听
    \t\t听 是声音传到耳膜引起振动后经听觉传送到大脑的过程
    \t\t倾听 是大脑将电化学脉冲重构为原始声音的再现,再赋予其意义的过程
    \t心不在焉的倾听
    \t\t发生在我们自动地或常规地回应别人的信息时
    \t心无旁骛的倾听
    \t\t对我们接收到的信息给予仔细而审慎的专注和反应

    五个要素
    \t听到
    \t\t生理维度
    \t专注
    \t\t心理过程
    \t理解
    \t\t发生在我们弄懂信息的意思时
    \t回应
    \t\t对说话者给予明显的反馈
    \t\t好的倾听者会使用非语言行为来表现他们的专注
    \t记忆
    \t\t记住信息的能力

    挑战
    \t无效的倾听类型
    \t\t虚伪地倾听
    \t\t\t虚伪地倾听者看上去是很专注的,但专注的样子只是礼貌的假象
    \t\t自恋地倾听
    \t\t\t设法把谈话的主题转移到他们自己身上
    \t\t\t\t说话者:「我的数学课程真的很难」

    自恋倾听者:「你认为你的数学难?那你应该来上一下我的物理课」
    \t\t\t另一个特点是打断别人说话
    \t\t选择性倾听
    \t\t\t只会针对他们感兴趣的部分回应
    \t\t隔绝性倾听
    \t\t\t与选择性倾听者相反,这类倾听者避免听到某些信息
    \t\t防卫性倾听
    \t\t\t认为别人说的话都是在攻击自己
    \t\t埋伏性倾听
    \t\t\t仔细地倾听说话者说的话只是为了搜集信息,以便借此攻击说话者的言论
    \t\t愚钝性倾听
    \t\t\t对说话者信息的表面内容做出反应,却漏掉了说话者没有直接表达出来的更为重要的情绪性信息。
    \t导致无效倾听的原因
    \t\t超负荷的信息
    \t\t先入为主
    \t\t\t我们通常会先将注意力放在自己关心的问题上
    \t\t飞快的思维
    \t\t\t倾听速度比说话速度快得多,当别人说话时我们便有了很多「多余时间」
    \t\t\t有效倾听的技巧就是利用「多余时间」来更好地理解说话者的想法,而不是让自己的注意力漫游
    \t\t费力
    \t\t\t仔细倾听别人说话所耗费的心力不亚于一次锻炼
    \t\t外在噪音
    \t\t错误假定
    \t\t\t有时候我们会假定说话者的想法太简单、太浅显,不值得我们付出注意力,然而事实可能正好相反
    \t\t缺乏明显的益处
    \t\t\t通常我们认为说话可以比倾听带来更多好处
    \t\t缺乏训练
    \t\t\t倾听和说话一样都需要技巧
    \t\t听力问题
    \t有效倾听技巧
    \t\t少说话
    \t\t\t避免自恋地倾听或一味地把话题转移到自己地想法上
    \t\t\t少说话并不意味着必须保持沉默
    \t\t摆脱注意力分散
    \t\t\t如果你要搜集地信息真的很重要,那你应该尽一切可能去消除那些会让你分心的内在和外在干扰
    \t\t\t\t关掉电视
    \t\t\t\t关闭手机
    \t\t\t\t安静的房间
    \t\t不要过早评断
    \t\t\t在理解别人说话的意思之前不要过早下评断
    \t\t\t确定你真正理解对方地所有意思后,再去评论
    \t\t寻找重点
    \t\t\t利用思考的速度比说话速度更快的能力,从听到的话中提取出对方的核心观点

    回应方式
    \t借力使力
    \t\t使用沉默和简短的言论来鼓励对方多说一些话
    \t\t帮助你更好地理解说话者
    \t\t帮助说话者弄清楚他们的想法和感觉
    \t问话
    \t\t帮助提问者
    \t\t\t更加清楚对方的想法和感受
    \t\t\t对事实和细节有更深入的理解
    \t\t\t清楚对方想法和感受,以及可能的期望
    \t\t帮助回答者
    \t\t\t有助于自我坦露
    \t\t\t了解他自己的各种期望和需要
    \t\t虚伪的问话类型
    \t\t\t给说话者设圈套
    \t\t\t\t「你不喜欢那部电影是吗?」
    \t\t\t附加问句
    \t\t\t\t「难道你不认为他会成为一个好老板吗?」
    \t\t\t\t「你说你会在5点钟打电话过来,但是你却忘了,不是吗?」
    \t\t\t实为陈述
    \t\t\t\t「你终于挂掉电话了?」
    \t\t\t\t「你借钱给托尼了?」
    \t\t\t\t「你会勇敢地面对他,让他接受应有的惩罚吗?」
    \t\t\t带有隐蔽计划
    \t\t\t\t「你星期五晚上忙吗?」
    \t\t\t\t「你会帮我吗?」
    \t\t\t\t「如果我告诉你发生了什么,你能保证不生气吗?」
    \t\t\t寻求「正确」答案
    \t\t\t\t「你觉得我应该穿哪双鞋?」
    \t\t\t\t「亲爱的,你觉得我看起来胖吗?」
    \t\t\t基于未经核实的假设
    \t\t\t\t「你为什么不听我说?」
    \t\t\t\t「出什么事了?」
    \t释义
    \t\t倾听者将自己解读的信息重说一次
    \t\t\t说话者:「我是很想去,可是我怕我负担不起。」

    释义式回应:「所以如果我们能一起想想办法,帮助你负担这笔钱,你就愿意和我们一起去了,是这样吗?」
    \t\t\t说话者:「天哪!你看起来真是有点糟糕!」

    释义式回应:「你是不是觉得我看起来胖太多了?」
    \t\t两个层次
    \t\t\t事实性信息
    \t\t\t\t「所以你是这周二开会,不是下周二,对吗?」
    \t\t\t个人性信息
    \t\t\t\t「所以,我的玩笑让你以为我不在乎你的问题?」
    \t\t三种方法
    \t\t\t改变说话者的修辞
    \t\t\t\t说话者:「双语教育只不过是另一个既失败又浪费钱的政策」

    释义者:「你看看我理解的对不对,你很生气是因为你觉得双语教育听起来很棒,但实际上却没什么作用,对吗?」
    \t\t\t举出一个例子
    \t\t\t\t实话者:「李是一个浑蛋,我真不敢相信他昨晚所做的事!」

    释义者:「你觉得那些笑话很惹人厌,对吗?」
    \t\t\t反应说话者的潜在寓意
    \t\t\t\t释义者:「你一直提醒我要小心,听起来好像你在担心会有事发生在我身上,会吗?」
    \t\t考虑因素
    \t\t\t这个问题够复杂吗?
    \t\t\t对你来说,有必要投入时间和关注吗?
    \t\t\t你能克制住不去评判吗?
    \t\t\t释义和你的其他倾听反应成比例吗?
    \t支持
    \t\t支持性回应就是听者表明自己和说话者立场一致
    \t\t分类
    \t\t\t同理心
    \t\t\t\t「我可以理解你为什么会这么沮丧」
    \t\t\t\t「是啊,这门课对我来说也很困难」
    \t\t\t同意
    \t\t\t\t「你说得对,房东真的很不公平」
    \t\t\t\t「听起来那份工作很适合你」
    \t\t\t提供协助
    \t\t\t\t「如果你需要我的话,我就在这里」
    \t\t\t\t「如果你喜欢,我很乐意下次考前再和你一起温习」
    \t\t\t赞美
    \t\t\t\t「哇,你做得真好!」
    \t\t\t\t「你是一个很好的人,如果她认识不到这一点,那是她的问题」
    \t\t\t恢复信心
    \t\t\t\t「最糟糕的情况已经结束了,从现在开始一切都会好转的」
    \t\t\t\t「我确定你会做得很好」
    \t\t无效的支持性反应
    \t\t\t否认别人拥有某种感觉的权力
    \t\t\t\t「不用担心」
    \t\t\t\t「这又没什么,不值得你这样难过」
    \t\t\t\t「你这样真的很好笑」
    \t\t\t看轻事情的重要性
    \t\t\t\t「嘿,那就只是……而已」
    \t\t\t聚焦在「彼时彼地」,而非「此时此地」
    \t\t\t\t「十年后你会连她的名字也记不起来」
    \t\t\t火上浇油的评断
    \t\t\t\t「你知道吗?那是你的错!当初你就不应该这么做」
    \t\t\t自我聚焦
    \t\t\t\t「我绝对理解你现在的感受,因为我也遇到过这种情况……」
    \t\t\t自我防卫
    \t\t\t\t「不要怪我!我已经做完我要做的那部分了。」
    \t\t有效的支持性参考原则
    \t\t\t要认识到你可以支持他人的努力,而不必赞同他的决定
    \t\t\t监控对方对你的支持性回应的反应
    \t\t\t要认识到支持也不总会受欢迎
    \t\t\t确保你对后果已经做好了准备
    \t分析
    \t\t倾听者对说话者的信息提供一种解释
    \t\t\t「我想真正困扰你的是……」
    \t\t\t「她已经在做了,因为……」
    \t\t\t「我认为你不是真的那样想。」
    \t\t\t「也许这个问题开始于他……」
    \t\t潜在问题
    \t\t\t解释可能不正确
    \t\t\t即使分析是正确的,告诉对方也不一定有用
    \t\t提出分析时遵循的原则
    \t\t\t最好用试探的而非绝对的口吻
    \t\t\t\t「也许这个问题的原因是……」
    \t\t\t\t「这个问题在我看来可能是……」
    \t\t\t确定对方愿意接受你的分析
    \t\t\t确定你提出分析的动机真的是出于帮助对方
    \t忠告
    \t\t通过提供解决方案来帮助对方
    \t\t注意事项
    \t\t\t这个忠告有必要吗?
    \t\t\t\t「我不能相信你竟然和他一起回来了」
    \t\t\t对方真的想听你的忠告吗?
    \t\t\t\t有时人们想要的只是一双倾听的耳朵,而不是他们问题的解决方案
    \t\t\t你提出劝告的方式正确吗?
    \t\t\t\t在提供劝告之前了解实情
    \t\t\t你的忠告是专家级别的吗?
    \t\t\t\t如果你不具备相关的专业知识,那你最好给说话者提供一些支持性回应,然后鼓励他向专家寻求建议
    \t\t\t提出忠告的人是关系密切、值得信任的人吗?
    \t\t\t你提出忠告的态度是谨慎、顾全对方面子的吗?
    \t评断
    \t\t用某种方式去评价信息发送者的想法或行为
    \t\t\t可能是讨人喜欢的
    \t\t\t\t「你的意见真棒!」
    \t\t\t\t「现在你正走在正确的道路上」
    \t\t\t也可能是不讨人喜欢的
    \t\t\t\t「你这样的态度是不会有什么好结果的」
    \t\t\t有时纯粹是为了批评别人
    \t\t\t\t「我早就告诉过你了」
    \t\t\t\t「你真该为你自己感到惭愧」
    \t\t\t\t有些是「建设性评价」,目的是希望能够让对方在未来有更好的进步,例如好朋友之间相互机遇的建设性评价
    \t\t评判被接受的两个条件
    \t\t\t当身处困境的人向你寻求判断时
    \t\t\t你判断的动机是真诚的、有建设性的,而不是为了奚落对方

    选择回应方式要考虑的因素
    \t性别
    \t\t男人和女人在倾听和回应方式上都存在差异
    \t\t\t女性经常提供情感支持的回应
    \t\t\t男性习惯通过评断对方的态度和价值来提供协助
    \t情境
    \t\t沟通者需要去分析情境,发展出合适的反应
    \t\t规则
    \t\t\t开头时使用寻求理解并提供最少指示的回应:借力使力、问话、释义、支持
    \t\t\t一旦收集到足够的实情,并且展现出了你的兴趣和关切,此时说话者更有可能接受你的分析、忠告和评断回应
    \t对象
    \t\t根据对象的不通而调整你的反应
    \t\t找出最适合的回应方式的一个办法是直接询问对方他想要你做什么
    \t\t\t「你是要听我的忠告,还是只需要吐吐苦水?」
    \t你的个人风格
    \t\t当你思考如何回应对方的信息时,最好同时反省一下自己的优缺点,然后做出相应的调整

    关键在于你要用自己的措辞重述别人的观点

    相信别人有能力思考他们自己的问题

    具有一种潜在价值:能让我们自由地将心思聚集在需要我们小心注意地的信息上

    自由主题

    暗示了一个事实:你是那个有权利和资格去评判说话者想法或行为的人

    因为分析就是在暗示你比对方优秀,看得比他透彻

    @Panmax
    \n","tags":["阅读"]},{"title":"Mac下MySQL安装与卸载","url":"/2016/Mac%E4%B8%8BMySQL%E5%AE%89%E8%A3%85%E4%B8%8E%E5%8D%B8%E8%BD%BD/","content":"

    写这篇博客的原因是,我刚用 Mac 的时候 MySQL 是在官网下载的安装包进行的安装,那时候不知道有 Homebrew 这个神器。用官方的包安装好后又做何很多的配置最终才能正常使用。但还是有不少问题,比如无法在终端启动MySQL,只能在系统偏好里,通过官方的启动器启动。

    \n

    后来有很长一段时间没有用过MySQL,今天再用的时候发现无法启动了,于是就把之前的进行了卸载,重新用Homebrew安装了一遍。

    \n

    卸载过程:

    sudo rm /usr/local/mysql
    sudo rm -rf /usr/local/mysql*
    sudo rm -rf /Library/StartupItems/MySQLCOM
    sudo rm -rf /Library/PreferencePanes/My*
    sudo vim /etc/hostconfig 按a进入编辑模式 然后手动删除 MYSQLCOM=-YES- 然后按Esc退出编辑模式 输入:wq!保存并退出
    rm -rf ~/Library/PreferencePanes/My*
    sudo rm -rf /Library/Receipts/mysql*
    sudo rm -rf /Library/Receipts/MySQL*
    sudo rm -rf /var/db/receipts/com.mysql.*
    \n

    安装过程:

    brew install mysql
    export PATH=$PATH:/usr/local/mysql/bin
    \n

    启动:

    export PATH=$PATH:/usr/local/mysql/bin
    mysql.server start
    \n

    首次登录:

    mysql -uroot
    ","categories":["Mac"],"tags":["MySQL"]},{"title":"Mac安装MySqldb和pylibmc时遇到的问题","url":"/2016/Mac%E5%AE%89%E8%A3%85MySqldb%E5%92%8Cpylibmc%E6%97%B6%E9%81%87%E5%88%B0%E7%9A%84%E9%97%AE%E9%A2%98/","content":"

    今天在 Mac 上安装 MySqldb 和 pylibmc 的时候遇到两个问题,在这里记录一下。

    \n

    安装 MySqldb 时,报:

    \n
    Failed building wheel for MySQL-python
    Running setup.py clean for MySQL-python
    Failed to build MySQL-python
    Installing collected packages: MySQL-python
    Running setup.py install for MySQL-python ... error
    Complete output from command /Users/jiapan/PycharmProjects/hodoor/venv/bin/python -u -c "import setuptools, tokenize;__file__='/private/var/folders/9j/c68zmjy53fq4t0j5y82wphr40000gn/T/pip-build-HrfNXN/MySQL-python/setup.py';exec(compile(getattr(tokenize, 'open', open)(__file__).read().replace('\\r\\n', '\\n'), __file__, 'exec'))" install --record /var/folders/9j/c68zmjy53fq4t0j5y82wphr40000gn/T/pip-DdwCnb-record/install-record.txt --single-version-externally-managed --compile --install-headers /Users/jiapan/PycharmProjects/hodoor/venv/bin/../include/site/python2.7/MySQL-python:
    running install
    running build
    running build_py
    creating build
    creating build/lib.macosx-10.11-x86_64-2.7
    copying _mysql_exceptions.py -> build/lib.macosx-10.11-x86_64-2.7
    creating build/lib.macosx-10.11-x86_64-2.7/MySQLdb
    copying MySQLdb/__init__.py -> build/lib.macosx-10.11-x86_64-2.7/MySQLdb
    copying MySQLdb/converters.py -> build/lib.macosx-10.11-x86_64-2.7/MySQLdb
    copying MySQLdb/connections.py -> build/lib.macosx-10.11-x86_64-2.7/MySQLdb
    copying MySQLdb/cursors.py -> build/lib.macosx-10.11-x86_64-2.7/MySQLdb
    copying MySQLdb/release.py -> build/lib.macosx-10.11-x86_64-2.7/MySQLdb
    copying MySQLdb/times.py -> build/lib.macosx-10.11-x86_64-2.7/MySQLdb
    creating build/lib.macosx-10.11-x86_64-2.7/MySQLdb/constants
    copying MySQLdb/constants/__init__.py -> build/lib.macosx-10.11-x86_64-2.7/MySQLdb/constants
    copying MySQLdb/constants/CR.py -> build/lib.macosx-10.11-x86_64-2.7/MySQLdb/constants
    copying MySQLdb/constants/FIELD_TYPE.py -> build/lib.macosx-10.11-x86_64-2.7/MySQLdb/constants
    copying MySQLdb/constants/ER.py -> build/lib.macosx-10.11-x86_64-2.7/MySQLdb/constants
    copying MySQLdb/constants/FLAG.py -> build/lib.macosx-10.11-x86_64-2.7/MySQLdb/constants
    copying MySQLdb/constants/REFRESH.py -> build/lib.macosx-10.11-x86_64-2.7/MySQLdb/constants
    copying MySQLdb/constants/CLIENT.py -> build/lib.macosx-10.11-x86_64-2.7/MySQLdb/constants
    running build_ext
    building '_mysql' extension
    creating build/temp.macosx-10.11-x86_64-2.7
    clang -fno-strict-aliasing -fno-common -dynamic -g -O2 -DNDEBUG -g -fwrapv -O3 -Wall -Wstrict-prototypes -Dversion_info=(1,2,5,'final',1) -D__version__=1.2.5 -I/usr/local/Cellar/mysql/5.7.12/include/mysql -I/usr/local/Cellar/python/2.7.11/Frameworks/Python.framework/Versions/2.7/include/python2.7 -c _mysql.c -o build/temp.macosx-10.11-x86_64-2.7/_mysql.o -fno-omit-frame-pointer
    _mysql.c:1589:10: warning: comparison of unsigned expression < 0 is always false [-Wtautological-compare]
    if (how < 0 || how >= sizeof(row_converters)) {
    ~~~ ^ ~
    1 warning generated.
    clang -bundle -undefined dynamic_lookup build/temp.macosx-10.11-x86_64-2.7/_mysql.o -L/usr/local/Cellar/mysql/5.7.12/lib -lmysqlclient -lssl -lcrypto -o build/lib.macosx-10.11-x86_64-2.7/_mysql.so
    ld: library not found for -lssl
    clang: error: linker command failed with exit code 1 (use -v to see invocation)
    error: command 'clang' failed with exit status 1
    \n

    产生原因,升级了Xcode,但是Xcode还没有启动过,里边的一些插件还没有升级。

    \n

    解决方法,启动Xcode,让Xcode完成升级。

    \n

    然后执行 xcode-select --install 等待完成后即可。

    \n

    UPDATE AT: 2018-05-02

    \n

    今天解决时问题时,上边的方法也不起作用了,stackoverflow 找到了一个方法:

    \n

    sudo env LDFLAGS="-I/usr/local/opt/openssl/include -L/usr/local/opt/openssl/lib" pip install MySQL-python

    \n

    第二个问题是再安装 pylibmc 的时候报

    \n
    ./_pylibmcmodule.h:42:10: fatal error: 'libmemcached/memcached.h' file not found

    #include <libmemcached/memcached.h>

    ^

    1 error generated.

    error: command 'clang' failed with exit status 1
    \n

    产生原因,Mac 使用 brew 安装的 memcached 的路径和 pylibmc 认为的默认路径不一致,所以需要指定一下路径。

    \n
    $ which memcached
    /usr/local/bin/memcached

    $ export LIBMEMCACHED=/usr/local
    $ pip install pylibmc
    \n","categories":["Code"],"tags":["Python"]},{"title":"《别让猴子跳回背上》摘抄","url":"/2021/Monkey-Business-excerpt/","content":"

    在遇到困难或问题时,员工总会寻找各种理由来证明不是自己的问题,然后将责任推到其他人或事上。道理也并不复杂,那就是人的本性中始终都在重复的一个永恒的主题:规避风险。

    \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

    管理者要正确针对自己的目标提出承诺,包含未来预定完成的时间表。

    \n

    陈述下一个步骤时,应该以可量化的语句来表达行动,这样执行必要措施时的模棱两可才会降低,而且表现才能改进。

    \n

    建立对员工的信赖时,最大的障碍是来自于你恐惧员工可以独当一面。

    \n

    尽可能在下属处理的范围内,给与对等的责任与行动自由。让他们独立工作,但在他们需要你协助喂食时,务必要在他们身边。

    \n

    只要你将猴子交付给某人,务必要排好追踪的会议时间表。这可让你将不预期的干预降至最低,掌控每天的行程。这也是管理猴子和确定它们跟好主人的惟一方法。

    \n

    喂养猴子的六大规则

    \n
      \n
    1. 要么喂养它们,要么射杀它们,千万不要让它们被活活饿死。
    2. \n
    3. 只要你找到需要喂养的猴子,你的下属就要找出时间喂养它们,但千万不要过量。
    4. \n
    5. 按照喂食进度表上的时间和地点喂养猴子是下属的责任,主管不必再沿途追逐即将饿死的猴子,胡乱地喂食。
    6. \n
    7. 如果有冲突发生,预定喂食猴子的时间可在任何一方的提议下做出更改,但不被视为延误;事情毫无进展不能作为重新安排喂食时间的借口。
    8. \n
    9. 无论何时,应尽可能面对面地喂养猴子,或者使用电话,绝对不要使用信件。备忘录、电子邮件、传真和报告可以适用于喂食过程,但不能替代面对面的对话。
    10. \n
    11. 超过好几页的备忘录、电子邮件、传真和报告应该在一页的摘要中写清楚,以便展开即时的对话。
    12. \n
    \n","tags":["摘抄"]},{"title":"早晨上班的动力","url":"/2023/Motivation-to-work/","content":"

    我上班有个习惯是比其他人至少早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":"

    UTF8:

    CREATE DATABASE `test2` DEFAULT CHARACTER SET utf8 COLLATE utf8_general_ci;
    \n

    GBK:

    create database test2 DEFAULT CHARACTER SET gbk COLLATE gbk_chinese_ci;
    ","categories":["MySql"],"tags":["MySql"]},{"title":"Neo4j 速查手册","url":"/2020/Neo4j-cheatsheet/","content":"

    基础概念

    图数据库中使用以下概念来存储数据:

    \n
      \n
    • 节点(Node):图数据记录
    • \n
    • 关系(Relationship):用来连接节点(拥有方向类型
    • \n
    • 属性(Property):在节点和关系中以键值对的形式存储数据
    • \n
    • 标签(Label):节点和关系的分组(可选)
    • \n
    \n

    Cypher

    匹配

    匹配节点

    MATCH (ee:Person)
    WHERE ee.name = "Emil"
    RETURN ee;
    \n
      \n
    • MATCH 子句指定节点和关系的模式
    • \n
    • (ee:Person) 带有 Person 标签的单节点模式,并将匹配项分配给变量 ee
    • \n
    • WHERE 子句用来对返回结果进行约束
    • \n
    • ee.name=”Emil”name 属性与 Emil 进行比较
    • \n
    • RETURN 用于请求特定结果的子句
    • \n
    \n

    匹配节点和关系

    MATCH (ee:Person)-[:KNOWS]-(friends)
    WHERE ee.name = "Emil"
    RETURN ee, friends
    \n
      \n
    • MATCH 子句描述从已知节点查找节点的模式
    • \n
    • (ee) 从标签为 Person 的节点开始模式
    • \n
    • -[:KNOWS]- 匹配 KNOWS 关系(方向不限)
    • \n
    • (friends) 绑定了 Emil 的朋友列表(认识的人)
    • \n
    \n

    匹配标签

    MATCH (n:Person)
    RETURN n
    \n

    或者

    \n
    MATCH (n)
    WHERE n:Person
    RETURN n
    \n

    匹配多个标签

    匹配 :Car :Person 标签

    \n
    MATCH (n)
    WHERE n:Person OR n:Car
    RETURN n
    \n

    匹配 :Car :Person 标签

    \n
    MATCH (n)
    WHERE n:Person:Car
    RETURN n
    \n

    匹配属性

    MATCH (a:Person)
    WHERE a.from = "Sweden"
    RETURN a
    \n

    返回属性 from 的值为 Sweden 的每个节点(和他们的关系)

    \n

    匹配有相同爱好的朋友

    Johan 正在学习冲浪,他想认识他的朋友中爱好冲浪的朋友

    \n
    MATCH (js:Person)-[:KNOWS]-()-[:KNOWS]-(surfer)
    WHERE js.name = "Johan" AND surfer.hobby = "surfing"
    RETURN DISTINCT surfer
    \n
      \n
    • () 空括号忽略这些节点
    • \n
    • DISTINCT 因为不止一条路径与模式匹配
    • \n
    \n

    ID 匹配

    每个节点都有一个内部的自增 ID,可以通过 <, <=, =, >=, <>IN 操作进行查询。

    \n

    通过 ID 查询

    \n
    MATCH (n)
    WHERE id(n) = 0
    RETURN n
    \n

    查询多个 ID

    \n
    MATCH (n)
    WHERE id(n) IN [1, 2, 3]
    RETURN n
    \n

    根据 ID 查询关系

    \n
    MATCH ()-[n]-()
    WHERE id(n) = 0
    RETURN n
    \n

    创建

    创建节点

    CREATE (ee:Person { name: "Emil", from: "Sweden", klout: 99 })
    \n
      \n
    • CREATE 子句用来创建数据
    • \n
    • () 圆括号用于表示节点
    • \n
    • ee:Person 将标签为 Person 的新节点赋值给 ee
    • \n
    • {} 花括号为节点添加属性(键值对)
    • \n
    \n

    创建节点和关系

    MATCH (ee:Person) WHERE ee.name = "Emil"
    CREATE (js:Person { name: "Johan", from: "Sweden", learn: "surfing" }),
    (ir:Person { name: "Ian", from: "England", title: "author" }),
    (rvb:Person { name: "Rik", from: "Belgium", pet: "Orval" }),
    (ally:Person { name: "Allison", from: "California", hobby: "surfing" }),
    (ee)-[:KNOWS {since: 2001}]->(js),(ee)-[:KNOWS {rating: 5}]->(ir),
    (js)-[:KNOWS]->(ir),(js)-[:KNOWS]->(rvb),
    (ir)-[:KNOWS]->(js),(ir)-[:KNOWS]->(ally),
    (rvb)-[:KNOWS]->(ally)
    \n
      \n
    • MATCH 子句将 Emil 赋给 ee
    • \n
    • CREATE 子句创建带有标签和属性的多个节点(用逗号分隔),同时创建了带有方向的关系 (a)-[:Label {key: value}]->(b)
    • \n
    \n

    在两个无关系的节点间新建关系

    MATCH (n), (m)
    WHERE n.name = "Allison" AND m.name = "Emil"
    CREATE (n)-[:KNOWS]->(m)
    \n

    或者使用 MERGE,这样可以确保关系只创建一次

    \n
    MATCH (n:User {name: "Allison"}), (m:User {name: "Emil"})
    MERGE (n)-[:KNOWS]->(m)
    \n

    创建带多个标签的节点

    CREATE (n:Actor:Director)
    \n

    更新

    更新节点属性(添加或修改)

    添加新的 owns 属性(如果已存在则执行修改)

    \n
    MATCH (n)
    WHERE n.name = "Rik"
    SET n.owns = "Audi"
    \n

    替换节点属性

    警告:如下操作会删除之前的属性并添加 playsage 属性

    \n
    MATCH (n)
    WHERE n.name = "Rik"
    SET n = {plays: "Piano", age: 23}
    \n

    批量添加新的节点属性(不删除老的)

    警告:如果 plays 或者 age 属性已经存在的情况下会被覆盖。

    \n
    MATCH (n)
    WHERE n.name = "Rik"
    SET n += {plays: "Piano", age: 23}
    \n

    属性不存在的情况下添加属性

    MATCH (n)
    WHERE n.plays = "Guitar" AND NOT (EXISTS (n.likes))
    SET n.likes = "Movies"
    \n

    为所有节点属性重命名

    MATCH (n)
    WHERE NOT (EXISTS (n.instrument))
    SET n.instrument = n.plays
    REMOVE n.plays
    \n

    或者

    \n
    MATCH (n)
    WHERE n.instrument is null
    SET n.instrument = n.plays
    REMOVE n.plays
    \n

    为现有节点添加标签

    给 id 为 7 和 8 的节点添加 :Food 标签

    \n
    MATCH (n)
    WHERE id(n) IN [7, 8]
    SET n:Food
    \n

    如果节点不存在,创建节点并更新(或添加)属性

    MERGE (n:Person {name: "Rik"})
    SET n.owns = "Audi"
    \n

    删除

    删除节点

    为了删除一个节点(比如,id=5),我们需要先删除他们的关系,然后才可以删除节点。

    \n
    MATCH (n)-[r]-()
    WHERE id(n) = 5
    DELETE r, n
    \n

    2.3+ 之后的简便写法:

    \n
    MATCH (n)
    WHERE id(n) = 5
    DETACH DELETE n
    \n

    删除指定节点的属性

    MATCH (n)
    WHERE n:Person AND n.name = "Rik" AND n.plays is NOT null
    REMOVE n.plays
    \n

    或者

    \n
    MATCH (n)
    WHERE n:Person AND n.name = "Rik" AND EXISTS (n.plays)
    REMOVE n.plays
    \n

    删除多个节点

    MATCH (n)
    WHERE id(n) IN [1, 2, 3]
    DELETE n
    \n

    删除全部节点上的标签

    从全部节点上删除 :Person 标签

    \n
    MATCH (n)
    REMOVE n:Person
    \n

    删除具有特定标签节点上的标签

    从带有 :Food:Person 标签的节点中删除 :Person 标签

    \n
    MATCH (n)
    WHERE n:Food:Person
    REMOVE n:Person
    \n

    删除节点中的多个标签

    从带有 :Food:Person 标签的节点中删除这两标签

    \n
    MATCH (n)
    WHERE n:Food:Person
    REMOVE n:Food:Person
    \n

    删除全部数据

    MATCH (n)
    OPTIONAL MATCH (n)-[r]-()
    DELETE n, r
    \n

    2.3+ 之后的简便写法:

    \n
    MATCH (n) DETACH DELETE n
    \n

    其他子句

    展示执行计划

    在查询语句前使用 PROFILE 或者 EXPLAIN

    \n

    PROFILE:显示执行计划,查询信息和数据库命中。如:Cypher version: CYPHER 3.0, planner: COST, runtime: INTERPRETED. 84 total db hits in 32 ms.

    \n

    EXPLAIN:显示执行计划和查询信息。如:Cypher version: CYPHER 3.0, planner: COST, runtime: INTERPRETED.

    \n

    Count

    全部节点数量

    \n
    MATCH (n)
    RETURN count(n)
    \n

    全部关系数量

    \n
    MATCH ()-->()
    RETURN count(*);
    \n

    Limit

    最多返回 2 个 from 属性值为 Sweden 的节点(及其关系)

    \n
    MATCH (a:Person)
    WHERE a.from = "Sweden"
    RETURN a
    LIMIT 2
    \n

    创建唯一属性约束

    使带有 Person 标签节点的 name 属性值唯一

    \n
    CREATE CONSTRAINT ON (n:Person)
    ASSERT n.name IS UNIQUE
    \n

    删除唯一属性约束

    DROP CONSTRAINT ON (n:Person)
    ASSERT n.name IS UNIQUE
    \n"},{"title":"投资组合理论 && 风险平价模型","url":"/2023/Portfolio-Theory-and-Risk-Parity-Model/","content":"

    投资组合理论和风险平价模型是两种与投资组合管理相关的重要概念,是金融领域中用于优化投资组合的方法。

    \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

    优点

      \n
    1. 风险分散:投资组合理论强调通过将不同资产以适当的权重组合在一起,实现风险的分散化,从而降低整体投资组合的风险。
    2. \n
    3. 预期回报最大化:投资组合理论帮助投资者在给定风险水平下,寻找最优的资产配置方式,以最大化预期回报。
    4. \n
    5. 考虑相关性:投资组合理论考虑资产之间的相关性,通过选择不同相关性的资产组合,可以实现更有效的投资组合。
    6. \n
    \n

    局限性

      \n
    1. 基于历史数据:投资组合理论通常基于历史数据来估计资产的预期回报和风险,但历史表现不一定能准确预测未来。
    2. \n
    3. 忽略非系统风险:投资组合理论主要关注系统性风险,即与整个市场相关的风险,而忽略了非系统性风险,即与特定公司或行业相关的风险。
    4. \n
    5. 需要大量数据和计算:实施投资组合理论需要大量的数据和计算,包括资产的历史表现、相关性矩阵等,这可能对个体投资者或资源有限的投资者来说是一个挑战。
    6. \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
    1. 风险平衡:风险平价模型通过平衡不同资产的风险贡献,实现投资组合的风险均衡。这可以帮助投资者降低对任何单个资产的依赖,从而提高整体投资组合的稳定性。
    2. \n
    3. 简单易懂:风险平价模型相对较简单,容易理解和实施。它不需要大量的数据和计算,适用于个体投资者或资源有限的投资者。
    4. \n
    \n

    局限性

      \n
    1. 忽略预期回报:风险平价模型关注风险的平衡,但忽略了资产的预期回报。这可能导致在追求风险均衡的同时牺牲了潜在的高回报机会。
    2. \n
    3. 对某些资产不适用:风险平价模型在处理某些特殊资产类别(如复杂衍生品)时可能存在困难,因为这些资产的风险无法简单地衡量和比较。
    4. \n
    \n

    投资组合理论与风险平价模型的主要区别

    目标和重点:

      \n
    • 投资组合理论的目标是在给定风险水平下,最大化投资组合的预期回报。它关注如何通过资产配置来实现最佳的风险-回报权衡。
    • \n
    • 风险平价模型的目标是平衡不同资产在整个投资组合中的风险贡献。它强调每个资产对总体风险的贡献应该是相等的。
    • \n
    \n

    风险分散方法

      \n
    • 投资组合理论通过将不同风险和回报特征的资产组合在一起,以实现风险的分散化。它考虑资产之间的相关性,并通过优化资产权重来达到风险分散的目标。
    • \n
    • 风险平价模型通过平衡不同资产的风险贡献来实现投资组合的风险分散。它将风险分配给各个资产,以确保它们在整个投资组合中对总体风险的贡献相等。
    • \n
    \n

    考虑因素:

      \n
    • 投资组合理论考虑了预期回报、风险和资产之间的相关性。它通过优化资产配置来平衡这些因素,以实现最佳的风险-回报组合。
    • \n
    • 风险平价模型更关注风险方面,特别是资产的风险贡献。它通过平衡不同资产的风险贡献来实现风险均衡,而对预期回报的考虑相对较少。
    • \n
    \n

    复杂性:

      \n
    • 投资组合理论在实践中通常需要更多的数据和计算,包括资产的历史表现、相关性矩阵等。它可能需要更多的复杂模型和技术分析来确定最佳的资产配置。
    • \n
    • 风险平价模型相对较简单,不需要大量数据和复杂计算。它可以作为一种直观且易于实施的方法,适用于个体投资者或资源有限的投资者。
    • \n
    \n

    关注点:

      \n
    • 投资组合理论关注整个投资组合的特征和表现,它试图找到最优的资产配置,以实现预期回报和风险的最佳权衡。
    • \n
    • 风险平价模型更关注投资组合内部的风险分散,它强调平衡不同资产的风险贡献,以降低整体投资组合的风险。
    • \n
    \n"},{"title":"Python kill编码问题","url":"/2016/Python-kill%E7%BC%96%E7%A0%81%E9%97%AE%E9%A2%98/","content":"
    \n

    之前在遇到字符串编码问题的时候都是跑到网上现去查资料,而且一直分不清 decodeencode 到底那个是解码,那个是编码,每次用的时候还要查一下文档,本次就做一个了断吧!

    \n
    \n

    Python 内部字符串编码为 unicode,因此在编码转换时,通常使用 unicode 作为中间编码, 先将其他编码的字符串解码(decode)成 unicode,再从 unicode 编码(encode)成另一种编码。

    \n

    unicode

    世界上存在着多种编码方式,同一个二进制数字可以被解释成不同的符号。因此,要想打开一个文本文件,就必须知道它的编码方式,否则用错误的编码方式解读,就会出现乱码。为什么电子邮件常常出现乱码?就是因为发信人和收信人使用的编码方式不一样。

    \n

    可以想象,如果有一种编码,将世界上所有的符号都纳入其中。每一个符号都给予一个独一无二的编码,那么乱码问题就会消失。这就是unicode,就像它的名字都表示的,这是一种所有符号的编码。

    \n

    decode

    decode 的作用是将其他编码的字符串解码成 unicode 编码。

    \n

    str.decode('gbk'),表示将 gbk 编码的字符串解码成 unicode 编码。

    \n

    encode

    encode 的作用是将 unicode 字符串编码成其他编码的字符串。

    \n

    str.encode('gbk'), 表示将 unicode 编码的字符串编码成 gbk 编码。

    \n
    \n

    因此转码的时候一定先搞明白,字符串是什么编码,然后 decodeunicode,再然后 encode 成其他编码。

    \n
    \n

    代码中字符串的编码与代码文件的编码一致

    如果这样写 s = u'中国' ,该字符串的编码就被指定为了 unicode 了,即 Python 的内部编码,而与代码文件本身编码无关,因此对于这种情况做编码转换,只需直接使用 encode 方法将其转换成指定编码即刻。

    \n

    如果对一个 unicode 字符串进行解码将会报错,所以可以使用 isinstance(s, unicode) 来判断是否为 unicode

    \n

    unicode 编码的字符串使用 encode 会报错。

    \n

    unicode(str, 'gbk')str.decode('gbk') 是一样的,都是将 gbk 编码的字符串转为 unicode 编码。

    \n
    \n

    唉,想起来之前有个非常耻辱的事,就是有次面试的时候对方问我 Python 中默认字符编码是什么,我回答的是: ascii

    \n

    还问我Python中如何进行编码和解码,其实我知道是用 decodeencode 但当时真的分不清楚哪个是做什么用的,所以忘了当时回答的对不对了。

    \n

    为了让自己把 decodeencode 分清楚,我想了个办法, decode 开头发音是 弟(di),所以弟就应该有个姐姐,所以 decode 就是解码,另外一个 encode 就自然是编码了。哈哈哈。

    \n

    对方还问了我,utf8unicode 有什么区别,后来查了下资料,结论是: UTF-8是Unicode的实现方式之一。 详情见阮一峰老师的博客

    \n","categories":["Code"],"tags":["Python","编码","decode","encode"]},{"title":"Python 查漏补缺","url":"/2016/Python-%E6%9F%A5%E6%BC%8F%E8%A1%A5%E7%BC%BA/","content":"

    在Python中当函数被定义时,默认参数只会运算一次,而不是每次被调用时都会重新运算。

    \n
    def add_to(num, target=[]):
    target.append(num)
    return target

    add_to(1)
    # Output: [1]

    add_to(2)
    # Output: [1, 2]

    add_to(3)
    # Output: [1, 2, 3]
    \n

    你应该永远不要定义可变类型的默认参数,除非你知道你正在做什么。你应该像这样做:

    \n
    def add_to(element, target=None):
    if target is None:
    target = []
    target.append(element)
    return target
    \n
    \n

    另一种三元运算符:

    \n
    #(返回假,返回真)[真或假]
    (if_test_is_false, if_test_is_true)[test]
    \n

    例子:

    \n
    fat = True
    fitness = ("skinny", "fat")[fat]
    print("Ali is ", fitness)
    #输出: Ali is fat
    \n

    这之所以能正常工作,是因为在Python中,True等于1,而False等于0,这就相当于在元组中使用0和1来选取数据。

    \n

    上面的例子没有被广泛使用,而且Python玩家一般不喜欢那样,因为没有Python味儿(Pythonic)。这样的用法很容易把真正的数据与true/false弄混。

    \n

    另外一个不使用元组条件表达式的缘故是因为在元组中会把两个条件都执行,而 if-else 的条件表达式不会这样。

    \n

    例如:

    \n
    condition = True
    print(2 if condition else 1/0)
    #输出: 2

    print((1/0, 2)[condition])
    #输出ZeroDivisionError异常
    \n

    这是因为在元组中是先建数据,然后用True(1)/False(0)来索引到数据。 而if-else条件表达式遵循普通的if-else逻辑树, 因此,如果逻辑中的条件异常,或者是重计算型(计算较久)的情况下,最好尽量避免使用元组条件表达式。

    \n
    \n\n

    当你在一个字典中对一个键进行嵌套赋值时,如果这个键不存在,会触发keyError异常。 defaultdict允许我们用一个聪明的方式绕过这个问题。 首先我分享一个使用dict触发KeyError的例子,然后提供一个使用defaultdict的解决方案。

    \n

    问题:

    \n
    some_dict = {}
    some_dict['colours']['favourite'] = "yellow"

    ## 异常输出:KeyError: 'colours'
    \n

    解决方案:

    \n
    import collections
    tree = lambda: collections.defaultdict(tree)
    some_dict = tree()
    some_dict['colours']['favourite'] = "yellow"

    ## 运行正常
    \n

    你可以用json.dumps打印出some_dict,例如:

    \n
    import json
    print(json.dumps(some_dict))

    ## 输出: {"colours": {"favourite": "yellow"}}
    \n
    \n

    列表辗平

    \n

    您可以通过使用itertools包中的itertools.chain.from_iterable轻松快速的辗平一个列表。下面是一个简单的例子:

    \n
    a_list = [[1, 2], [3, 4], [5, 6]]
    print(list(itertools.chain.from_iterable(a_list)))
    # Output: [1, 2, 3, 4, 5, 6]

    # or
    print(list(itertools.chain(*a_list)))
    # Output: [1, 2, 3, 4, 5, 6]
    \n

    待续

    ","categories":["笔记"],"tags":["Python"]},{"title":"Python中合并两个字典","url":"/2016/Python%E4%B8%AD%E5%90%88%E5%B9%B6%E4%B8%A4%E4%B8%AA%E5%AD%97%E5%85%B8/","content":"

    有两个字典:

    \n
    user = {'name': "Trey", 'website': "http://treyhunner.com"}

    defaults = {'name': "Anonymous User", 'page_name': "Profile Page"}
    \n

    现在想合并两个字典,得到一个新的字典,要求:

    \n
      \n
    1. 如果存在重复的键,user字典中的值应覆盖defaults字典中的值;
    2. \n
    3. defaults和user中的键可以是任意合法的键;
    4. \n
    5. defaults和user中的值可以是任意值;
    6. \n
    7. 在创建context字典时,defaults和user的元素不能出现变化;
    8. \n
    9. 更新context字典时,不能更改defaults或user字典。
    10. \n
    \n

    以上两个字典合并结果为:

    \n
    {'website': 'http://treyhunner.com', 'name': 'Trey', 'page_name': 'Profile Page'}
    \n

    Python 3 中最优雅的实现方法:

    \n
    context = {**defaults, **user}
    \n
    \n

    Python 2 中:

    \n
    多次更新
    context = {}
    context.update(defaults)
    context.update(user)
    \n

    这里我们创建了一个新的空字典,并使用其update方法从其他字典中添加元素。请注意,我们首先添加的是defaults字典中的元素,以保证user字典中的重复键会覆盖掉defaults中的键。

    \n
    复制,然后更新
    context = defaults.copy()
    context.update(user)
    \n
    ChainMap转换成字典
    context = dict(ChainMap(user, defaults))
    ","categories":["Code"],"tags":["Python"]},{"title":"Python只能以关键字形式指定参数","url":"/2016/Python%E5%8F%AA%E8%83%BD%E4%BB%A5%E5%85%B3%E9%94%AE%E5%AD%97%E5%BD%A2%E5%BC%8F%E6%8C%87%E5%AE%9A%E5%8F%82%E6%95%B0/","content":"

    Python 3 可以声明只能通过关键字来作为参数的函数:

    def safe_division(number, divisor, *, ignore_overflow=False, ingore_zero_division=False):  
    try:
    return number / divisor
    except OverflowError:
    if ignore_overflow:
    return 0
    else:
    raise
    except ZeroDivisionError:
    if ingore_zero_division:
    return float('int')
    else:
    raise
    \n

    参数列表里的 * 号,标志着位置参数结束,之后的参数都只能以关键字形式来指定。

    \n

    save_division(10, 0, False, True) # error

    \n

    save_division(10, 0, ignore_zero_division=True) # ok

    \n
    \n

    Python 2 中实现以关键字来指定的参数:

    Python 2 并没有明确的语法来定义这种只能以关键字形式指定的参数。不过我们可以在参数列表中使用 ** 操作符,并且领函数遇到无效的调用时抛出TypeErrors,这样就可以实现与Python 3 相同的功能了。

    \n

    为了使Python 2 版本的safe_division函数具备只能以关键字形式来指定的参数,我们可以先令该函数接受 **kwargs 参数,然后用 pop 方法把期望的关键字从 kwargs 字典里取走,如果字典的键里面没有那个关键字,那么 pop 方法的第二个参数就会成为默认值。最后为了防止调用者提供无效参数值,我们需要确认 kwargs 字典里面已经没有关键字参数了。

    \n
    # Python 2
    def safe_division(number, divisor, **kwargs):
    ignore_overflow = kwargs.pop('ignore_overflow', False)
    ingore_zero_division = kwargs.pop('ingore_zero_division', False)
    if kwargs:
    raise TypeError('Unexpected **kwargs: %r' % kwargs)
    try:
    return number / divisor
    except OverflowError:
    if ignore_overflow:
    return 0
    else:
    raise
    except ZeroDivisionError:
    if ingore_zero_division:
    return float('int')
    else:
    raise
    \n

    safe_division(1, 0, False, False) # error

    \n

    safe_division(0, 0, unexpected=True) # error

    \n

    save_division(10, 0, ignore_zero_division=True) # ok

    \n","categories":["Code"],"tags":["Python","关键字参数","Effective Python"]},{"title":"Python最佳实践指南 阅读笔记","url":"/2016/Python%E6%9C%80%E4%BD%B3%E5%AE%9E%E8%B7%B5%E6%8C%87%E5%8D%97-%E9%98%85%E8%AF%BB%E7%AC%94%E8%AE%B0/","content":"

    创建将0到19连接起来的字符串

    nums = []
    for n in range(20):
    nums.append(str(n))
    print "".join(nums)

    # 更好的写法
    nums = [str(n) for n in range(20)]
    print "".join(nums)
    \n

    拼接多个已有的字符串

    foo = 'foo'
    bar = 'bar'

    foobar = foo + bar # 好的做法

    foo += 'ooo' # 不好的做法, 应该这么做:
    foo = ''.join([foo, 'ooo'])
    \n

    也可以使用 % 格式运算符来连接确定数量的字符串,但 PEP 3101 建议使用 str.format() 替代 % 操作符。

    foo = 'foo'
    bar = 'bar'

    foobar = '%s%s' % (foo, bar) # 可行
    foobar = '{0}{1}'.format(foo, bar) # 更好
    foobar = '{foo}{bar}'.format(foo=foo, bar=bar) # 最好
    \n

    不要重复使用命名

    items = 'a b c d'  # 首先指向字符串...
    items = items.split(' ') # ...变为列表
    items = set(items) # ...再变为集合
    \n
    \n

    重复使用命名对效率并没有提升:赋值时无论如何都要创建新的对象。然而随着复杂度的 提升,赋值语句被其他代码包括 ‘if’ 分支和循环分开,使得更难查明指定变量的类型。 在某些代码的做法中,例如函数编程,推荐的是从不重复对同一个变量命名赋值。Java 内的实现方式是使用 ‘final’ 关键字。Python并没有 ‘final’ 关键字而且这与它的哲学 相悖。尽管如此,避免给同一个变量命名重复赋值仍是是个好的做法,并且有助于掌握 可变与不可变类型的概念。

    \n
    \n

    考虑该不该用任意参数列表(*args)

    \n

    如果一个函数接受的参数列表具有 相同的性质,通常把它定义成一个参数,这个参数是一个列表或者其他任何序列会更清晰。

    \n
    \n

    函数单个出口可能更好

    \n

    当一个函数在其正常过程中有多个主要出口点时,它会变得难以调试和返回其 结果,所以保持单个出口点可能会更好。这也将有助于提取某些代码路径,而且多个出口点 很有可能意味着这里需要重构。

    \n
    \n
    def complex_function(a, b, c):
    if not a:
    return None # 抛出一个异常可能会更好
    if not b:
    return None # 抛出一个异常可能会更好

    # 一些复杂的代码试着用a,b,c来计算x
    # 如果成功了,抵制住返回x的诱惑
    if not x:
    # 一些关于x的计算的Plan-B
    return x
    \n

    常见Python习语

      \n
    • 解包
    • \n
    \n
    for index, item in enumerate(some_list):
    # 使用index和item做一些工作

    a, b = b, a

    a, (b, c) = 1, (2, 3)
    \n
      \n
    • 创建一个被忽略的变量
    • \n
    \n
    filename = 'foobar.txt'
    basename, __, ext = filename.rpartition('.')
    \n
      \n
    • 创建一个含N个对象的列表
    • \n
    \n
    four_nones = [None] * 4
    \n
      \n
    • 创建一个含N个列表的列表
    • \n
    \n
    four_lists = [[] for __ in xrange(4)]
    \n
      \n
    • 根据列表来创建字符串
    • \n
    \n
    letters = ['s', 'p', 'a', 'm']
    word = ''.join(letters)
    \n
      \n
    • 在集合体(collection)中查找一个项
    • \n
    \n
    s = set(['s', 'p', 'a', 'm'])
    l = ['s', 'p', 'a', 'm']

    def lookup_set(s):
    return 's' in s

    def lookup_list(l):
    return 's' in l
    \n

    在下列场合在使用集合或者字典而不是列表,通常会是个好主意:

    \n

    集合体中包含大量的项
    你将在集合体中重复地查找项
    你没有重复的项

    \n

    你不需要明确地比较一个值是True,或者None,或者0

    糟糕

    if attr == True:
    print 'True!'

    if attr == None:
    print 'attr is None!'
    \n

    优雅

    # 检查值
    if attr:
    print 'attr is truthy!'

    # 或者做相反的检查
    if not attr:
    print 'attr is falsey!'

    # or, since None is considered false, explicitly check for it
    if attr is None:
    print 'attr is None!'
    \n

    访问字典元素

    糟糕

    d = {'hello': 'world'}
    if d.has_key('hello'):
    print d['hello'] # 打印 'world'
    else:
    print 'default_value'
    \n

    优雅

    d = {'hello': 'world'}

    print d.get('hello', 'default_value') # 打印 'world'
    print d.get('thingy', 'default_value') # 打印 'default_value'

    # Or:
    if 'hello' in d:
    print d['hello']
    \n

    在每次函数调用中,通过使用指示没有提供参数的默认参数 None 通常是 个好选择),来创建一个新的对象。

    举例:

    def append_to(element, to=[]):
    to.append(element)
    return to
    \n

    你可能认为

    \n
    my_list = append_to(12)
    print my_list # [12]

    my_other_list = append_to(42)
    print my_other_list # [42]
    \n

    实际结果为

    \n
    # [12]

    # [12, 42]
    \n

    当函数被定义时,一个新的列表就被创建一次 ,而且同一个列表在每次成功的调用中都被使用。

    \n

    当函数被定义时,Python的默认参数就被创建 一次,而不是每次调用函数的时候创建。 这意味着,如果你使用一个可变默认参数并改变了它,你 将会 在未来所有对此函数的 调用中改变这个对象。

    \n

    迟绑定闭包

    举例

    def create_multipliers():
    return [lambda x : i * x for i in range(5)]

    for multiplier in create_multipliers():
    print multiplier(2)
    \n

    你期望的结果

    \n
    0
    2
    4
    6
    8
    \n

    实际结果

    \n
    8
    8
    8
    8
    8
    \n

    五个函数被创建了,它们全都用4乘以 x 。

    \n

    Python的闭包是 迟绑定 。 这意味着闭包中用到的变量的值,是在内部函数被调用时查询得到的。

    \n

    这里,不论 任何 返回的函数是如何被调用的, i 的值是调用时在周围作用域中查询到的。 接着,循环完成, i 的值最终变成了4。

    \n

    这个陷阱并不和 lambda 有关,不通定义也会这样

    def create_multipliers():
    multipliers = []

    for i in range(5):
    def multiplier(x):
    return i * x
    multipliers.append(multiplier)

    return multipliers
    \n

    解决方案

    最一般的解决方案可以说是有点取巧(hack)。由于 Python 拥有为函数默认参数 赋值的行为,你可以创建一个立即绑定参数的闭包,像下面这样:

    \n
    def create_multipliers():
    return [lambda x, i=i : i * x for i in range(5)]
    \n

    或者,可以使用 function.partial 函数

    \n
    from functools import partial
    from operator import mul

    def create_multipliers():
    return [partial(mul, i) for i in range(5)]
    \n","categories":["Code"],"tags":["Python","读书笔记"]},{"title":"回归模型 vs 分类模型","url":"/2023/Regression-model-vs-Classification-model/","content":"

    在机器学习中,回归模型和分类模型是两种常见的预测模型,它们的主要区别在于其预测目标和输出类型。

    \n

    预测目标

      \n
    • 回归模型的预测目标是连续数值。
        \n
      • 回归模型用于预测输出变量的数值,例如房价预测或股票价格预测。
      • \n
      • 回归模型试图建立输入特征与输出值之间的数值关系。
      • \n
      \n
    • \n
    • 分类模型的预测目标是离散类别。
        \n
      • 分类模型用于将输入实例分配到预定义的类别中,例如垃圾邮件分类或图像识别。
      • \n
      • 分类模型试图学习输入特征与类别之间的关系。
      • \n
      \n
    • \n
    \n

    输出类型

      \n
    • 回归模型的输出是连续的。
        \n
      • 回归模型生成一个实数或浮点数作为预测结果,可以是任意精度的数值。
      • \n
      • 例如,预测某个人的年龄可以是一个实数,如25.6岁。
      • \n
      \n
    • \n
    • 分类模型的输出是离散的。
        \n
      • 分类模型预测样本属于预定义类别的概率或直接预测样本的类别标签。
      • \n
      • 例如,对于垃圾邮件分类,模型的输出可以是”垃圾邮件”或”非垃圾邮件”。
      • \n
      \n
    • \n
    \n

    小孩子都能懂的回归模型解释

    回归模型就像是一个预测机器,可以帮助我们猜测事物的未来。假设你喜欢吃冰淇淋,而冰淇淋的价格通常会随着天气变化而变化。现在,我们可以观察天气情况和冰淇淋的价格,然后用这些信息来猜测未来的价格。

    \n

    比如,如果明天是个炎热的夏天,天气很热,那么冰淇淋的价格可能会比较高,因为很多人想要买冰淇淋来解暑。相反,如果明天是个寒冷的冬天,天气很冷,那么冰淇淋的价格可能会比较低,因为很少人会想要吃冰淇淋。

    \n

    回归模型就是通过观察过去的天气和冰淇淋价格的关系,来预测将来的价格。它会考虑到很多因素,例如天气、季节和需求,然后给我们一个猜测的价格。虽然它不能百分之百准确地猜测价格,但它可以给我们一个大概的预测,帮助我们做决策。

    \n

    小孩子都能懂的分类模型解释

    分类模型就像是一个分类小助手,可以帮助我们将东西归类。想象一下,你有很多玩具,例如球、娃娃和积木。现在,你想要把它们分类整理,把球放在一起、把娃娃放在一起,积木也放在一起。

    \n

    分类模型就是帮助我们做这个分类工作的机器。它会观察玩具的特点,比如形状、颜色和材质,然后根据这些特点把它们分成不同的类别。就像是在玩玩具时,你可以根据它们的外观和特点来决定它们应该放在哪个盒子里。

    \n

    分类模型可以帮助我们在很多不同的情况下进行分类,比如识别动物、区分水果、辨别颜色等。它可以根据事物的特征将它们分成不同的组别,让我们更好地理解和组织世界。

    \n

    应用场景

    回归模型的应用场景:

      \n
    1. 房价预测:根据房屋的特征(如面积、卧室数量、地理位置等),预测房屋的价格。
    2. \n
    3. 销售量预测:根据过去的销售数据、广告投入和季节性因素,预测未来某个产品的销售量。
    4. \n
    5. 股票价格预测:根据股票过去的价格数据、市场指标和新闻事件,预测股票的未来走势。
    6. \n
    7. 气候模型:根据历史气象数据、大气压力和温度等因素,预测未来的天气情况。
    8. \n
    9. 医学研究:根据患者的临床特征和生物标记物,预测患者的疾病风险或治疗效果。
    10. \n
    \n

    分类模型的应用场景:

      \n
    1. 垃圾邮件分类:根据电子邮件的内容、发件人和其他特征,将电子邮件分为垃圾邮件和非垃圾邮件。
    2. \n
    3. 图像识别:根据图像的特征和内容,将图像分类为不同的对象或场景,如猫、狗、汽车或风景。
    4. \n
    5. 疾病诊断:根据患者的症状、体征和医学测试结果,将患者的疾病分类为不同的类别,如心脏病、癌症或糖尿病。
    6. \n
    7. 情感分析:根据文本的情感特征,将文本分类为积极、消极或中性的情感。
    8. \n
    9. 客户细分:根据客户的行为、偏好和购买历史,将客户分为不同的细分群体,以便进行个性化营销。
    10. \n
    \n"},{"title":"SELinux 开启导致 Nginx 启动失败","url":"/2018/SELinux-%E5%BC%80%E5%90%AF%E5%AF%BC%E8%87%B4-Nginx-%E5%90%AF%E5%8A%A8%E5%A4%B1%E8%B4%A5/","content":"

    今天在启动一个 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)

    \n

    经过一番查询,发现是因为 SELinux 导致的,平时用到的不多,直接将其关闭即可。

    \n

    查看状态

    \n
    /usr/sbin/sestatus -v      ##如果SELinux status参数为enabled即为开启状态
    SELinux status: enabled
    \n

    or

    \n
    getenforce                 ##也可以用这个命令检查
    \n

    关闭SELinux:

    \n

    临时关闭(不用重启机器):

    \n
    setenforce 0                  ##设置SELinux 成为permissive模式
    ##setenforce 1 设置SELinux 成为enforcing模式
    \n

    修改配置文件需要重启机器:

    \n
    修改/etc/selinux/config 文件
    将SELINUX=enforcing改为SELINUX=disabled
    \n

    重启机器即可

    \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

    已上外链已失效。

    \n

    大致看了下 SELinxu 的作用一个 Linux 的强化方案,大部分情况下是需要启动的 https://www.zhihu.com/question/20559538

    \n"},{"title":"SOLID:面向对象设计的五个基本原则","url":"/2020/SOLID-Design-Principles/","content":"

    \"\"

    \n

    先来看下维基百科对 SOLID 的介绍:

    \n
    \n

    在程序设计领域,SOLID 是由罗伯特·C·马丁在 21 世纪早期引入的记忆术首字母缩略字,指代了面向对象编程和面向对象设计的五个基本原则。当这些原则被一起应用时,它们使得一个程序员开发一个容易进行软件维护和扩展的系统变得更加可能。

    \n
    \n

    SOLID 是以下五个单词的缩写:

    \n
      \n
    • Single Responsibility Principle(单一职责原则)
    • \n
    • Open Closed Principle(开闭原则)
    • \n
    • Liskov Substitution Principle(里氏替换原则)
    • \n
    • Interface Segregation Principle(接口隔离原则)
    • \n
    • Dependency Inversion Principle(依赖倒置原则)
    • \n
    \n

    单一职责原则

    单一职责原则的英文是 Single Responsibility Principle,缩写为 SRP

    \n

    可以从两个角度来理解单一职责原则:

    \n
      \n
    1. 一个类或者模块只负责完成一个职责(或者功能)。
    2. \n
    3. 一个类,应该只有一个引起它变化的原因。
    4. \n
    \n

    对于这两种理解方式,我分别举例来说明。

    \n

    一个类或者模块只负责完成一个职责(或者功能)

    这里的模块可以看作比类更加粗粒度的代码块,模块中包含多个类,多个类组成一个模块。

    \n

    来看下边的代码:

    \n
    public class UserInfo {
    private long userId;
    private String username;
    private String email;
    private String telephone;
    private long createTime;
    private long lastLoginTime;
    private String avatarUrl;
    private String provinceOfAddress; // 省
    private String cityOfAddress; // 市
    private String regionOfAddress; // 区
    private String detailedAddress; // 详细地址
    // ...省略其他属性和方法...
    }
    \n

    站在不同的应用场景不同阶段的需求背景下,对 UserInfo 类的职责是否单一的判定,可能都是不一样的:

    \n
      \n
    • 如果在这个社交产品中,用户的地址信息跟其他信息一样,只是单纯地用来展示,那 UserInfo 现在的设计就是合理的。
    • \n
    • 如果这个社交产品发展得比较好,之后又在产品中添加了电商的模块,用户的地址信息还会用在电商物流中,那我们最好将地址信息从 UserInfo 中拆分出来,独立成用户物流信息(或者叫地址信息、收货信息等)。
    • \n
    • 如果做这个社交产品的公司发展得越来越好,公司内部又开发出了跟多其他产品(可以理解为其他 App)。公司希望支持统一账号系统,也就是用户一个账号可以在公司内部的所有产品中登录。这个时候,我们就需要继续对 UserInfo 进行拆分,将跟身份认证相关的信息(比如,email、telephone 等)抽取成独立的类。
    • \n
    \n
    \n

    在某种应用场景或者当下的需求背景下,一个类的设计可能已经满足单一职责原则了,但如果换个应用场景或着在未来的某个需求背景下,可能就不满足了,需要继续拆分成粒度更细的类。

    \n
    \n

    一个类,应该只有一个引起它变化的原因

    我们这里以一个矩形类 Rectangle 为例,如图所示:

    \n

    \"\"

    \n

    Rectangle 有两个方法:

    \n
      \n
    • 绘图方法 draw()
    • \n
    • 计算面积方法 area()
    • \n
    \n

    现在有两个应用程序要依赖这个 Rectangle 类:

    \n
      \n
    1. 几何计算应用程序:只需要计算面积,不需要绘图。
    2. \n
    3. 图形界面应用程序:绘图的时候,程序需要计算面积。
    4. \n
    \n

    在计算机屏幕上绘图是一件非常麻烦的事情,所以对于绘图这个需求来说,需要依赖专门的 GUI 库。一个 GUI 库可能有几十 M 甚至数百 M。

    \n

    本来几何计算程序作为一个纯科学计算程序,主要是一些数学计算代码,现在程序打包完,却不得不把一个不相关的 GUI 库也打包进来。本来程序包可能只有几百 K,现在变成了几百 M。

    \n

    当图形界面应用程序不得不修改 Rectangle 类的时候,还得重新编译几何计算应用程序,反之亦然。这个情况下,我们就可以说 Rectangle 类有两个引起它变化的原因。

    \n

    当然,这里用前一种理解也是可以的(一个类或者模块只负责完成一个职责):Rectangle承担了两个职责,一个是几何形状的计算,一个是在屏幕上绘制图形。

    \n

    我们可以将 Rectangle 拆分成两个类:

    \n
      \n
    1. GeometricRectangle: 这个类负责实现图形面积计算方法 area()
    2. \n
    3. Rectangle:只保留单一绘图方法 draw()
    4. \n
    \n

    现在绘制长方形的时候可以使用计算面积的方法,而几何计算应用程序则不需要依赖一个不相关的绘图方法以及一大堆的 GUI 组件。

    \n

    拆分后的类图如下所示:

    \n

    \"\"

    \n

    从 Web 应用架构演进看单以职责原则

    从事过 Java Web 开发的老码农都经历过下边这 3 个开发阶段。

    \n

    阶段 1:请求处理以及响应的全部操作都在 Servlet 里,Servlet 获取请求数据,进行逻辑处理,访问数据库,得到处理结果,根据处理结果构造返回的 HTML

    \n

    \"\"

    \n

    阶段 2:于是后来就有了 JSP,如果说 Servlet 是在程序中输出 HTML,那么 JSP 就是在 HTML 中调用程序。

    \n

    \"\"

    \n

    这个阶段,基于 JSP 开发的 Web 程序在职责上进行了一些最基本的分离:构造页面的 JSP 和处理逻辑的业务模型分离。

    \n

    阶段 3:各种 MVC 框架的出现,MVC 框架通过控制器将视图与模型彻底分离。

    \n

    \"\"

    \n

    有了 MVC,就可以顺理成章地将复杂的业务模型进行分层了。通过分层方式,将业务模型分为业务层、服务层、数据持久层,使各层职责进一步分离,更符合单一职责原则。

    \n

    \"\"

    \n
    \n

    也是因为 MVC 框架的出现,才使得前后端开发成为两个不同的工种,前端工程师只做视图模板开发,后端工程师只做业务开发,彼此之间没有直接的依赖和耦合,各自独立开发、维护自己的代码。

    \n
    \n

    如何判断一个类是否满足单一职责?

    前边提到过,不同的应用场景、不同阶段的需求背景、不同的业务层面,对同一个类的职责是否单一,可能会有不同的判定结果。我们可以通过一些侧面指标来指导我们的判断。

    \n

    出现下面这些情况就有可能说明类的设计不满足单一职责原则:

    \n
      \n
    • 类中的代码行数、函数或者属性过多
    • \n
    • 依赖的其他类过多,或者依赖此类的其他类过多
    • \n
    • 私有方法过多
    • \n
    • 比较难给类起一个合适的名字
    • \n
    • 类中大量的方法都是集中操作类中的某几个属性
    • \n
    \n

    开闭原则

    \n

    开闭原则是所有设计原则中最有用的,因为扩展性是代码质量最重要的衡量标准之一。在 23 种经典设计模式中,大部分设计模式都是为了解决代码的扩展性问题而存在的,主要遵从的设计原则就是开闭原则

    \n
    \n

    开闭原则的英文是 Open Closed Principle,缩写为 OCP

    \n

    开闭原则说的是:软件实体(模块、类、函数等等)应该对扩展是开放的,对修改是关闭的。

    \n
      \n
    • 对扩展是开放的,意味着软件实体的行为是可扩展的,当需求变更的时候,可以对模块进行扩展,使其满足需求变更的要求。
    • \n
    • 对修改是关闭的,意味着当对软件实体进行扩展的时候,不需要改动当前的软件实体;不需要修改代码;对于已经完成的类文件不需要重新编辑;对于已经编译打包好的模块,不需要再重新编译。
    • \n
    \n

    两者结合起来表述为:添加一个新的功能应该是,在已有代码基础上扩展代码(新增模块、类、方法等),而非修改已有代码(修改模块、类、方法等)。

    \n

    举例说明开闭原则

    public class Alert {
    private AlertRule rule;
    private Notification notification;

    public Alert(AlertRule rule, Notification notification) {
    this.rule = rule;
    this.notification = notification;
    }

    public void check(String api, long requestCount, long errorCount,
    long durationOfSeconds) {
    long tps = requestCount / durationOfSeconds;

    if (tps > rule.getMatchedRule(api).getMaxTps()) {
    notification.notify(NotificationEmergencyLevel.URGENCY, "...");
    }

    if (errorCount > rule.getMatchedRule(api).getMaxErrorCount()) {
    notification.notify(NotificationEmergencyLevel.SEVERE, "...");
    }
    }
    }
    \n

    以上代码中的 AlertRule 存储告警规则,可以自由设置。

    \n

    Notification 是告警通知类,支持邮件、短信、微信、手机等多种通知渠道。

    \n

    NotificationEmergencyLevel 表示通知的紧急程度,不同的紧急程度对应不同的发送渠道。

    \n

    业务逻辑主要集中在 check() 函数中:当接口的 TPS 超过某个预先设置的最大值时,以及当接口请求出错数大于某个最大允许值时,就会触发告警。

    \n

    现在,如果我们需要添加一个功能,当每秒钟接口超时请求个数,超过某个预先设置的最大阈值时,我们也要触发告警发送通知。

    \n

    不遵循开闭原则的修改

    主要的改动有两处:

    \n
      \n
    1. 修改 check() 函数的入参,添加一个新的统计数据 timeoutCount,表示超时接口请求数
    2. \n
    3. check() 函数中添加新的告警逻辑
    4. \n
    \n

    public class Alert {
    // ...省略AlertRule/Notification属性和构造函数...

    // 改动一:添加参数timeoutCount
    public void check(String api, long requestCount, long errorCount, long timeoutCount, long durationOfSeconds) {
    long tps = requestCount / durationOfSeconds;
    if (tps > rule.getMatchedRule(api).getMaxTps()) {
    notification.notify(NotificationEmergencyLevel.URGENCY, "...");
    }
    if (errorCount > rule.getMatchedRule(api).getMaxErrorCount()) {
    notification.notify(NotificationEmergencyLevel.SEVERE, "...");
    }
    // 改动二:添加接口超时处理逻辑
    long timeoutTps = timeoutCount / durationOfSeconds;
    if (timeoutTps > rule.getMatchedRule(api).getMaxTimeoutTps()) {
    notification.notify(NotificationEmergencyLevel.URGENCY, "...");
    }
    }
    }
    \n

    如此进行的代码修改导致将导致以下问题:

    \n
      \n
    1. 调用这个接口的代码都要做相应的修改
    2. \n
    3. 修改了 check() 函数,相应的单元测试都需要修改
    4. \n
    \n

    粗暴一点说,当我们在代码中看到 if/else 或者 switch/case 关键字的时候,基本可以判断违反开闭原则了。

    \n

    遵循开闭原则的修改

    重构一下之前的 Alert 代码,让它的扩展性更好一些:

    \n
      \n
    1. check() 函数的多个入参封装成 ApiStatInfo
    2. \n
    3. 引入 handler 的概念,将 if 判断逻辑分散在各个 handler
    4. \n
    \n
    public class Alert {
    private List<AlertHandler> alertHandlers = new ArrayList<>();
    public void addAlertHandler(AlertHandler alertHandler) {
    this.alertHandlers.add(alertHandler);
    }
    public void check(ApiStatInfo apiStatInfo) {
    for (AlertHandler handler : alertHandlers) {
    handler.check(apiStatInfo);
    }
    }
    }
    public class ApiStatInfo {
    //省略constructor/getter/setter方法
    private String api;
    private long requestCount;
    private long errorCount;
    private long durationOfSeconds;
    }
    public abstract class AlertHandler {
    protected AlertRule rule;
    protected Notification notification;
    public AlertHandler(AlertRule rule, Notification notification) {
    this.rule = rule;
    this.notification = notification;
    }
    public abstract void check(ApiStatInfo apiStatInfo);
    }
    public class TpsAlertHandler extends AlertHandler {
    public TpsAlertHandler(AlertRule rule, Notification notification) {
    super(rule, notification);
    }
    @Override
    public void check(ApiStatInfo apiStatInfo) {
    long tps = apiStatInfo.getRequestCount()/ apiStatInfo.getDurationOfSeconds();
    if (tps > rule.getMatchedRule(apiStatInfo.getApi()).getMaxTps()) {
    notification.notify(NotificationEmergencyLevel.URGENCY, "...");
    }
    }
    }
    public class ErrorAlertHandler extends AlertHandler {
    public ErrorAlertHandler(AlertRule rule, Notification notification){
    super(rule, notification);
    }
    @Override
    public void check(ApiStatInfo apiStatInfo) {
    if (apiStatInfo.getErrorCount() > rule.getMatchedRule(apiStatInfo.getApi()).getMaxErrorCount()) {
    notification.notify(NotificationEmergencyLevel.SEVERE, "...");
    }
    }
    }
    \n

    现在我们基于重构之后的代码来实现每秒钟接口超时请求个数超过某个最大阈值就告警的新功能就方便多了。

    \n

    不考虑调用方修改的情况下,实现方只需进行两处的改动:

    \n
      \n
    1. ApiStatInfo 类中添加新的属性 timeoutCount
    2. \n
    3. 添加新的 TimeoutAlertHander
    4. \n
    \n

    调用方的改动也很简单:

    \n
      \n
    1. TimeoutAlertHander 类的实例注册到 alert 对象中
    2. \n
    3. apiStatInfo 对象 设置 timeoutCOunt 的值。
    4. \n
    \n

    修改后代码如下:

    \n
    public class Alert { // 代码未改动... }
    public class ApiStatInfo {//省略constructor/getter/setter方法
    private String api;
    private long requestCount;
    private long errorCount;
    private long durationOfSeconds;
    private long timeoutCount; // 改动一:添加新字段
    }
    public abstract class AlertHandler { //代码未改动... }
    public class TpsAlertHandler extends AlertHandler {//代码未改动...}
    public class ErrorAlertHandler extends AlertHandler {//代码未改动...}
    // 改动二:添加新的handler
    public class TimeoutAlertHandler extends AlertHandler {//省略代码...}

    public class ApplicationContext {
    private AlertRule alertRule;
    private Notification notification;
    private Alert alert;

    public void initializeBeans() {
    alertRule = new AlertRule(/*.省略参数.*/); //省略一些初始化代码
    notification = new Notification(/*.省略参数.*/); //省略一些初始化代码
    alert = new Alert();
    alert.addAlertHandler(new TpsAlertHandler(alertRule, notification));
    alert.addAlertHandler(new ErrorAlertHandler(alertRule, notification));
    // 改动三:注册handler
    alert.addAlertHandler(new TimeoutAlertHandler(alertRule, notification));
    }
    //...省略其他未改动代码...
    }

    public class Demo {
    public static void main(String[] args) {
    ApiStatInfo apiStatInfo = new ApiStatInfo();
    // ...省略apiStatInfo的set字段代码
    apiStatInfo.setTimeoutCount(289); // 改动四:设置tiemoutCount值
    ApplicationContext.getInstance().getAlert().check(apiStatInfo);
    }
    \n

    重构之后的代码更加灵活和易扩展。如果我们要想添加新的告警逻辑,只需要基于扩展的方式创建新的 handler 类即可,不需要改动原来的 check() 函数的逻辑。而且,我们只需要为新的 handler 类添加单元测试,老的单元测试都不会失败,也不用修改。

    \n

    开闭原则的设计初衷是:只要它没有破坏原有的代码的正常运行,没有破坏原有的单元测试,我们就可以说,这是一个合格的代码改动。

    \n

    通过上边举过的例子可以看出:添加一个新功能,不可能任何模块、类、方法的代码都不「修改」,这个是做不到的。我们要做的是尽量让修改操作更集中、更少、更上层,尽量让最核心、最复杂的那部分逻辑代码满足开闭原则。

    \n

    同样一个代码改动,在粗代码粒度下,被认定为「修改」,在细代码粒度下,又可以被认定为「扩展」。

    \n
      \n
    • 比如,改动一,添加属性和方法相当于修改类,在类这个层面,这个代码改动可以被认定为「修改」;
    • \n
    • 代码改动并没有修改已有的属性和方法,在方法(及其属性)这一层面,它又可以被认定为「扩展」。
    • \n
    \n

    如何做到「对扩展开放、修改关闭」?

    站在「」的角度:

    为了尽量写出扩展性好的代码,我们要时刻具备扩展意识、抽象意识、封装意识。这些「潜意识」可能比任何开发技巧都重要。

    \n

    站在「」的角度:

    最常用来提高代码扩展性的方法有:多态、依赖注入、基于接口而非实现编程,以及大部分的设计模式(比如,装饰、策略、模板、职责链、状态)。

    \n

    来看一个遵循开闭原则的例子:

    结合了多态、依赖注入、基于接口而非实现通过 Kafka 来发送异步消息。

    \n
      \n
    • 我们抽象了一组跟具体消息队列(Kafka)无关的异步消息接口
    • \n
    • 所有上层系统都依赖这组抽象的接口编程,并且通过依赖注入的方式来调用
    • \n
    • 当我们要替换新的消息队列的时候,比如将 Kafka 替换成 RocketMQ,可以很方便地拔掉老的消息队列实现,插入新的消息队列实现。
    • \n
    \n

    // 这一部分体现了抽象意识
    public interface MessageQueue { //... }
    public class KafkaMessageQueue implements MessageQueue { //... }
    public class RocketMQMessageQueue implements MessageQueue {//...}

    public interface MessageFormatter { //... }
    public class JsonMessageFormatter implements MessageFormatter {//...}
    public class MessageFormatter implements MessageFormatter {//...}

    public class Demo {
    private MessageQueue msgQueue; // 基于接口而非实现编程
    public Demo(MessageQueue msgQueue) { // 依赖注入
    this.msgQueue = msgQueue;
    }

    // msgFormatter:多态、依赖注入
    public void sendNotification(Notification notification, MessageFormatter msgFormatter) {
    //...
    }
    }
    \n

    实现开闭原则的关键是抽象。当一个模块依赖的是一个抽象接口的时候,就可以随意对这个抽象接口进行扩展,这个时候,不需要对现有代码进行任何修改,利用接口的多态性,通过增加一个新实现该接口的实现类,就能完成需求变更。

    \n

    开闭原则可以说是软件设计原则的原则,是软件设计的核心原则,其他的设计原则更偏向技术性,具有技术性的指导意义,而开闭原则是方向性的,在软件设计的过程中,应该时刻以开闭原则指导、审视自己的设计:当需求变更的时候,现在的设计能否不修改代码就可以实现功能的扩展?如果不是,那么就应该进一步使用其他的设计原则和设计模式去重新设计。

    \n

    如何在项目中灵活应用开闭原则?

    写出支持「对扩展开放、对修改关闭」的代码的关键是预留扩展点:

    \n
      \n
    • 对于一些比较确定的、短期内可能就会扩展,或者需求改动对代码结构影响比较大的情况,或者实现成本不高的扩展点,在编写代码的时候,我们就可以事先做些扩展性设计。
    • \n
    • 反之,对于一些不确定未来是否要支持的需求,或者实现起来比较复杂的扩展点,我们可以等到有需求驱动的时候,再通过重构代码的方式来支持扩展的需求。
    • \n
    \n
    \n

    话外音:这种技术视野的前提是需要在某个领域进行深耕。

    \n
    \n

    最后提醒一下,天下没有免费的午餐,有些情况下,代码的扩展性会跟可读性相冲突。很多时候,我们都需要在扩展性可读性之间做权衡

    \n

    里氏替换原则

    单一职责原则的英文是 Liskov Substitution Principle,缩写为 LSP

    \n

    官方一些的介绍:子类对象能够替换程序中父类对象出现的任何地方,并且保证原来程序的逻辑行为不变及正确性不被破坏。

    \n

    通俗地说就是:子类型必须能够替换掉它们的基类型。

    \n

    通俗地详细点说:程序中,所有使用基类的地方,都应该可以用子类代替。

    \n

    里氏替换原则示例 1

    如下代码中,父类 Transporter 使用 org.apache.http 库中的 HttpClient 类来传输网络数据。子类 SecurityTransporter 继承父类 Transporter,增加了额外的功能,支持传输 appIdappToken 安全认证信息。

    \n
    public class Transporter {
    private HttpClient httpClient;

    public Transporter(HttpClient httpClient) {
    this.httpClient = httpClient;
    }

    public Response sendRequest(Request request) {
    // ...use httpClient to send request
    }
    }

    public class SecurityTransporter extends Transporter {
    private String appId;
    private String appToken;

    public SecurityTransporter(HttpClient httpClient, String appId, String appToken) {
    super(httpClient);
    this.appId = appId;
    this.appToken = appToken;
    }

    @Override
    public Response sendRequest(Request request) {
    if (StringUtils.isNotBlank(appId) && StringUtils.isNotBlank(appToken)) {
    request.addPayload("app-id", appId);
    request.addPayload("app-token", appToken);
    }
    return super.sendRequest(request);
    }
    }

    public class Demo {
    public void demoFunction(Transporter transporter) {
    Reuqest request = new Request();
    //...省略设置request中数据值的代码...
    Response response = transporter.sendRequest(request);
    //...省略其他逻辑...
    }
    }

    // 里式替换原则
    Demo demo = new Demo();
    demo.demofunction(new SecurityTransporter(/*省略参数*/););
    \n

    子类 SecurityTransporter 的设计完全符合里式替换原则,可以替换父类出现的任何位置,并且原来代码的逻辑行为不变且正确性也没有被破坏。

    \n

    这样看来里氏替换原则不就是简单利用了多态的特性吗?我们通过一个反例来看下这两者的区别:

    \n
    \n

    通俗地说,接口(抽象类)的多个实现就是多态。多态可以让程序在编程时面向接口进行编程,在运行期绑定具体类,从而使得类之间不需要直接耦合,就可以关联组合,构成一个更强大的整体对外服务。

    \n
    \n

    我们对刚刚那个例子中 SecurityTransporter 类的 sendRequest() 方法稍加改造一下。

    \n
      \n
    • 改造前,如果 appId 或者 appToken 没有设置,我们就不做校验;
    • \n
    • 改造后,如果 appId 或者 appToken 没有设置,则直接抛出 NoAuthorizationRuntimeException 未授权异常。
    • \n
    \n
    public class SecurityTransporter extends Transporter {
    //...省略其他代码..
    @Override
    public Response sendRequest(Request request) {
    if (StringUtils.isBlank(appId) || StringUtils.isBlank(appToken)) {
    throw new NoAuthorizationRuntimeException(...);
    }
    request.addPayload("app-id", appId);
    request.addPayload("app-token", appToken);
    return super.sendRequest(request);
    }
    }
    \n

    改造之后的代码仍然可以通过 Java 的多态语法,动态地用子类 SecurityTransporter 来替换父类 Transporter,也并不会导致程序编译或者运行报错。但是,从设计思路上来讲,SecurityTransporter 的设计是不符合里式替换原则的。

    \n

    虽然从定义描述和代码实现上来看,多态和里式替换有点类似,但它们关注的角度是不一样的。

    \n
      \n
    • 多态是面向对象编程的一大特性,也是面向对象编程语言的一种语法。它是一种代码实现的思路
    • \n
    • 里式替换是一种设计原则,是用来指导继承关系中子类该如何设计的,子类的设计要保证在替换父类的时候,不改变原有程序的逻辑以及不破坏原有程序的正确性。
    • \n
    \n

    所以,判断子类的设计实现是否违背里式替换原则,还有一个小窍门:那就是拿父类的单元测试去验证子类的代码。如果某些单元测试运行失败,就有可能说明,子类的设计实现没有完全地遵守父类的约定,子类有可能违背了里式替换原则。

    \n

    里氏替换原则示例 2

    我们来看个违反历史替换原则的例子:

    \n

    CircleSquare 继承了基类 Shape,然后在应用的方法中,根据输入 Shape 对象类型进行判断,根据对象类型选择不同的绘图函数将图形画出来。

    \n
    void drawShape(Shape shape) {
    if (shape.type == Shape.Circle ) {
    drawCircle((Circle) shape);
    } else if (shape.type == Shape.Square) {
    drawSquare((Square) shape);
    } else {
    ……
    }
    }
    \n

    这种写法的代码既常见又糟糕,它同时违反了开闭原则和里氏替换原则。

    \n
      \n
    • 首先看到这样的 if/else 代码,就可以判断违反了(我们刚刚在上个部分讲过的)开闭原则:当增加新的 Shape 类型的时候,必须修改这个方法,增加 else if 代码。
    • \n
    • 其次也因为同样的原因违反了里氏替换原则:当增加新的Shape 类型的时候,如果没有修改这个方法,没有增加 else if 代码,那么这个新类型就无法替换基类 Shape
    • \n
    \n

    要解决这个问题其实也很简单,只需要在基类 Shape 中定义 draw 方法,所有 Shape 的子类,CircleSquare 都实现这个方法就可以了:

    \n
    public abstract Shape{
    public abstract void draw();
    }
    \n

    上面那段 drawShape() 代码也就可以变得更简单:

    \n
    void drawShape(Shape shape) {
    shape.draw();
    }
    \n

    这段代码既满足开闭原则:增加新的类型不需要修改任何代码。也满足里氏替换原则:在使用基类的这个方法中,可以用子类替换,程序正常运行。

    \n

    如何在实践中遵循里氏替换原则

    子类在设计的时候,要遵守父类的行为约定(或者叫协议)。父类定义了函数的行为约定,那子类可以改变函数的内部实现逻辑,但不能改变函数原有的行为约定,这也是我们常说的「Design By Contract」,中文翻译就是「按照协议(契约、约定)来设计」。

    \n

    以下是三种常见的违背约定的情况:

    \n
      \n
    1. 子类违背父类声明要实现的功能
        \n
      • 如:父类中提供的 sortOrdersByAmount() 订单排序函数,是按照金额从小到大来给订单排序的,而子类重写这个 sortOrdersByAmount() 订单排序函数之后,是按照创建日期来给订单排序的。那子类的设计就违背里式替换原则。
      • \n
      \n
    2. \n
    3. 子类违背父类对输入、输出、异常的约定
        \n
      • 如:在父类中,某个函数约定:运行出错的时候返回 null;获取数据为空的时候返回空集合(empty collection)。而子类重载函数之后,实现变了,运行出错返回异常(exception),获取不到数据返回 null
      • \n
      • 在父类中,某个函数约定,输入数据可以是任意整数,但子类实现的时候,只允许输入数据是正整数,负数就抛出异常,也就是说,子类对输入的数据的校验比父类更加严格,那子类的设计就违背了里式替换原则。
      • \n
      • 在父类中,某个函数约定,只会抛出 ArgumentNullException 异常,那子类的设计实现中只允许抛出 ArgumentNullException 异常,任何其他异常的抛出,都会导致子类违背里式替换原则。
      • \n
      \n
    4. \n
    5. 子类违背父类注释中所罗列的任何特殊说明
        \n
      • 如:父类中定义的 withdraw() 提现函数的注释是这么写的:「用户的提现金额不得超过账户余额……」,而子类重写 withdraw() 函数之后,针对 VIP 账号实现了透支提现的功能,也就是提现金额可以大于账户余额
      • \n
      \n
    6. \n
    \n

    子类的协议不能比父类更严格,否则使用者在用子类替换父类的时候,就会因为更严格的协议而失败。

    \n

    在类的继承中,如果父类方法的访问控制是 protected,那么子类 override 这个方法的时候,可以改成是 public,但是不能改成 private。因为 private 的访问控制比 protected 更严格,能使用父类 protected 方法的地方,不能用子类的 private 方法替换,否则就是违反里氏替换原则的。相反,如果子类方法的访问控制改成 public 就没问题,即子类可以有比父类更宽松的协议。同样,子类 override 父类方法的时候,不能将父类的 public 方法改成 protected,否则会出现编译错误。

    \n

    实践中,当你继承一个父类仅仅是为了复用父类中的方法的时候,那么很有可能你离错误的继承已经不远了。一个类如果不是为了被继承而设计,那么最好就不要继承它。

    \n

    粗暴一点地说,如果不是抽象类或者接口,最好不要继承它

    \n

    如果你确实需要使用一个类的方法,最好的办法是组合这个类而不是继承这个类,这就是人们通常说的组合优于继承

    \n
    Class A{
    public Element query(int id){...}
    public void modify(Element e){...}
    }

    Class B{
    private A a;
    public Element select(int id){
    a.query(id);
    }
    public void modify(Element e){
    a.modify(e);
    }
    }
    \n

    接口隔离原则

    接口隔离原则的英文是 SInterface Segregation Principle,缩写为 ISP

    \n

    这个原则是说:客户端不应该强迫依赖它不需要的接口

    \n

    我们可以从三个角度理解「接口」:

    \n
      \n
    • 一组 API 接口集合
    • \n
    • 单个 API 接口或函数
    • \n
    • OOP 中的接口概念
    • \n
    \n

    下面我们逐个进行说明。

    \n

    把「接口」理解为一组 API 接口集合

    在设计微服务或者类库接口的时候,如果部分接口只被部分调用者使用,那我们就需要将这部分接口隔离出来,单独给对应的调用者使用,而不是强迫其他调用者也依赖这部分不会被用到的接口。

    \n

    举例说明:

    \n
    public interface UserService {
    boolean register(String cellphone, String password);
    boolean login(String cellphone, String password);
    UserInfo getUserInfoById(long id);
    UserInfo getUserInfoByCellphone(String cellphone);
    }

    public interface RestrictedUserService {
    boolean deleteUserByCellphone(String cellphone);
    boolean deleteUserById(long id);
    }

    public class UserServiceImpl implements UserService, RestrictedUserService {
    // ...省略实现代码...
    }
    \n

    删除用户是一个非常慎重的操作,我们只希望通过后台管理系统来执行,所以这个接口只限于给后台管理系统使用。如果我们把它放到 UserService 中,那所有使用到 UserService 的系统,都可以调用这个接口。不加限制地被其他业务系统调用,就有可能导致误删用户。

    \n

    参照接口隔离原则,调用者不应该强迫依赖它不需要的接口,将删除接口单独放到另外一个接口 RestrictedUserService 中,然后将 RestrictedUserService 只打包提供给后台管理系统来使用。

    \n

    把「接口」理解为单个 API 接口或函数

    隔离原则就可以理解为:函数的设计要功能单一,不要将多个不同的功能逻辑在一个函数中实现。

    \n

    接口隔离原则跟单一职责原则有点类似,不过稍微还是有点区别。

    \n
      \n
    • 单一职责原则针对的是模块、类、接口的设计
    • \n
    • 接口隔离原则相对于单一职责原则,一方面它更侧重于接口的设计,另一方面它的思考的角度不同
    • \n
    \n

    接口隔离原则提供了一种判断接口是否职责单一的标准:通过调用者如何使用接口来间接地判定。如果调用者只使用部分接口或接口的部分功能,那接口的设计就不够职责单一。

    \n

    举例说明:

    \n
    public class Statistics {
    private Long max;
    private Long min;
    private Long average;
    private Long sum;
    private Long percentile99;
    private Long percentile999;
    //...省略constructor/getter/setter等方法...
    }

    public Statistics count(Collection<Long> dataSet) {
    Statistics statistics = new Statistics();
    //...省略计算逻辑...
    return statistics;
    }
    \n

    在上面的代码中,count() 函数的功能不够单一,包含很多不同的统计功能,比如,求最大值、最小值、平均值等等。如果某个统计需求只涉及 Statistics 罗列的统计信息中一部分,而 count() 函数每次都会把所有的统计信息计算一遍,就会做很多无用功,势必影响代码的性能

    \n

    按照接口隔离原则,我们应该把 count() 函数拆成几个更小粒度的函数,每个函数负责一个独立的统计功能:

    \n
    public Long max(Collection<Long> dataSet) { //... }
    public Long min(Collection<Long> dataSet) { //... }
    public Long average(Colletion<Long> dataSet) { //... }
    // ...省略其他统计函数...
    \n

    把「接口」理解为 OOP 中的接口概念

    接口的设计要尽量单一,不要让接口的实现类和调用者,依赖不需要的接口函数或方法。

    \n

    使用接口隔离原则,就是定义多个接口,不同调用者依赖不同的接口,只看到自己需要的方法。而实现类则实现这些接口,通过多个接口将类内部不同的方法隔离开来。

    \n

    那么如果强迫用户依赖他们不需要的方法,会导致什么后果呢?

    \n
      \n
    • 一来,用户可以看到这些他们不需要,也不理解的方法,这样无疑会增加他们使用的难度,如果错误地调用了这些方法,就会产生 bug。
    • \n
    • 二来,当这些方法如果因为某种原因需要更改的时候,虽然不需要但是依赖这些方法的用户程序也必须做出更改,这是一种不必要的耦合。
    • \n
    \n

    举例说明:

    \n

    把「接口」理解为 OOP 中的接口概念

    假如我们需要开发一个支持根据远程配置中心配置来动态更改缓存配置的缓存服务。

    \n

    \"\"

    \n

    这个缓存服务 Client 类的方法主要包含两个部分:

    \n
      \n
    • 一部分是缓存服务方法,get()put()delete() 这些,这些方法是面向调用者的
    • \n
    • 另一部分是配置更新方法 reBuild(),这个方法主要是给远程配置中心调用的
    • \n
    \n

    但是问题是,Cache 类的调用者如果看到 reBuild() 方法,并错误地调用了该方法,就可能导致 Cache 连接被错误重置,导致无法正常使用 Cache 服务。所以必须要将 reBuild() 方法向缓存服务的调用者隐藏,而只对远程配置中心的本地代理开放这个方法。

    \n

    我们可以进行如下调整:

    \n

    实现类同时实现 Cache 接口和 CacheManageable 接口,其中 Cache 接口提供标准的 Cache 服务方法,应用程序只需要依赖该接口。而 CacheManageable 接口则对外暴露 reBuild() 方法。

    \n

    \"\"

    \n

    使用接口隔离原则,就是定义多个接口,不同调用者依赖不同的接口,只看到自己需要的方法。而实现类则实现这些接口,通过多个接口将类内部不同的方法隔离开来。

    \n

    依赖倒置原则

    \n

    单一职责原则和开闭原则的原理比较简单,但是,想要在实践中用好却比较难。而依赖倒置原则正好相反。依赖倒置原则用起来比较简单,但概念理解起来比较难。

    \n
    \n

    依赖倒置原则的英文是 Dependency Inversion Principle,缩写为 DIP

    \n

    依赖倒置原则说的是:高层模块不依赖低层模块,它们共同依赖同一个抽象,这个抽象接口通常是由高层模块定义,低层模块实现。同时抽象不要依赖具体实现细节,具体实现细节依赖抽象。

    \n

    所谓高层模块和低层模块的划分,简单来说就是,在调用链上,调用者属于高层,被调用者属于低层

    \n

    在具体讲解依赖倒置原则前,我们先来看几个与之有关的常见概念:控制反转、依赖注入、依赖注入框架。

    \n

    控制反转(IOC)

    控制反转是一个比较笼统的设计思想,并不是一种具体的实现方法,一般用来指导框架层面的设计

    \n

    框架提供了一个可扩展的代码骨架,用来组装对象、管理整个执行流程。程序员利用框架进行开发的时候,只需要往预留的扩展点上,添加跟自己业务相关的代码,就可以利用框架来驱动整个程序流程的执行。

    \n

    这里的「控制」指的是对程序执行流程的控制,而「反转」指的是在没有使用框架之前,程序员自己控制整个程序的执行。在使用框架之后,整个程序的执行流程可以通过框架来控制。流程的控制权从程序员「反转」到了框架。

    \n

    我们举个例子来看一下:

    \n
    public class UserServiceTest {
    public static boolean doTest() {
    // ...
    }

    public static void main(String[] args) {//这部分逻辑可以放到框架中
    if (doTest()) {
    System.out.println("Test succeed.");
    } else {
    System.out.println("Test failed.");
    }
    }
    }
    \n

    在上面的代码中,所有的流程都由程序员来控制。如果我们抽象出一个下面这样一个框架,我们再来看,如何利用框架来实现同样的功能。

    \n
    public abstract class TestCase {
    public void run() {
    if (doTest()) {
    System.out.println("Test succeed.");
    } else {
    System.out.println("Test failed.");
    }
    }

    public abstract boolean doTest();
    }

    public class JunitApplication {
    private static final List<TestCase> testCases = new ArrayList<>();

    public static void register(TestCase testCase) {
    testCases.add(testCase);
    }

    public static final void main(String[] args) {
    for (TestCase case: testCases) {
    case.run();
    }
    }
    \n

    把这个简化版本的测试框架引入到工程中之后,我们只需要在框架预留的扩展点,也就是 TestCase 类中的 doTest() 抽象函数中,填充具体的测试代码就可以实现之前的功能了,完全不需要写负责执行流程的 main() 函数了。

    \n
    public class UserServiceTest extends TestCase {
    @Override
    public boolean doTest() {
    // ...
    }
    }

    // 注册操作还可以通过配置的方式来实现,不需要程序员显示调用register()
    JunitApplication.register(new UserServiceTest();
    \n

    控制反转的方式有很多,除了依赖注入,还有模板模式等,我们常用的 Spring 框架主要是通过依赖注入来实现的控制反转。

    \n

    下面我们来看看依赖注入。

    \n

    依赖注入(DI)

    依赖注入跟控制反转恰恰相反,它是一种具体的编码技巧

    \n

    依赖注入用一句话来概括就是:不通过 new() 的方式在类内部创建依赖类对象,而是将依赖的类对象在外部创建好之后,通过构造函数、函数参数等方式传递(或注入)给类使用。

    \n

    这里给出一个例子,分别用非依赖注入和依赖注入来实现同一个需求:Notification 类负责消息推送,依赖 MessageSender 类实现推送商品促销、验证码等消息给用户。

    \n

    代码如下:

    \n
    // 非依赖注入实现方式
    public class Notification {
    private MessageSender messageSender;

    public Notification() {
    this.messageSender = new MessageSender(); //此处有点像hardcode
    }

    public void sendMessage(String cellphone, String message) {
    //...省略校验逻辑等...
    this.messageSender.send(cellphone, message);
    }
    }

    public class MessageSender {
    public void send(String cellphone, String message) {
    //....
    }
    }
    // 使用Notification
    Notification notification = new Notification();

    // 依赖注入的实现方式
    public class Notification {
    private MessageSender messageSender;

    // 通过构造函数将messageSender传递进来
    public Notification(MessageSender messageSender) {
    this.messageSender = messageSender;
    }

    public void sendMessage(String cellphone, String message) {
    //...省略校验逻辑等...
    this.messageSender.send(cellphone, message);
    }
    }
    //使用Notification
    MessageSender messageSender = new MessageSender();
    Notification notification = new Notification(messageSender);
    \n

    通过依赖注入的方式来将依赖的类对象传递进来,这样就提高了代码的扩展性,我们可以灵活地替换依赖的类(将 MessageSender 定义成接口)。

    \n

    依赖注入框架(DI Framework)

    在实际的软件开发中,一些项目可能会涉及几十、上百、甚至几百个类,类对象的创建和依赖注入会变得非常复杂。如果这部分工作都是靠程序员自己写代码来完成,容易出错且开发成本也比较高。而对象创建和依赖注入的工作,本身跟具体的业务无关,我们完全可以抽象成框架来自动完成。

    \n

    这个框架就是「依赖注入框架」。我们只需要通过依赖注入框架提供的扩展点,简单配置一下所有需要创建的类对象、类与类之间的依赖关系,就可以实现由框架来自动创建对象、管理对象的生命周期、依赖注入等原本需要程序员来做的事情。

    \n

    常见的依赖注入框架有:Google Guice、Java Spring、Pico Container、Butterfly Container 等。

    \n
    \n

    框架的一个特点是,当开发者使用框架开发一个应用程序时,无需在程序中调用框架的代码,就可以使用框架的功能特性。比如:

    \n
    \n
      \n
    • 程序不需要调用 Spring 的代码,就可以使用 Spring 的依赖注入、MVC 这些特性,开发出低耦合、高内聚的应用代码
    • \n
    • 程序不需要调用 Tomcat 的代码,就可以监听HTTP 协议端口,处理 HTTP 请求
    • \n
    \n

    依赖倒置原则(DIP)

    最后回到我们这部分的主角。

    \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 层提供的接口,但这种依赖并不是依赖倒置原则!在依赖倒置原则中,除了具体实现要依赖抽象,最重要的是,抽象是属于谁的抽象

    \n

    最后再举一个依赖倒置原则的例子:

    \n

    Button 按钮控制 Lamp 灯泡,按钮按下的时候,灯泡点亮或者关闭。按照常规的设计思路,我们可能会设计出如下的类图关系,Button 类直接依赖 Lamp 类。

    \n

    \"\"

    \n

    这样设计的问题在于,Button 依赖 Lamp,那么对 Lamp 的任何改动,都可能会使 Button 受到牵连,做出联动的改变。同时,我们也无法重用 Button 类。

    \n

    解决之道就是将这个设计中的依赖于实现,重构为依赖于抽象。这里的抽象就是:打开关闭目标对象。

    \n
      \n
    • Button 定义一个抽象接口 ButtonServer,在 ButtonServer 中描述抽象:打开、关闭目标对象
    • \n
    • 由具体的目标对象,比如 Lamp 实现这个接口,从而完成 Button 控制 Lamp 这一功能需求
    • \n
    \n

    \"\"

    \n

    通过这样一种依赖倒置,Button 不再依赖 Lamp,而是依赖抽象 ButtonServer,而 Lamp 也依赖 ButtonServer,高层模块和低层模块都依赖抽象。Lamp 的改动不会再影响 Button,而 Button 可以复用控制其他目标对象,比如电机,或者任何由按钮控制的设备,只要这些设备实现 ButtonServer 接口就可以了。

    \n

    依赖倒置原则也被称为好莱坞原则:Don’t call me,I will call you.

    \n

    遵循依赖倒置原则有这样几个编码守则:

    \n
      \n
    1. 应用代码中多使用抽象接口,尽量避免使用那些多变的具体实现类。
    2. \n
    3. 不要继承具体类,如果一个类在设计之初不是抽象类,那么尽量不要去继承它。对具体类的继承是一种强依赖关系,维护的时候难以改变。
    4. \n
    5. 不要重写(Override)包含具体实现的函数。
    6. \n
    \n
    \n

    软件开发有时候像变魔术一样,常常表现出违反常识的特性,让人目眩神晕,而这正是软件编程这门艺术的魅力所在,感受到这种魅力,在自己的软件设计开发中体现出这种魅力,你就迈进了软件高手的大门。

    \n
    \n

    参考

    极客时间:后端技术面试38讲设计模式之美

    \n"},{"title":"SQLAlchemy 分表实践","url":"/2017/SQLAlchemy-%E5%88%86%E8%A1%A8%E5%AE%9E%E8%B7%B5/","content":"

    去年年底利用工作之余开发了一个进销存相关的 SAAS 项目,ORM 用的 SQLAlchemy,并且进行了一些分表操作,这里来做个简单的记录(也只能是简单记录了,我是小半年前进行的分表调研)。

    \n

    我没有直接使用 继承自 db.Model 的 ORM 类来操作数据库,而是在其之上又封装了一层,将更具体的一些数据库操作进行了封装。

    \n

    举个例子,我有一个继承自 db.ModelItem 类,同时还有一个自己的 Item 类,然后在自己的 Item 类中引用继承自 db.Model 的类,为了防止名称冲突,在引用时我会将继承自 db.Model 的类叫做 SqlaItem

    \n

    分表是如何实现的呢,我通过 SQLAlchemyautomap_base() 将数据库中所有表进行了映射,然后自己实现了分表函数,通过分表函数得到分表的名称,然后动态的拿到那个表所对应的 ORM。

    \n

    直接看代码:

    \n
    # -*- coding: utf-8 -*-

    from __future__ import unicode_literals, absolute_import

    from sqlalchemy import create_engine
    from sqlalchemy.ext.automap import automap_base

    from fuxi.config import SQLALCHEMY_DATABASE_URI

    engine = create_engine(SQLALCHEMY_DATABASE_URI)


    def ab_cls():
    ab_cls = automap_base()
    ab_cls.prepare(engine, reflect=True)
    return ab_cls


    def _get_ab_cls():
    if not getattr(_get_ab_cls, '_ab_cls', None):
    _ab_cls = ab_cls()
    _get_ab_cls._ab_cls = _ab_cls
    return _get_ab_cls._ab_cls

    ab_cls = _get_ab_cls()
    \n

    所有分了表的类,都要通过 ab_cls 来获取表映射出来的对象。

    \n

    还是以 Item 为例,看一下我的相关代码

    \n
    @classmethod
    def _get_db_branch_name_by_user(cls, user_id):
    branch = str(user_id)[-1]
    return branch

    @classmethod
    def _get_item_dao(cls, branch_name):
    tablename = 'item_%s' % branch_name
    dao = getattr(ab_cls.classes, tablename)
    return dao

    @classmethod
    def get_dao(cls, user_id):
    branch = cls._get_db_branch_name_by_user(user_id)
    return cls._get_item_dao(branch)
    \n

    我通过这几个方法实现了获取分表映射的功能,在具体使用时,可以直接用 get_dao(user_id) 获取表映射(我是通过用户ID的规则进行的分表)。

    \n

    随便看一个操作:

    \n
    @classmethod
    def gets_by_name(cls, user_id, name):
    SqlaItem = cls.get_dao(user_id)

    cond = (SqlaItem.user_id == user_id)
    cond &= (SqlaItem.status != ItemStatus.DELETE.value)
    if name:
    cond &= (SqlaItem.name == name)
    query = db.session.query(SqlaItem).filter(cond)
    return [cls.init_from_sqla(x) for x in query]
    \n

    我先通过 get_dao(user_id) 获取到这个用户数据所在的表的映射,然后就可进行各种 CURD 操作了。

    \n

    也就是说,在我的项目中其实有两种获取 SqlaXxxx 的方法,如果没有分表,那么直接用继承自 db.Model 的类即可,如果是分了表的,就用动态映射出来的,所以后者实际上是不需要写继承自 db.Model 的类的,但是为了在初始化时生成所有表结构,我还是写了这些类,只不过这些类所对应的表都是分表中的第一张表,以 Item 为例

    \n
    class Item(db.Model):
    """
    商品
    分表策略: 用户ID最后 1 位
    """
    __tablename__ = 'item_1'
    ...
    \n

    这样的话,我在执行 create_db 时所有需要分表的第一个表都会被建好,这个时候,我只需要再写个简单的脚本,就可以帮我把剩余的表建出来了,因为我所有分表结尾都是以下划线1或者01组成的,意思是,如果表需要分成 10 个,那么对应的第一个表的名称就是 xxx_1,如果需要分成 100 个,那么对应的第一个表的名称就是 xxx_01,所以我根据这个规则写了生成剩余表的脚本,如下:

    \n
    # -*- coding: utf-8 -*-

    from __future__ import unicode_literals, absolute_import

    import os
    from urlparse import urlparse

    import MySQLdb

    from envcfg.json.fuxi import SQLALCHEMY_DATABASE_URI

    p = urlparse(SQLALCHEMY_DATABASE_URI)

    DATABASE_HOST = p.hostname
    DATABASE_USER = p.username
    DATABASE_PASSWD = p.password if p.password else ""


    def _get_table_name(table):
    table_name_start = table.find('`') + 1
    table_name_end = table.find('`', table_name_start)
    return table[table_name_start:table_name_end]


    def create_table(table):
    """
    首先根据表名确定当前表是否需要分表
    如果表名是已1结尾,那么就创建0-9结尾的表
    如果表名是已01结尾,那么就创建00-99结尾的表
    如果以上情况都不是,直接创建
    """
    db = MySQLdb.connect(DATABASE_HOST, DATABASE_USER, DATABASE_PASSWD, "fuxi")
    cursor = db.cursor()

    assert table.startswith('CREATE TABLE')
    table_name = _get_table_name(table)
    if table_name.endswith('01'):
    replace_table = table.replace(table_name, '%s')
    for i in range(100):
    sharding_table_name = '%s_%02d' % (table_name[:-3], i)
    try:
    cursor.execute(replace_table % sharding_table_name)
    print '%s create success' % sharding_table_name
    except Exception as e:
    if e.args[0] != 1050:
    raise e
    print '%s exsit' % sharding_table_name
    elif table_name.endswith('1'):
    replace_table = table.replace(table_name, '%s')
    for i in range(10):
    sharding_table_name = '%s_%s' % (table_name[:-2], i)
    try:
    cursor.execute(replace_table % sharding_table_name)
    print '%s create success' % sharding_table_name
    except Exception as e:
    if e.args[0] != 1050:
    raise e
    print '%s exsit' % sharding_table_name
    else:
    try:
    cursor.execute(table)
    print '%s create success' % table_name
    except Exception as e:
    if e.args[0] != 1050:
    raise e
    print '%s exsit' % table_name
    db.close()


    def gets_all_tables():
    with open(os.path.dirname(os.path.realpath(__file__)) + '/fuxi.sql', 'r') as f:
    content = f.read()
    tables = content.split('\\n\\n')
    for table in tables:
    create_table(table)

    if __name__ == '__main__':
    gets_all_tables()
    \n

    这种方式有个弊端,在项目启动时就需要将所有表结构读入到内存中,直接的表现是启动比较慢,占用内存比较多。

    \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
    \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

    待续。。。

    \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 中可以基于 ControllerAdviceHttpMessageConverter 实现对数据返回的包装。

    \n

    实现如下,先来写一个 POJO 来定义一下返回格式:

    \n
    import com.example.demo.common.exception.base.ErrorCode;
    import lombok.AllArgsConstructor;
    import lombok.Getter;
    import org.springframework.http.HttpStatus;

    @Getter
    @AllArgsConstructor
    public class Response<T> {

    private int code = HttpStatus.OK.value();

    private String msg = \"success\";

    private T data;

    public Response(T data) {
    this.data = data;
    }

    public Response(int code, String msg) {
    this.code = code;
    this.msg = msg;
    }

    public Response(int code, T data) {
    this.code = code;
    this.data = data;
    }

    public Response(ErrorCode errorCode) {
    this.code = errorCode.getCode();
    this.msg = errorCode.getMessage();
    }

    public Response(ErrorCode errorCode, T data) {
    this.code = errorCode.getCode();
    this.msg = errorCode.getMessage();
    this.data = data;
    }
    }
    \n
    \n

    这里用到了 lomboklombok 的使用介绍不在本文范围内。

    \n
    \n

    用一个 ResponseBodyAdvice 类的实现包装 Controller 的返回值:

    \n

    以下是我以前的实现方式:

    \n
    import com.example.demo.common.RequestContextHolder;
    import com.fasterxml.jackson.databind.ObjectMapper;
    import org.slf4j.Logger;
    import org.slf4j.LoggerFactory;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.core.MethodParameter;
    import org.springframework.core.annotation.Order;
    import org.springframework.http.MediaType;
    import org.springframework.http.server.ServerHttpRequest;
    import org.springframework.http.server.ServerHttpResponse;
    import org.springframework.web.bind.annotation.ControllerAdvice;
    import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice;

    @ControllerAdvice
    public class FormatResponseBodyAdvice implements ResponseBodyAdvice {
    private static Logger logger = LoggerFactory.getLogger(FormatResponseBodyAdvice.class);

    @Autowired
    private ObjectMapper objectMapper;

    @Override
    public boolean supports(MethodParameter returnType, Class converterType) {
    return true;
    }

    @Override
    public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType, Class selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) {

    Object wrapperBody = body;
    try {
    if (!(body instanceof Response)) {
    if (body instanceof String) {
    wrapperBody = objectMapper.writeValueAsString(new Response<>(body));
    } else {
    wrapperBody = new Response<>(body);
    }
    }
    } catch (Exception e) {
    logger.error(\"request uri path: {}, format response body error\", request.getURI().getPath(), e);
    }
    return wrapperBody;
    }

    }
    \n

    为什么要对返回类型是 String 时进行特殊处理呢?因为如果直接返回 new Response<>(body) 的话,在使用时返回 String 类型的话,会报类型转换异常,当时也没有理解什么原因导致的,所以最后使用了 jacksonResponse 又做了一次序列化。

    \n

    今天找到了导致这个异常的原因:

    \n
    \n

    因为在所有的 HttpMessageConverter 实例集合中,StringHttpMessageConverter 要比其它的 Converter 排得靠前一些。我们需要将处理 Object 类型的 HttpMessageConverter 放得靠前一些,这可以在 Configuration 类中完成:

    \n
    \n
    import org.springframework.context.annotation.Configuration;
    import org.springframework.http.converter.HttpMessageConverter;
    import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
    import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

    import java.util.List;

    @Configuration
    public class WebConfiguration implements WebMvcConfigurer {

    @Override
    public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
    converters.add(0, new MappingJackson2HttpMessageConverter());
    }
    }
    \n

    然后 FormatResponseBodyAdvice 就可以修改为如下实现:

    \n
    import org.springframework.core.MethodParameter;
    import org.springframework.http.MediaType;
    import org.springframework.http.server.ServerHttpRequest;
    import org.springframework.http.server.ServerHttpResponse;
    import org.springframework.http.server.ServletServerHttpRequest;
    import org.springframework.web.bind.annotation.ControllerAdvice;
    import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice;


    @ControllerAdvice
    public class FormatResponseBodyAdvice implements ResponseBodyAdvice {

    @Override
    public boolean supports(MethodParameter returnType, Class converterType) {
    return true;
    }

    @Override
    public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType,
    Class selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) {

    if (!(body instanceof Response)) {
    return new Response<>(body);
    }

    return body;

    }
    }
    \n

    比之前的实现方式优雅了很多而且不用再处理 jackson 的异常了。

    \n

    写一个 Controller 来尝试一下:

    \n
    @RestController
    public class HelloController {

    @GetMapping("/hello")
    public String hello() {
    return "hello world!";
    }

    }
    \n

    请求这个端点得到结果:

    \n
    {
    "code": 200,
    "msg": "success",
    "data": "hello world!"
    }
    \n

    说明我们的配置是成功的,同时可以在相应头中看到:

    \n
    content-type: application/json;charset=UTF-8
    \n

    如果是之前的实现方式,这里的值就是:

    \n
    content-type: html/text
    \n

    也不太符合 restful 规范。

    \n"},{"title":"基于 Feign 实现强类型接口","url":"/2020/Strongly-type-interface-based-on-Feign/","content":"

    强弱类型语言

    众所周知编程语言有强弱类型之分,进一步还有动态和静态之分。比如 Java、C# 是强类型的(strongly typed)静态语言,Javascript、PHP 是弱类型的(weakly typed)动态语言。

    \n

    强类型静态语言常常被称为类型安全(type safe)语言,一般在会编译期间进行强制类型检查,提前避免一些类型错误。弱类型动态语言虽然也有类型的概念,但是比较松散灵活,而且大多是解释型语言,一般没有强制类型检查,类型问题一般要在运行期才能暴露出来。

    \n

    强弱类型的语言各有优劣、相互补充,各有适用的场景。比如服务端开发经常用强类型的,前端 Web 界面经常会用 Javascript 这种弱类型语言。

    \n

    \"1.png\"

    \n

    强弱类型 API

    对于服务 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

    \"2.png\"

    \n

    当然以上区分并不是业界标准,只是个人基于经验总结出来的一种区分的方法。

    \n

    强弱类型 API 优劣

    强类型服务接口的好处是:接口规范、自动代码生成、自动编码解码、编译期自动类型检查。强类型接口的好处也带来不利的一面:首先是客户端和服务端强耦合,任何一方升级改动可能会造成另一方 break,另外自动代码生成需要工具支持,而开发这些工具的成本也比较高。其次强类型接口开发测试不太友好,一般的浏览器、Postman 这样的工具无法直接访问强类型接口。

    \n

    弱类型服务接口的好处是客户端和服务器端不强耦合,不需要开发特别的代码生成工具,一般的 HTTP Client就可以调用,开发测试友好,不同的浏览器、Postman 可以轻松访问。弱类型服务接口的不足是需要调用方手动编码解码消息、没有自动代码的生成、没有编译器接口类型检查、代码不容易规范、开发效率相对低,而且容易出现运行期的错误。

    \n

    有没有办法结合强弱类型服务接口各自的好处同时又规避他们的不足呢?

    \n

    我们的做法是在 Spring Rest 弱类型接口的基础上借助 Spring Feign 支持的强类型接口的特性实现强类型 Rest 接口的调用机制,同时兼备强弱类型接口的好处。

    \n

    首先我们来介绍下 Spring FeignSpring Feign 本质上是一种动态代理机制(Dynamic Proxy),只需要我们给出 Restful API 对应的 Java 接口,它就可以在运行期动态的拼装出对应接口的强类型客户端。拼装出的客户端的结构和请求响应流程如下图所示:

    \n

    \"3.png\"

    \n
      \n
    1. 客户应用发起一个请求并传入一个 Request Bean,这个请求通过 Java 接口首先被动态代理截获
    2. \n
    3. 通过相应的编码器(Encoder)进行编码,成为 Request Json
    4. \n
    5. Request Json 根据需要可以经过一些拦截器(Interceptor)做进一步处理
    6. \n
    7. 处理完之后传递给 HTTP Client,HTTP Client 将 Request Json通过 HTTP 协议发送至服务器端
    8. \n
    9. 当服务端响应回来后,相应的 Response Json 会被 HTTP Client 接收到
    10. \n
    11. 经过一些拦截器做一些响应处理
    12. \n
    13. 转发给解码器(Decoder)解码为 Response Bean
    14. \n
    15. 最后 Response Bean 通过 Java 接口返回给调用方
    16. \n
    \n

    整个请求响应流程的关键步骤是编码和解码,也就是 Java 对象和 Json 消息的互转,这个过程也被称为序列化和反序列化,另外一种叫法为「Json 对象绑定」。对于一个服务框架而言,序列化、反序列化器的性能对于服务框架性能影响是最大的,也就是说可以认为 DecoderEncoder 决定了服务框架总体的性能。

    \n

    虽然我们开发出来的服务是弱类型的 Restful 服务,但是因为有 Spring Feign 的支持,我们只要简单的给出一个强类型的 Java API 接口就自动获得了一个强类型客户端,也就是说利用 Spring Feign 我们可以同时获得强弱类型的好处(编译器自动类型检查、不需要手动编码解码、不需要开发代码生成工具、客户端和服务器端不强耦合),这样可以同时规范代码风格,提升开发测试效率。

    \n

    我们可以在项目内为每个微服务提供两个模块,一个是 API 接口模块(如 mail-api),另一个是服务实现模块(如 mail-svc)。API接口模块内就是强类型的 Java API 接口(包括请求响应的 DTO),可以直接被 Spring Feign 引用并动态拼装出强类型客户端。

    \n

    项目结构如下:

    .
    ├── README.md
    ├── account
    │   ├── Dockerfile
    │   ├── pom.xml
    │   └── src
    │   ├── main
    │   └── test
    ├── account-api
    │   ├── pom.xml
    │   └── src
    │   └── main
    ├── mail
    │   ├── Dockerfile
    │   ├── mail.iml
    │   ├── pom.xml
    │   └── src
    │   ├── main
    │   └── test
    ├── mail-api
    │   ├── pom.xml
    │   └── src
    │   └── main
    └── pom.xml
    \n
    \n

    注:我这里没有采用 xxx-apixx-svc 的命名方式,直接是 xxx-api 表示 API Client 模块, xxx 为服务实现模块。

    \n
    \n

    我们以用户注册后发送通知邮件来写一个简单的例子。发送邮件 client 定义如下:

    // mail-api/src/main/java/com/demo/mail/client/MailClient.java

    import com.demo.common.api.dto.Response;
    import com.demo.mail.dto.EmailSendDTO;
    import org.springframework.cloud.openfeign.FeignClient;
    import org.springframework.web.bind.annotation.PostMapping;
    import org.springframework.web.bind.annotation.RequestBody;

    import javax.validation.Valid;

    @FeignClient(name = "mail")
    public interface MailClient {
    @PostMapping(path = "/send")
    Response<Boolean> send(@RequestBody @Valid EmailSendDTO mailSendDTO);
    }
    \n

    其中 Response 定义如下:

    @Data
    public class Response<T> {

    private int code = 0;

    private String message = "OK";

    private T data;

    }
    \n

    用户服务调用发邮件 API 实现如下:

    // account/src/main/java/com/demo/account/service/UserService.java

    public class UserService {

    @Autowired
    private final UserRepository userRepository;

    @Autowired
    private MailClient mailClient;

    public boolean register(UserDTO userVO) {
    // 忽略参数验证部分代码...

    User user= userRepository.save(UserDTOConvert.convertTo(userVO));

    EmailSendDTO mail = EmailSendDTO.builder()
    .to("user.getEmail()")
    .subject("welcome!")
    .htmlBody("hello," + user.getName()).build();
    try {
    Response<Boolean> response = mailClient.send(mail);
    } catch (Exception e) {
    log.error(e.getMessage());
    throw new AppException(SysErrorEnum.SYSTEM_ERROR);
    }

    if (response.getCode() != 0) {
    throw new ServiceException(response.getCode(), response.getMessage());
    } else if (!response.getData()) {
    throw new ServiceException(AccountErrorEnum.MAIL_SEND_ERROR);
    }

    return true;
    }
    }
    \n

    为了体现异常处理流程,上边代码仅用于演示,在生产环境下发邮件应该为异步处理,无需检查发送结果。我们在服务内加了全局异常处理,所以直接向上抛出即可。

    \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跟下边同学沟通,但我发现我还是没有太多可说的,我缺乏绩效沟通时的基本技巧,加上平时的思考比较少,(而且不擅长画饼),很容易冷场,在对方问一些刁钻或者我没有考虑过的问题的时候脑子里很容易就出现一片空白或者像一团浆糊,这让我想到读过的「一句顶一万句」这部小说中经常用的闷嘴葫芦这个词。

    \n

    还好和我配合的这位虚线TL足够有经验,基本都是他来对线,我继续做辅助,很幸运每次我遇到困难时都会有贵人相助。

    \n

    另外因为上个季度离职的同学比较多,只需要和4位同学沟通就行,我这个资深 I 人在每沟通完一个人后都很疲惫,需要自己待一会来充电,想到下个季度要沟通8个同学就有些头大🥲

    \n"},{"title":"4个最重要的企业财务指标","url":"/2023/The-4-most-important-business-financial-indicators/","content":"

    当谈到财务指标时,净资产收益率、毛利率、净利率和市盈率是经常被提及的几个指标,这些也是巴菲特最看重的4个指标。

    \n

    巴菲特是历史上最伟大的价值投资者,他的交易逻辑的核心是:寻找优质企业并长期持有这些企业的股票。如何判断一个企业是否优质?这时就可以依据上边提到的4个指标来进行判断了。

    \n

    净资产收益率(Return on Equity,ROE)

    这个指标可以帮助投资者评估企业的盈利能力和管理效率

    \n

    净资产收益率是用来衡量企业利润与其净资产之间关系的指标。

    \n

    它反映了企业利用所有者权益实现的盈利能力。

    \n

    净资产收益率的计算公式为:净资产收益率 = 净利润 / 平均净资产。

    \n
      \n
    • 净利润

      \n

      指的是企业在一定时期内扣除所有成本和费用后所剩下的利润。

      \n
        \n
      • 它是企业经营活动的最终利润。
      • \n
      • 净利润可以通过企业的损益表(或利润表)中的数据来计算得出。
      • \n
      \n
    • \n
    • 平均净资产

      \n

      是指企业在一定期间内的

      \n

      资产净值的平均值

      \n

      \n
        \n
      • 资产净值指的是企业的资产减去负债,也可以理解为企业的所有者权益或净资产
      • \n
      • 平均净资产则是将期初净资产和期末净资产相加后除以2,表示在该期间内的平均资产净值。
      • \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
    1. 盈利能力评估:净资产收益率反映了企业在一定期间内每单位净资产所创造的净利润水平。较高的净资产收益率意味着企业能够有效地利用其资产创造盈利,表明企业在经营活动中取得了较高的利润回报。相反,较低的净资产收益率可能意味着企业的盈利能力较弱,资产利用效率不高。
    2. \n
    3. 管理效率评估:净资产收益率反映了企业管理层对资产的运营和配置能力。较高的净资产收益率表明企业管理层能够有效地管理和运营资产,实现更高的利润水平。这可能反映了企业在生产、销售、成本控制等方面的优秀管理能力。相反,较低的净资产收益率可能暗示企业的管理效率较低,资产配置和运营方面存在问题。
    4. \n
    \n
    \n

    毛利率(Gross Profit Margin)

    这个指标可以帮助投资者了解企业的盈利能力和生产成本控制情况

    \n

    毛利率是用来衡量企业销售产品或提供服务后的毛利润与销售收入之间的关系的指标。

    \n

    它反映了企业在销售过程中所保留的利润比例。

    \n

    毛利率的计算公式为:毛利率 = 毛利润 / 销售收入。

    \n
      \n
    • 毛利润

      \n

      是指企业在销售产品或提供服务后剩余的销售收入减去与销售直接相关的成本。

      \n
        \n
      • 它表示企业在核心业务活动中所保留的利润。
      • \n
      • 毛利润 = 销售收入 - 与销售直接相关的成本
      • \n
      \n
    • \n
    • 销售收入

      \n

      是指企业在一定时期内通过销售产品或提供服务所获得的总收入。

      \n
        \n
      • 它代表了企业主要经营活动的收入来源,可以在企业的损益表中找到。
      • \n
      \n
    • \n
    \n

    如果一个行业的毛利率低于20%,那么几乎可以断定这个行业存在着过度竞争。

    \n

    净利率(Net Profit Margin)

    这个指标可以帮助投资者评估企业的盈利能力和经营效率

    \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
    1. 毛利率是企业销售产品或提供服务后的毛利润与销售收入之间的比例关系。毛利润是指销售收入减去直接与销售相关的成本,例如生产成本、原材料成本和直接人工成本等。毛利率的计算公式为:毛利率 = (销售收入 - 销售成本)/ 销售收入。毛利率衡量了企业从核心业务活动中获得的利润比例,它反映了企业在销售过程中所保留的利润比例。
    2. \n
    3. 净利率是企业净利润与销售收入之间的比例关系。净利润是指销售收入减去所有成本和费用,包括销售成本、管理费用、利息费用和税费等。净利润是企业最终实现的利润。净利率的计算公式为:净利率 = 净利润 / 销售收入。净利率衡量了企业在销售过程中实现的净利润比例,它反映了企业在营运活动中的盈利能力。
    4. \n
    \n

    总结起来,毛利率关注的是销售收入和与销售直接相关的成本之间的关系,它衡量了企业从核心业务中获得的利润比例。而净利率则考虑了所有成本和费用,包括销售成本以外的费用,衡量了企业在所有经营活动中实现的净利润比例。净利率相对于毛利率更全面地反映了企业的盈利能力和经营效率。

    \n
    \n

    市盈率(Price-to-Earnings Ratio,P/E Ratio)

    市盈率是衡量股票相对于每股盈利的价格的指标。

    \n

    它是投资者评估一家公司的股票是否被低估或高估的重要指标。

    \n

    市盈率的计算公式为:市盈率 = 股票市场价格 / 每股税后收益。

    \n
      \n
    • 股票市场价格指的是股票在市场上的交易价格,也就是投资者购买或出售股票所需支付或获得的价格。

      \n
    • \n
    • 每股税后收益

      \n

      是指企业每股普通股的税后净利润,也可以理解为每一股票所对应的盈利。

      \n
        \n
      • 计算公式为:企业的净利润 / 总发行的普通股数量
      • \n
      \n
    • \n
    \n

    较高的市盈率可能意味着市场对该股票有较高的期望和溢价,而较低的市盈率可能意味着市场对该股票的期望较低。

    \n

    举例

    假设有一家公司,它在某一年度的每股收益为10元,而股票的市场价格为100元。

    \n

    首先,计算市盈率: 市盈率 = 市场价格 / 每股收益 = 100元 / 10元 = 10倍

    \n

    在这个例子中,每股收益为10元,市场价格为100元。

    \n

    通过计算,得到市盈率为10倍。

    \n
    \n

    优秀企业四个指标的参考值

      \n
    • ROE > 20%
    • \n
    • 毛利率 > 40%
    • \n
    • 净利率 > 5%
    • \n
    • 市盈率 20-40 之间(按照A股标准计算得出)
    • \n
    \n
    \n

    净资产收益率高和净利率高的公司是否一定是好的投资对象?

    ROE高和净利率高通常被视为公司财务状况较好的指标,但并不意味着这些公司一定是好的投资对象。以下是一些需要考虑的因素:

    \n
      \n
    1. 行业和周期性:不同行业的盈利能力和周期性有所不同。即使一家公司的ROE和净利率很高,如果它所处的行业面临结构性问题或周期性低迷,那么这些指标的表现可能会受到影响。
    2. \n
    3. 可持续性:高ROE和净利率可能是暂时的,而非持续的。投资者需要评估这些指标是否具有持续性,例如公司的竞争优势、市场地位和可持续的盈利模式。
    4. \n
    5. 债务水平:高ROE和净利率的公司可能通过借入资金来实现这些指标,但高负债率可能增加公司的风险。因此,投资者需要关注公司的债务水平和偿债能力。
    6. \n
    7. 估值:高ROE和净利率的公司可能被市场高度看好,导致其股票价格被高估。投资者需要综合考虑股票的估值水平,以确定是否存在投资机会。
    8. \n
    9. 其他因素:除了财务指标,投资者还应考虑公司的管理团队、战略规划、产品竞争力、创新能力以及行业趋势等因素,这些因素对于评估公司的长期投资价值也是至关重要的。
    10. \n
    \n

    因此,高ROE和净利率只是投资决策的起点,而不是唯一的决策依据。投资者应该进行全面的研究和分析,以综合考虑多种因素,并结合自己的投资目标和风险承受能力做出决策。

    \n
    \n

    ROE 和 ROI 的区别

      \n
    1. ROE是用来衡量企业利用所有者权益(净资产)创造利润的能力。它的计算公式是净利润除以平均净资产。ROE衡量了股东权益的回报率,反映了企业在投入资本的同时,通过运营活动创造的盈利能力。ROE通常用于评估企业的盈利能力和资产利用效率。
    2. \n
    3. ROI(Return on Investment投资回报率)是用来衡量特定投资项目或资产的回报率。它的计算公式是净利润除以投资成本,并通常以百分比表示。ROI可以用于评估特定投资项目的经济效益,衡量投资的回报程度。ROI可以用于比较不同投资项目之间的收益率,帮助投资者做出投资决策。
    4. \n
    \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

    我最喜欢的句子

      \n
    • 人们习惯把别人的失败归咎于错误的决策,而把自己的失败归咎于糟糕的运气。
    • \n
    • 现代资本擅长创造两种东西:财富和嫉妒。
    • \n
    • 时间自由是财富能带给你的最大红利。
    • \n
    • 富有的最高级形式是,每天早上起床后你都可以说:“今天我能做我想做的任何事。”
    • \n
    • 通过用金钱购买昂贵之物获得的尊重和羡慕可能远比你想象中少。
    • \n
    • 历史是对变化的研究,但具有讽刺性的是,人们却将历史当作预测未来的工具。
    • \n
    • 杠杆——以负债的方式进行投资——把常规风险扩大到了足以导致毁灭的程度。
    • \n
    • 只有当你能给一项计划数年或数十年的时间去成长时,复利的效应才能得到最佳体现。
    • \n
    • 无论在工作生涯的哪个节点,都要定下这样均衡的目标:每年做好适中的储蓄,给自己适度的自由时间,让通勤不超过适当的时长,至少花适量的时间来陪伴家人。
    • \n
    • 如果你把波动看作要买的入场券,情况就会完全不同。
    • \n
    • 市场回报永远不会是免费的,现在不是,将来也不会是。你需要支付一定的费用,就像要花钱购买一件产品一样。
    • \n
    • 在做计划的时候,我们会专注于我们想做的和能做的事情,而忽略了他人的计划和能力,但他人的决策也会对结果产生影响。
    • \n
    • 用能让你睡踏实的方式来理财。
    • \n
    • 如果你想提高投资回报,最简单而有效的方法就是拉长时间。时间是投资中最强大的力量。
    • \n
    • 增长是由复利驱动的,而复利通常需要时间。毁灭却可能由独立的致命因素导致,可以在很短的时间内发生;它也可能由失去信心引发,而信心可以在一瞬间崩塌。
    • \n
    \n

    全部摘抄的句子

      \n
    • 一个无法控制个人情绪的天才或许会引发财务上的灾难,但反过来看——那些没有接受过专业金融教育的普通人,也可以凭借与智商衡量标准无关的良好行为习惯,最终走向富裕。
    • \n
    • 财务方面的成功并不是一门硬科学,而是一种软技能——你怎么做,比你掌握多少知识更重要。
    • \n
    • 有两种事物会影响每一个人,不管你是否对它们感兴趣——健康和金钱。
    • \n
    • 我认为,这种现象的主要原因是,我们思考和学习理财的方式更像学习物理的(涉及很多法则和定律),而不像学习心理学的(关注情感及其微妙变
    • \n
    • 关于金钱的知识和经验可以被用于生活中的其他许多问题,比如风险、信心和幸福中。很少有其他事物能像金钱这样,仿佛一面强有力的放大镜,帮助你理解人们为何会做出某些举动。
    • \n
    • 人类涉及金钱的行为是地球上最伟大的表演之一。
    • \n
    • 历史从来不会重复,人类却总会重蹈覆辙。
    • \n
    • 你对金钱的个人经验可能只有0.00000001%符合实际,但它构成了你对世界运作方式的主观判断的80%。
    • \n
    • 研究股市的历史后,你会觉得自己明白了某些事,但只有亲身经历过,感受过它的巨大影响,你才可能真正改变自己的行为
    • \n
    • 有些事只有真正经历过才会懂。
    • \n
    • 人们一生中的投资决策在很大程度上取决于其生活经历——尤其是成年后的早期经历。
    • \n
    • 每个人对金钱的体验都是不同的,即使是在那些你觉得经历很相似的人之间。
    • \n
    • 个体的不同经历可能导致他们对那些看似没有争议的话题出现完全不同的看法。
    • \n
    • 人们做的与金钱相关的每个决定都有其合理的一面,因为这些决定是他们在掌握了当时所能掌握的信息,然后将其纳入自己对世界运作方式的独特认知框架后做出的。
    • \n
    • 每个关于金钱的决定对当时的他们来说都是合理的,是建立在他们当时具备的条件之上的选择。
    • \n
    • 我们之所以经常在金钱方面做出看似疯狂的决策,是因为相较之下在这场游戏里我们都是新手,而在你看来不可理喻的行为对我而言却合乎情理。但是,没有谁真的失去了理智——我们都在依靠自己独特的经验做出选择,而这些经验在特定的时间点和情境下都是合理的。
    • \n
    • 生活中的每一个结果都受到个人努力之外的其他作用的影响。
    • \n
    • 任何事都没有表面看来那样美好或糟糕。
    • \n
    • 在生活这场游戏中起作用的除了我们自己,还有其他70亿人,同时还存在着无数的变量。那些在你控制之外的行为产生的意外影响可能比你有意识的行为产生的影响更大。
    • \n
    • 因为运气难以被量化,把他人的成功归咎于运气又是一种不礼貌的举动,所以我们大多数时候会自动忽略运气在成功中扮演的重要角色。
    • \n
    • 在评价别人时,将成就归功于运气会显得你很嫉妒和刻薄,哪怕我们知道的确存在运气的成分;而在评价自己时,将成就归功于运气则会令自己感到气馁,难以接受。
    • \n
    • 人们习惯把别人的失败归咎于错误的决策,而把自己的失败归咎于糟糕的运气
    • \n
    • 不要太关注具体的个人和案例研究,而要看到具有普适性的模式。
    • \n
    • 预防失败的诀窍是:做好你的财务规划,使其不至于因为一次糟糕的投资和未能达成的财务目标而全盘崩溃,保证自己能在投资道路上持续前进,一直等到好运降临的那一刻。
    • \n
    • 风险的存在也意味着在评价自身的失败时,我们应该原谅和理解自己。
    • \n
    • 为了赚他们并未拥有也不需要的钱,他们拿自己已经拥有并确实需要的东西去冒险了。这是愚蠢至极的做法。冒着失去重要东西的风险去争取并不重要的东西的行为毫无道理可言。
    • \n
    • 最难的理财技能是让逐利适可而止。
    • \n
    • 现代资本擅长创造两种东西:财富和嫉妒。
    • \n
    • 幸福是你拥有的减去你期待的。
    • \n
    • 攀比就像一场没有人能打赢的战役,取胜的唯一办法是根本不要加入这场战争——用知足的态度接受一切,即使这意味着自己比周围的人逊色
    • \n
    • 如果你无法拒绝潜在的金钱诱惑,那么欲望最终可能将你吞没。
    • \n
    • 一个领域里的知识和经验常常可以为其他领域提供重要的借鉴。
    • \n
    • 冰期形成的主要原因并非极寒的冬季,而是凉爽的夏季。
    • \n
    • 地球冰川形成的关键并不一定是大量的降雪,而是雪能累积下来,无论量有多少。
    • \n
    • 成功的投资并不需要你一直做出成功的决定。你只要做到一直不把事情搞砸就够了。
    • \n
    • 但守富的方式却只有一种:在保持节俭的同时,还需要一些谨小慎微。
    • \n
    • 致富和守富是两种完全不同的技能。
    • \n
    • 致富需要的是冒险精神、乐观心态,以及放手一搏的勇气。
    • \n
    • 守富需要谦逊和敬畏之心,需要清楚财富来得有多快,去得就有多容易。守富需要节俭,并要承认你获得的财富中一部分源自运气,所以不要指望无限复制过去的成功。
    • \n
    • 生存应该成为你一切策略的基础,无论是关于投资、规划个人职业还是经营生意的
    • \n
    • 没有任何收益值得你冒失去一切的风险。
    • \n
    • 你的财务规划要求的具体前提条件越多,你的财务状况就越脆弱。
    • \n
    • 从长远看结果是积极的,但从短期看过程可能很糟糕”这一点乍看之下不符合直觉,但生活中很多事确实是这样的。
    • \n
    • 经济、市场和个人职业生涯通常也会遵循一条相似的路径——在不断的损失中持续增长的过程。
    • \n
    • 对一个投资者来说,为了避免心态膨胀,付出再大的代价都是值得的。
    • \n
    • 当投资者持有这些藏品的时间足够长,这系列投资组合的整体收益就会趋近其中表现最好的部分的收益
    • \n
    • 一个投资者在一半的时间里都看走了眼,最后却仍然能致富,这个事实是不符合直觉的。它也意味着我们低估了许多事物失败的频率,所以当失败发生时,我们就会反应过度
    • \n
    • 任何规模巨大、利润丰厚、声名远播或影响力深远的事物都源自某个尾事件——从几千甚至几百万个事件中脱颖而出的一个。
    • \n
    • 拿破仑对军事天才的定义是“当身边所有人都进入非理性状态时还能继续正常行事的人”。
    • \n
    • “当下”其实并没有那么重要。作为投资者,你今天、明天或下周做的决定远不如你一生中个别几天做的决定重要。
    • \n
    • 一个投资天才也应该是一个当身边所有人都进入非理性状态时还能继续正常行事的人。
    • \n
    • 如果你是一个优秀的雇员,在经过三番五次的尝试和试验后,你终究会在适合自己的领域找到适合自己的公司。
    • \n
    • 当我们特别关注某些榜样的成功时,我们就会忽视这样一个事实:他们的成功来自他们全部行为中的一小部分。这种忽视会让我们觉得我们自己的失败、亏损和挫折是因为我们做错了什么。
    • \n
    • “重要的不是你对了还是错了,”“金融大鳄”乔治·索罗斯(George Soros)曾说,“而是当你对的时候,你能赚到多少,或者当你错的时候,你会损失多少。”你即使有一半的时间都在犯错,到最后依然能赢。
    • \n
    • 时间自由是财富能带给你的最大红利。
    • \n
    • 富有的最高级形式是,每天早上起床后你都可以说:“今天我能做我想做的任何事。”
    • \n
    • 幸福是一个复杂的话题,因为每个人的幸福观都不同,但如果幸福的分数有一个公分母——一种普遍的快乐源泉——那就是对生活的全面掌控。
    • \n
    • 在自己喜欢的任何时候和自己喜欢的对象做想做的事,而且想做多久就做多久,这样的自由是极其珍贵的,而这就是金钱能带给我们的最大红利
    • \n
    • 不是工资多少,不是房子大小,也不是工作好坏,而是对自己想做什么、什么时候做、和谁一起做拥有掌控能力。这是生活中决定幸福感的通用变量。
    • \n
    • 金钱最大的内在价值是它能赋予你掌控自己时间的能力——这句话没有任何夸张的成分。
    • \n
    • 拥有更多财富则意味着在失业后可以从容地等待更好的职业机会,而不必急于抓住遇到的第一根救命稻草。这种能力可以改变一个人的生活。
    • \n
    • 拥有更多财富则意味着可以选择一份待遇不高但时间灵活的,或是通勤时间比较短的工作
    • \n
    • 做一份自己喜欢却无法掌控时间的事和做自己讨厌的事没什么区别。
    • \n
    • 与前几代人相比,我们对时间的控制力降低了。正因为控制时间是影响幸福感的关键因素,所以我们无须对尽管现在的我们更富有了,但我们没有感到更快乐这一事实感到惊讶。
    • \n
    • 这里存在一个悖论:我们都想通过财富来告诉其他人,自己应该受到他们的爱慕与敬仰。但事实上,其他人常常会跳过敬仰你这一步。这并不是因为他们觉得你的财富不值得羡慕,而是因为他们会把你的财富当作标尺,转而表达自己渴望被爱慕与敬仰的愿望。
    • \n
    • 你或许觉得你需要一辆昂贵的车子、一块豪华的手表和一座很大的房子,但我想告诉你的是,你并非真想得到这些东西本身。你真想得到的是来自他人的尊重和羡慕。你觉得拥有昂贵的东西会让别人尊重和羡慕你,但可惜,别人不会——尤其是那些你希望得到其尊重和羡慕的人。
    • \n
    • 通过用金钱购买昂贵之物获得的尊重和羡慕可能远比你想象中少。
    • \n
    • 比起豪车,谦虚、善良和同情心等人格特质才能帮你获得更多尊重。
    • \n
    • 炫富是让财富流失的最快途径。
    • \n
    • 我们总是喜欢用看到的东西为标准来判断一个人是否富有,因为这些是摆在我们面前、实实在在的东西。
    • \n
    • 现代资本主义致力于帮助人们通过超前消费的方式来享受原本力不能及的物质生活,并将这种消费观发展为一个备受推崇的产业。
    • \n
    • 财富并不是我们能看到的外在部分。
    • \n
    • 财富是由未被转化为实物的金融资产体现的
    • \n
    • 让自己感到富有的最佳方式莫过于把大笔钱花在那些真正美好的东西上。但想真变得富有,你需要做的是花自己已经有的钱,而不是透支还不属于自己的钱。事情就是这么简单。
    • \n
    • 想真变得富有,唯一的途径就是别去消耗你拥有的财富。这不仅仅是积累财富的唯一方式,也是富有的真正定义
    • \n
    • 人们对一次身体锻炼所能燃烧的能量的估值比实际消耗的能量高了4倍,而他们接下来平均摄入的能量大约是运动中消耗的能量的2倍。
    • \n
    • 我们很容易找到有钱的人做榜样,但想找到富有的人却不容易,因为从性质上讲,他们的成功更隐蔽。
    • \n
    • 富有的前提其实是克制。
    • \n
    • 我们擅长通过模仿来学习,但财富看不见的特性让我们很难模仿和学习他人的经验。
    • \n
    • 这个世界上有很多看起来低调但实际上很富有的人,还有很多看上去很有钱却生活在破产边缘的人。
    • \n
    • 个人的节俭和储蓄行为——在金融方面的节约和高效——是金钱等式中你具备更强控制力的部分,而且在未来也会像今天一样,是百分百行得通的方法。
    • \n
    • 财富是对收入扣除开支后剩下的部分进行积累的结果。
    • \n
    • 即使你收入不高,你依然可以积累财富,但如果你的储蓄率不高,你绝不可能积累财富——两相对比,孰轻孰重显而易见。
    • \n
    • 如果你学会用更少的钱来获得同样多的幸福感,你的欲望和所得之间就会产生积极的落差。你也可以通过提升收入来造就这种落差,但欲望和所得之间的落差才是你更容易控制的。
    • \n
    • 但在金钱收支公式的两端,人们在一端投入了大量的精力,在另一端却鲜有作为。这就给了大多数人一个机会
    • \n
    • 当你把存款定义为虚荣的自我和收入之差时,你就能明白,为什么很多收入不低的人很难存下钱来,因为他们每天都在和自己想要尽情炫耀并与其他炫富者攀比的本能抗争。
    • \n
    • 在一个智力方面的竞争已经白热化,而很多旧有技术已经被自动化技术取代的世界里,竞争优势开始转向更加细微的软件层面,比如沟通能力、共情能力,以及最重要的一点——一个人的灵活度。
    • \n
    • 当智力不再是一种持久的优势时,拥有别人没有的灵活度是少数几种能帮你拉开与别人的距离的特质。
    • \n
    • 在做投资决策时,不要试图保持绝对理性,而要做出对你而言合乎情理,也就是更好接受的选择。
    • \n
    • 坚持对理财来说才是至关重要的一点。
    • \n
    • 医生的职责不是简单地治好病,而是使用能让病人接受的人性化手段治好病。
    • \n
    • 在影响收益表现(包括收益额和在一定时间内有所收益的概率)的诸多金融参数中,相关性最大的莫过于在经济不景气的年份对投资策略的长期坚持。
    • \n
    • 任何能让你留在投资游戏中的因素都会在时间方面增强你的优势。
    • \n
    • 如果你一开始就对投资对象很感兴趣——这家企业的使命、产品、团队和技术等方面都非常合你的口味——那么当它因为收益下滑或需要帮助而进入不可避免的低谷期时,你至少会因为感到自己在做一件有意义的事而对损失没有那么在意。
    • \n
    • 在其他一些涉及金钱的情况下,做个现实主义者也比做个绝对理性主义者强。
    • \n
    • 大多数对经济和股市走向的预测都极不靠谱,但是做预测这种行为本身是合乎情理的。
    • \n
    • 人生中很少有理论与现实一致的时候。
    • \n
    • 历史是对变化的研究,但具有讽刺性的是,人们却将历史当作预测未来的工具。
    • \n
    • 世界上总在发生过去从来没有发生过的事。
    • \n
    • 历史研究的主要内容是意料之外的事件,但投资者和经济学家们却经常将其看作对未来不容置疑的指南。
    • \n
    • 但是投资并非硬科学。投资从本质上说,是规模巨大的一群人根据有限的信息针对将给他们生活幸福度带来巨大影响的事情做出不完美决策的行为,而这会让最聪明的人也变得紧张、贪婪和疑神疑鬼。
    • \n
    • 和金钱相关的任何事背后最重要的驱动力,是人们对各种现象的合理化解释以及对商品和服务的偏好。
    • \n
    • 历史上最重要的事件往往是一些重大的、史无前例的意外事件。
    • \n
    • 人们对刺激物的反应会随着时间前进而趋于稳定。
    • \n
    • 为错误留出余地的行为的智慧就在于承认不确定性、随机性和概率——“一切未知情况”——的存在
    • \n
    • 在尽量扩大预测与实际可能发生情况的概率之差的同时,为自己留出即使预测错误也能从头再来的余地。
    • \n
    • 安全边际的目的在于让预测变得不再必要。
    • \n
    • 那就是我们不能把眼前的世界看成黑白分明的
    • \n
    • 在你可以接受可能出现的各种结果的灰色区域展开追求,才是最明智的前进方式。
    • \n
    • 但在和金钱有关的几乎所有事务中,人们都低估了容错空间的必要性。
    • \n
    • 我们不愿意留出容错空间的原因有两个。第一,我们认为一定有人知道未来会发生什么,因此承认未来的不可知性会让我们感到不舒服。第二,一旦预测成真,你就错过了充分利用该预测去采取行动的时机,会让自己蒙受损失。
    • \n
    • 我们很容易低估30%的金融资产损失对自己心理产生的影响。你的信心可能在机会最好的时候严重受挫
    • \n
    • 理论上的承受力和情感上的承受力之间的差距是人们常常忽略的一种容错空间。
    • \n
    • 获得幸福的最佳方式是把目标定得低一些。
    • \n
    • 冒险行为中的乐观偏见”或者“‘俄罗斯轮盘赌在统计学上是可行的’综合征”——当不利结果无论如何都无法接受时,人们一厢情愿地认为出现有利结果的可能性更大的现象。
    • \n
    • 如果一件事有95%的概率成功,那么剩下的5%的失败概率就意味着在你人生中的某个时间点,你一定会遭遇失败。如果这种失败意味着输得精光,那么即使出现有利局面的概率是95%,这个险也不值得你去冒,无论它看上去多么诱人。
    • \n
    • 杠杆——以负债的方式进行投资——把常规风险扩大到了足以导致毁灭的程度。
    • \n
    • 大多数时候的理性乐观主义会让人们忽视极端少数情况下倾家荡产的可能性。
    • \n
    • 随时可以做自己想做的事而且想做多久就做多久的能力,才是无限投资回报的源泉。
    • \n
    • 在金钱方面,隐患最大的单点故障便是短期开支全部依靠工资,而没有在计划中的开支和将来可能需要的开支之间用存款来建立缓冲空间。
    • \n
    • 如果你的理财规划只为已知的风险做准备,那么它会缺乏足够大的安全边际,是无法经受现实世界考验的。
    • \n
    • 事实上,每个计划中最重要的部分,就是为计划赶不上变化的情况做好预案。
    • \n
    • 我们很难预料到自己未来的想法。
    • \n
    • 每个5岁小男孩在成长过程中都有过开拖拉机的梦想。在一个小男孩的眼中,没有什么工作能比每天开着拖拉机,喊着“呜呜呜,嘟嘟嘟,大拖拉机来啦”更美好的事了。
    • \n
    • 一个30多岁的新手父母对人生目标的规划是18岁时的他或她无法想象的。
    • \n
    • 在我们生命中的每个阶段,我们都会做出一些决定。这些决定会深刻地影响我们未来的生活。当我们实现了曾经的梦想后,我们并不总会对自己当初的决定感到开心。所以我们看到,青少年花了大价钱文身,在长大后又要花大价钱洗掉;有人年轻时急着和某人结婚,上了年纪后却盼着和同一个人离婚;有人中年时努力想得到的东西,年老后却又拼命想放弃……这样的例子不胜枚举。
    • \n
    • 只有当你能给一项计划数年或数十年的时间去成长时,复利的效应才能得到最佳体现。
    • \n
    • 无论在工作生涯的哪个节点,都要定下这样均衡的目标:每年做好适中的储蓄,给自己适度的自由时间,让通勤不超过适当的时长,至少花适量的时间来陪伴家人。
    • \n
    • 在一个人人都随着时间改变的世界上,沉没成本——过去的决策导致的无法收回的支出——就像一头拦路虎。
    • \n
    • 投资的成功需要投资者付出相应的代价,但衡量这种代价的不是金钱,而是波动、恐惧、怀疑、不确定感和悔恨——如果你不是那个直接面对它们的人,这些代价都容易被你忽视
    • \n
    • 财富之神并不青睐那些只求回报却不愿付出的。
    • \n
    • 投资成功需要付出的代价是我们无法立刻看到的。它不会被直观地写在标签上。所以,当你需要支付这种账单时,你会觉得这笔钱并不是为购买好东西而支付的价钱,反倒更像做错事后必须缴纳的罚款,虽然在人们看来,付账是很正常的事,缴纳罚款却是应该避免的,所以人们觉得应该采取某些明智的预防措施,让自己避免受罚。
    • \n
    • 把市场波动看作要支付的价钱而不是该缴纳的罚款的视角看似微不足道,却是培养正确理财心态的重要部分
    • \n
    • 如果你把波动看作要买的入场券,情况就会完全不同。
    • \n
    • 几乎所有波动都是一种费用,而非一笔罚款。
    • \n
    • 市场回报永远不会是免费的,现在不是,将来也不会是。你需要支付一定的费用,就像要花钱购买一件产品一样。
    • \n
    • 人们常常在缺乏足够信息和不讲逻辑的情况下做出一些理财决定,之后又悔不当初。但站在他们当时的角度来看,这些决定是有道理的。
    • \n
    • 投资者们总是会天真地向那些和自己情况不一样的人学习理财经验。
    • \n
    • 当投资者的目标和时间规划不同时——在任何一种投资中都会出现这种情况——在一个人看来不合理的价格在另一个人看来也许是可以接受的,因为他们各自关注的因素是不同的
    • \n
    • 金融领域内的一条铁律是:金钱会追逐回报的最大化。
    • \n
    • 当交易者推高短期回报,更多的投资者就会开始入场。不久之后——通常时间都不会太久——短线投资者就成了最有权威的股市定价者了。
    • \n
    • 泡沫与估值上升的关系不大。它体现的其实是另一种情况:随着越来越多的短线投资者入场,交易周期变得越来越短。
    • \n
    • 泡沫之所以会形成,并不是因为人们在非理性地参与长期投资,而是因为人们在某种程度上堪称理性地转向短线交易,以追逐不断滚雪球式增长的积极动量。
    • \n
    • 很多金融和投资决策都建立在对他人的观察、模仿或与他人对赌的基础上,但如果你不知道为什么有些人会那样做,你就不知道他们那种行为会持续多久,什么会让他们改变主意,或者他们是否会吸取教训并做出调整。
    • \n
    • 尽可能努力明确自己玩的是什么游戏。
    • \n
    • 但在多年前,我曾这样总结我的理财哲学:我是一名被动的投资者,但对这个世界创造货真价实的经济增长的能力持乐观态度。我相信在接下来的30年里,这种增长会让我的财富不断增加。
    • \n
    • 出于一些我无法理解的原因,人们总喜欢听别人说这个世界要完蛋了。
    • \n
    • 对绝大多数人来说,保持乐观都是最好的选择,因为这个世界在大多时候对大多数人来说都是越变越好的
    • \n
    • 乐观主义是一种信念,相信就算过程中充满坎坷,随着时间过去,你心目中好结果出现的概率也比坏结果出现的概率大
    • \n
    • 如果你告诉人们一切都会变得很好,他们可能会不以为然,或者用怀疑的目光看着你。但如果你说他们正处于危险中,你就会获得他们的全部注意力。
    • \n
    • 一个在众人心怀绝望时满怀希望的人不会被看重,但一个在众人都心怀希望时满怀绝望的人却会被视为圣人
    • \n
    • 人类对失去的过度厌恶是在演化过程中形成的一种保护机制。
    • \n
    • 在进行直接比较或权衡时,失去带给我们的精神影响比得到更大。
    • \n
    • 相比机遇,对威胁反应更快的生物成功生存和繁殖的可能性才更大
    • \n
    • 悲观主义者在推测未来趋势时经常没有将市场会如何适应局势纳入考虑。
    • \n
    • 经济学中有一条铁律:极好和极糟的环境都很难长期维持,因为市场的供需会以很难预测的方式对环境进行适应。
    • \n
    • 眼前的问题有多糟糕,人们解决问题的动力就有多强——这是经济史中普遍存在的一种现象,却很容易被悲观主义者忽视,
    • \n
    • 进步发生得太慢,让人难以发觉,但挫折却出现得太快,让人难以忽视。
    • \n
    • 增长是由复利驱动的,而复利通常需要时间。毁灭却可能由独立的致命因素导致,可以在很短的时间内发生;它也可能由失去信心引发,而信心可以在一瞬间崩塌。
    • \n
    • 在投资中,你必须认识到成功的代价——在长期增长的背景下出现的波动和损失——并做好为其买单的准备。
    • \n
    • 故事是现存的对经济影响最大的潜在力量之一。
    • \n
    • 故事是经济发展中最强大的一股力量。
    • \n
    • 你越希望某事是真的,你就越容易相信一个高估其成真可能性的故事。
    • \n
    • 金融领域内的很多投资观点都带有这样的特性:一旦你听从它们,选择了某种策略或方法,你就同时在金钱和心理上进行了双重投资。
    • \n
    • 每个人对世界的看法都是不完整的,但每个人都会编织完整的故事来弥补其中的空白。
    • \n
    • 后见之明,即人们解释过去事件的能力,给了我们一种仿佛这个世界可以被理解的错觉,也给了我们一种仿佛这个世界自有其原则的错觉,哪怕在实际上一团混乱的情况下。这是我们在很多领域犯错的重要原因。
    • \n
    • 对控制力的幻想比充满不确定性的现实更容易让人接受,所以我们死死抓着某些故事不放,骗自己以为结果尽在掌握。
    • \n
    • 在做计划的时候,我们会专注于我们想做的和能做的事情,而忽略了他人的计划和能力,但他人的决策也会对结果产生影响。
    • \n
    • 无论是在解释过去还是预测未来时,我们都专注于技能起到的因果性作用,而忽略了运气的重要影响。
    • \n
    • 我们专注于我们知道的,忽视了我们不知道的,而这让我们对自己的想法过于自信。
    • \n
    • 当事态朝正确的方向发展时,要保持谦逊;当事态朝错误的方向发展时,要心怀谅解或同情。这是因为任何事都没有表面看来那样美好或糟糕。
    • \n
    • 虚荣越少,财富越多。你能存下多少钱,要看你彰显自我的需求与你的收入之间的差距,而财富恰恰存在于看不到的地方
    • \n
    • 用能让你睡踏实的方式来理财。
    • \n
    • 如果你想提高投资回报,最简单而有效的方法就是拉长时间。时间是投资中最强大的力量。
    • \n
    • 你应该始终通过衡量自己的整体投资情况,而不是根据某一笔投资的成败来评价自己的表现。
    • \n
    • 利用财富来获取对时间的掌控,因为对人生的幸福感而言,最严重而普遍的扣分项就是时间上的不自由。在任何时候和喜欢的人去做喜欢的事而且想做多久就做多久的能力,才是财富能带给你的最大红利。
    • \n
    • 多一些善意,少一些奢侈。
    • \n
    • 存钱。存就是了。存钱不需要什么特定理由。
    • \n
    • 明确成功需要付出的代价。然后做好支付的准备,因为没有什么有价值的东西是免费的。
    • \n
    • 你应该喜欢风险,因为长期看它能带给你回报
    • \n
    • 这些决策的目的往往不是追求最高的回报,而是尽量降低让伴侣或孩子失望的可能。
    • \n
    • 我的目的并不是赚大钱。我想要的不过是独立自主而已
    • \n
    • 最主要的秘诀是控制你的欲望,在能力范围内尽可能节俭地生活。自主性与你的收入水平无关,而是由你的储蓄率决定的。而当你的收入超过一定水平后,你的储蓄率是通过控制自己对生活方式的欲望决定的。
    • \n
    • 在你负担得起的范围内舒适地生活,不产生过多欲望,你会避免现代西方世界中许多人要承受的巨大社会压力。
    • \n
    • 退出无谓的激烈竞争,以获得内心平静为目标来调节你的行为,才是真正的成功。
    • \n
    • 比起让金融资产的长期收益最大化,不用每个月还贷款的选择让我们感觉更好,因为这让我感到独立和自由。
    • \n
    • 查理·芒格说:“复利的第一条原则是:除非万不得已,永远不要打断这个过程。”
    • \n
    • 对大多数投资者来说,用平均成本法【一种以定期及定额投资去积累资产(包括股票及基金)的方法,即“定投”。】去投资低成本的指数基金将是长线投资成功率最高的选择。
    • \n
    • 我始终坚持的投资理念是,在投资领域,努力和结果之间几乎没有关系。这是因为世界是由尾事件驱动的——少数几个变量是大部分回报的来源。
    • \n
    • 我的投资策略并不依靠选择正确的行业或者把握下一次经济衰退的机会,而是依靠高储蓄率、耐心和认为接下来几十年里全球经济将不断创造价值的乐观态度。
    • \n
    • 历史不过是糟心事接踵而至的过程。
    • \n
    • 在第二次世界大战结束后,美国人花了75年的时间,培养出了普通家庭对债务文化的高接受度。
    • \n
    • 虽然每个群体呐喊的具体细节不同,但他们呐喊的原因——至少部分原因——是在第二次世界大战后形成的对社会本该大体平等的预期落空了。他们没能获得别人获得的利益。
    • \n
    • 想想这种心态一旦受到社交媒体和有线新闻网强大的传播力量催化后会变成什么样。在这些平台上,人们比以往任何时候都更容易看到别人是怎样生活的——这就像火上浇油一样。
    • \n
    • 互联网让人们越频繁地接触新观点,人们对这些新观点的存在就越感到愤怒
    • \n
    • 预期的调整总是晚于实际情况的变化。
    • \n
    \n

    长期理财规划

    我为自己设计了一个为期至少10年的理财规划,规则很简单:通过蚂蚁财富,每天分别定投100元到「黄金ETF」 和「标普500ETF」,如果行情出现1%的回撤时则当日加仓50元,当盈利超过30%时则赎回30%的仓位,后边再慢慢加仓定投回去。

    \n

    我们来粗略估算了一下10年下来的投资金额:

    \n
    # 每年大约有250个交易日,每年的投资额为:
    250天*100元*2只股票=5万元

    # 多预备10%的钱来补仓
    5万+0.5万=5.5万

    # 10年下来就是
    5.5万*10=55万
    \n

    按照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
    • 无拘无束的自由
    • \n
    • 青年时期的灵巧与活力
    • \n
    • 青春的性吸引力
    • \n
    • 长生不老的空想
    • \n
    • 对子女的权威
    • \n
    • 各种各样暂时性的权力
    • \n
    • 身体永远健康
    • \n
    • 自我以及生命本身
    • \n
    \n

    总体说来,这些就是我们在人生过程中必须放弃的生活环境、个人 欲望和处世态度。放弃这些的过程就是心智完美成长的过程。

    \n

    第二部分 爱

    爱,是为了促进自己和他人心智成熟,而不断拓展自我界限,实现自我完善的一种意愿。

    \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

    在 Mac OS 中安装:

    $ brew install tmux

    \n

    开启 oh-my-zsh tmux 插件:

    plugins=(
    tmux
    )
    \n

    开启后提供以下快捷命令:

    \n
      \n
    1. ta <session-name>:接入某个已存在的会话
    2. \n
    3. ts <session-name>:新建一个指定名称的会话
    4. \n
    5. tl:查看当前所有的 Tmux 会话
    6. \n
    7. tksv:用于杀死全部会话
    8. \n
    9. tkss <session-name>:用于杀死某个会话
    10. \n
    \n

    最简操作流程

      \n
    1. 新建会话 tmux new -s my_session。
        \n
      • 安装 zsh tmux 插件后可使用 ts my_session
      • \n
      \n
    2. \n
    3. 在 Tmux 窗口运行所需的程序。
    4. \n
    5. 按下快捷键 Ctrl+b d 将会话分离。
    6. \n
    7. 下次使用时,重新连接到会话 tmux attach-session -t my_session
        \n
      • 安装 zsh tmux 插件后可使用 ta my_session
      • \n
      \n
    8. \n
    \n

    常用快捷键:

    # session
    Ctrl+b d:分离当前会话。
    Ctrl+b s:列出所有会话(切换回话)。
    Ctrl+b $:重命名当前会话。

    # pane
    Ctrl+b %:划分左右两个窗格。
    Ctrl+b ":划分上下两个窗格。
    Ctrl+b <arrow key>:光标切换到其他窗格。<arrow key>是指向要切换到的窗格的方向键,比如切换到下方窗格,就按方向键↓。
    Ctrl+b ;:光标切换到上一个窗格。
    Ctrl+b o:光标切换到下一个窗格。
    Ctrl+b {:当前窗格左移。
    Ctrl+b }:当前窗格右移。
    Ctrl+b Ctrl+o:当前窗格上移。
    Ctrl+b Alt+o:当前窗格下移。
    Ctrl+b x:关闭当前窗格。
    Ctrl+b !:将当前窗格拆分为一个独立窗口。
    Ctrl+b z:当前窗格全屏显示,再使用一次会变回原来大小。
    Ctrl+b Ctrl+<arrow key>:按箭头方向调整窗格大小。
    Ctrl+b q:显示窗格编号。

    # window
    Ctrl+b c:创建一个新窗口,状态栏会显示多个窗口的信息。
    Ctrl+b p:切换到上一个窗口(按照状态栏上的顺序)。
    Ctrl+b n:切换到下一个窗口。
    Ctrl+b <number>:切换到指定编号的窗口,其中的<number>是状态栏上的窗口编号。
    Ctrl+b w:从列表中选择窗口。
    Ctrl+b ,:窗口重命名。
    \n

    解决滚轮无效的问题

    新建 ~/.tmux.conf 配置文件后写入如下内容:

    \n
    set-option -g mouse on

    bind -n WheelUpPane if-shell -F -t = "#{mouse_any_flag}" "send-keys -M" "if -Ft= '#{pane_in_mode}' 'send-keys -M' 'select-pane -t=; copy-mode -e; send-keys -M'"
    bind -n WheelDownPane select-pane -t= \\; send-keys -M
    \n

    需杀掉全部会话(tksv),重新启动新的会话后以上配置才能生效。

    \n

    修改历史记录上限

    历史记录默认上限为 2000 行,可通过在配置文件中加入如下配置来进行修改:

    \n

    set-option -g history-limit 10000

    \n

    参考链接

    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

    作为 Java 程序员,我们几乎都会碰到 java.lang.OutOfMemoryError 异常。

    \n
    \n

    \"\"

    \n

    JVM 在抛出 java.lang.OutOfMemoryError 时,除了会打印出一行描述信息,还会打印堆栈跟踪,因此我们可以通过这些信息来找到导致异常的原因。

    \n

    其中一种异常是 java.lang.OutOfMemoryError: Unable to create native threads,我们通过这篇文章来彻底搞懂它。

    \n

    抛出这个异常的过程大概是这样的:

    \n
      \n
    1. Java 程序向 JVM 请求创建一个新的 Java 线程。
    2. \n
    3. JVM 本地代码(Native Code)代理该请求,通过调用操作系统 API 去创建一个操作系统级别的线程 Native Thread。
    4. \n
    5. 操作系统尝试创建一个新的 Native Thread,需要同时分配一些内存给该线程,每一个 Native Thread 都有一个线程栈,线程栈的大小由 JVM 参数-Xss决定。
    6. \n
    7. 由于各种原因,操作系统创建新的线程可能会失败,下面会详细谈到。
    8. \n
    9. JVM 抛出 java.lang.OutOfMemoryError: Unable to create new native thread 错误。
    10. \n
    \n

    因此关键在于第四步线程创建失败,JVM 就会抛出 OutOfMemoryError,那具体有哪些因素会导致线程创建失败呢?

    \n
      \n
    1. JVM 内存大小限制
    2. \n
    3. ulimit -u 限制
    4. \n
    5. 参数 sys.kernel.threads-max 限制
    6. \n
    7. 参数 sys.kernel.pid_max 限制
    8. \n
    \n

    第一种失败原因简单说一下:Java 创建一个线程需要消耗一定的栈空间,并通过-Xss参数指定。需要注意的是栈空间如果过小,可能会导致 StackOverflowError,尤其是在递归调用的情况下,但是栈空间过大会占用过多内存。

    \n

    同时还要注意,对于一个 32 位 Java 应用来说,用户进程空间是 4GB,内核占用 1GB,那么用户空间就剩下 3GB,因此它能创建的线程数大致可以通过这个公式算出来:

    \n
    Max memory(3GB) = [-Xmx] + [-XX:MaxMetaSpaceSize] + number_of_threads * [-Xss]
    \n

    不过对于 64 位的应用,由于虚拟进程空间近乎无限大,因此不会因为线程栈过大而耗尽虚拟地址空间。但是请你注意,64 位的 Java 进程能分配的最大内存数仍然受物理内存大小的限制。

    \n

    下边重点来介绍后边三种失败因素。

    \n

    在搞明白 pid_maxulimit -uthread_max 的区别前,需要先明白进程和线程之间的区别。

    \n
    \n

    一个最典型的区别是,(同一个进程内的)线程运行时共享内存空间,而进程在独立的内存空间中运行。

    \n
    \n

    pid_max

    pid_max 参数表示系统全局的 PID 号数值的限制,每一个进程都有 ID,ID 的值超过这个数,进程就会创建失败,pid_max 参数可以通过以下命令查看:

    \n
    cat /proc/sys/kernel/pid_max
    \n

    默认情况下,执行以上命令返回 32768,这意味着我们可以在系统中同时运行 32768进程,这些进程可以在独立的内存空间中运行。

    \n

    修改方法

    echo 65535 > /proc/sys/kernel/pid_max

    \n

    上面只是临时生效,重启机器后会失效

    \n

    永久生效方法:

    \n

    /etc/sysctl.conf 中添加 kernel.pid_max = 65535

    \n
    vi /etc/sysctl.conf
    kernel.pid_max = 65535
    \n

    或者:

    \n
    echo "kernel.pid_max = 65535" >> /etc/sysctl.conf
    \n

    threads-max

    treamd-max 用来限制操作系统全局的线程数,通过以下命令查看 treamd-max 参数:

    \n
    cat /proc/sys/kernel/threads-max
    \n

    上边的命令返回 126406,这意味着我可以在共享内存空间中拥有 126406线程

    \n

    ulimit -u

    limit -u 表示当前用户可以运行的最大进程数

    \n

    这个值怎么来的

    root 账号下 ulimit -u 得到的值默认是 cat /proc/sys/kernel/threads-max 的值 / 2,即系统线程数的一半。

    \n

    普通账号下 ulimit -u 得到的值默认是 /etc/security/limits.d/20-nproc.conf文件中指定的:

    \n

    \"\"

    \n

    修改方式

    ulimit -u 65535
    \n

    ulimit 受到全局限制

    ulimit -u 的值受全局的 kernel.pid_max 的值限制。也就是说如果 kernel.pid_max=1024,那么即使你的 ulimit -u 的值是 63203,用户能打开的最大进程数还是 1024。

    \n"},{"title":"要不到手是成长","url":"/2023/Unable-to-obtain-is-growth/","content":"

    陈奕迅的《红玫瑰》中有一句:得不到的永远在骚动。

    \n

    红楼梦中的薛蟠就是一个很好的例子:

    \n

    最早因为得不到香菱而打死冯渊,得到后几天就腻了;

    \n

    第二次因为得不到柳湘莲而在酒席上大闹,被湘莲暴揍一顿;

    \n

    第三次在娶了夏金桂不久又骚动着想要得到宝蟾。

    \n
    \n

    说句题外话,在写上边的时候发现香菱和湘莲的读音有几分像,有没有可能是作者有意为之,湘莲就是来为香菱报仇的?

    \n
    \n

    在这几次骚动中,唯一一次求而不得就是调戏柳湘莲那一次,而那一次也是薛蟠得到最多成长的一次。薛蟠因为怕丢人,就跟随老管家出远门学做生意去了,按作者的意思是过程还不错,只是回来的路上遇到了土匪,不过最后又被柳湘莲久了,二人还拜了把子。

    \n

    再说一个因得不到而骚动的例子,贾赦想要取老太太身边的丫鬟鸳鸯,但用了各种招数都没到手,最后被老太太臭骂一顿才罢休,于是他从外边买了个小媳妇,也是就稀罕了两天后边就让她独守空房了。

    \n

    这么来看得到了又怎样呢?还不如在努力后发现得不到时把心态放平和,未来还能作为一个美好的回忆。

    \n

    UpdatedAt2023年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

    WakaTime

    网站宣传语是: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
    • 该理论认为,每一种想法或认知都可以创造出一种特定的感受。

      \n
    • \n
    • 每当我们感到焦虑或害怕的时候,都是我们自己在杞人忧天。

      \n
    • \n
    • 当你开始感到焦虑的时候,你的消极想法和情绪开始相互作用,形成了一个不断加深的恶性循环。这些消极想法会导致焦虑和恐惧,而焦虑和恐惧又会让你的想法更加消极。

      \n
    • \n
    • 当你感到焦虑时,心中的很多想法都并非现实。

      \n
    • \n
    • 健康的焦虑情绪源自对实际存在的危险的感知,

      \n
    • \n
    • 神经性焦虑并不是由真实的威胁导致的。导致神经性焦虑的想法往往都是扭曲的、不合逻辑的。

      \n
    • \n
    • 如果法”(What-If Technique)治疗。

      \n
    • \n
    • 他也发现自己其实无限放大了自己在别人眼中的重要性,却忽略了身边的很多律师其实都是以自我为中心的“自恋狂”。

      \n
    • \n
    • 接受悖论(acceptance paradox)。

      \n
    • \n
    • “软弱”实际上是他最强的力量,而他引以为傲的“力量”恰恰一直是他最大的软肋。

      \n
    • \n
    • 试图隐藏的软弱、焦虑和对自己的怀疑恰恰是他和其他人连接的一根红线。

      \n
    • \n
    • 佛教教导我们痛苦不是来自现实,而是来自我们对现实的判断。

      \n
    • \n
    • 当你改变思维方式时,你就可以改变你的感受。

      \n
    • \n
    • 暴露模型

      \n
    • \n
    • 这种模型认为,当你焦虑时,你其实是在极力逃避你害怕的事情。

      \n
    • \n
    • 名为“洪水法”(Flooding)的治疗方法。在恐慌之时,我们不要刻意逃避,而是故意让自己暴露在害怕的事物面前,让自己充满焦虑。

      \n
    • \n
    • 向焦虑妥协,

      \n
    • \n
    • 情感隐藏模型认为“善良”(niceness)是焦虑的主要原因。

      \n
    • \n
    • 当你焦虑的时候,你几乎总是在刻意逃避困扰你的问题,但是你并没有意识到这一点。你之所以会把困扰自己的问题从意识中推出来,是因为你想要变得善良,不想让任何人因此感到沮丧或不安。

      \n
    • \n
    • 在这种模型之下,只要你将自己的情绪原原本本地展现出来,你的焦虑感自然会一扫而空。

      \n
    • \n
    • 了三种对抗焦虑的有效方式。认知法帮助我们识破那些让我们焦虑抑郁的消极想法。暴露法帮我们直面过去一直逃避的恐惧。情感隐藏法则帮助我们找到我们精心隐藏在内心深处的冲突或情感。

      \n
    • \n
    • 人,生而不同。

      \n
    • \n
    • 焦虑情绪源自对危险的感知。如果你一直告诉自己马上就会有不好的事情发生,你就会感到焦虑。

      \n
    • \n
    • 与杞人忧天的焦虑情绪不同,如果你感到抑郁,你会觉得悲剧已经发生了。

      \n
    • \n
    • 如果你感到抑郁,就一定会感到焦虑。如果你感到焦虑,你也许也会感到一丝抑郁。

      \n
    • \n
    • 抑郁会带来很大的痛苦,因为抑郁的情绪会剥夺你的自尊。而且,在抑郁的状态下,你也会更加容易觉得没有希望,会很容易认为自己的痛苦是永不休止的。

      \n
    • \n
    • 抑郁是这个世界上最古老、也是最残酷的骗子,因为你会骗自己去相信很多根本不真实的东西。抑郁情绪会让你认为自己很糟糕,认为自己应该做得更好,还会感觉自己从来不曾开心、满足,也不会拥有创造力,无法和他人建立亲密的关系。

      \n
    • \n
    • 很多神经病学家都认为,抑郁症和焦虑症的成因是大脑分泌的血清素不足,而躁狂症(极其欣快兴奋的状态)的成因则是大脑分泌的血清素过多

      \n
    • \n
    • 人脑和电脑的区别在于,人脑每天都会产生新的脑细胞和新的电信号回路。所以,每天早上我们醒来的时候,从字面意义上说,我们都是一个“全新的人”,因为我们的脑细胞在过去的24小时里已经全部更新一遍了。

      \n
    • \n
    • 医生们自己每天也不停地听到这种化学物质失衡理论。而这种理论的传播动力更多地是来自其背后的制药公司,

      \n
    • \n
    • 实际上,我们到现在为止,甚至都不知道大脑是怎么创造出意识的,更别提

      \n
    • \n
    • 我们对自己的期待有的时候会给我们的思维方式、感受以及行为方式带来意料之外的影响。

      \n
    • \n
    • 希望”是最有效的抗抑郁药。

      \n
    • \n
    • 你可以为抑郁

      \n
    • \n
    • 最近的研究证明,所有的处方类抗抑郁药其实除了安慰剂效应之外真的都没有其他的治疗效果。

      \n
    • \n
    • 抗抑郁药所谓的功效之中,大概有75%到80%都归功于安慰剂效应。

      \n
    • \n
    • 他们营销药物的需求和科学研究之间出现了矛盾。

      \n
    • \n
    • 认知行为疗法也是目前在美国实验最广的心理治疗方法,同时也是在临床治疗中使用最多的疗法。

      \n
    • \n
    • 于抑郁症和焦虑症的治疗来说,无论是从长期来看还是短期来看,认知行为疗法都比药物治疗更加有效。

      \n
    • \n
    • 都应该至少一周做一次“简明情绪量表”的测试(见本书第29页),以随时监测自己是否有康复的迹象。

      \n
    • \n
    • 从全球范围来看,焦虑和抑郁是两种最普通、最常见的心理健康问题,这两种情绪会让人觉得非常痛苦。

      \n
    • \n
    • 主动在生活中运用它们才行。 为此,你必须要做到以下三件事: 1. 你必须放弃焦虑和抑郁的某些隐藏的好处,这可能会给你造成损失。 2. 你必须敢于直面心中最大的恐惧,这需要你拥有极大的勇气和决心。 3. 你必须做一些笔头上的练习,这要求你必须脚踏实地地做出积极的努力。

      \n
    • \n
    • 列出所有让你抓狂的感受、想法或者习惯的优缺点。然后就这些列出来的优点和缺点做一个权衡,再来综合考虑到底要不要改变。

      \n
    • \n
    • 他认为焦虑感可以让他始终保持警惕,从而免受未来的其他潜在的伤害。这其实是所有焦虑患者都共有的一种想法。我把这称为“魔法思维”(magical thinking)。

      \n
    • \n
    • 真正能让你提高效率的焦虑情绪很少很少,更多情况下,焦虑会让你的效率越来越低。

      \n
    • \n
    • 如果你想要战胜你的焦虑,你就必须直面你心中的怪兽,战胜你心中最深处的恐惧。

      \n
    • \n
    • 并不是暴露本身让你不再恐惧,而是在暴露治疗的过程中出现了某一刻,这一刻你突然意识到,让你恐惧的想法其实并不是事实。

      \n
    • \n
    • “每日情绪日志”的基本理念就是:只要你改变了自己的想法,你就可以改变自己的感受。

      \n
    • \n
    • 填写“每日情绪日志”可以分为五个步骤: 第一步 写下令你难受或不安的事件。

      \n
    • \n
    • 第二步 圈出你的“情绪”。

      \n
    • \n
    • 第三步 记录消极想法。

      \n
    • \n
    • 表6-1 每日情绪日志

      \n
    • \n
    • 表6-2 认知扭曲对照表

      \n
    • \n
    • 在你感到焦虑和沮丧的瞬间就可能发现你的所有问题。当你开始改变自己的思考和感受的方式的时候,你就可以找到解决你所有问题的方法了。

      \n
    • \n
    • 正确识别出消极想法中的扭曲认知与其说是科学,不如说是一门艺术,所以即使没有全部勾对,也无须担心。

      \n
    • \n
    • 当你在你的想法中发现这些扭曲认知的时候,你得先想一想首先需要用到哪个方法。

      \n
    • \n
    • 双重标准法本身利用的就是人类在处理事情时的天性。当我们沮丧的时候,我们就会觉得心烦意乱,抓狂不已。但是当我们和有同样情绪问题的朋友聊天的时候,又会变得格外客观冷静,并且有同情心。

      \n
    • \n
    • 6-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
    • 识别和评估你的消极情绪很重要,因为特定种类的感受是由某些特定的消极想法引起的。

      \n
    • \n
    • 第三步 识别消极想法 当你感到不安时,记录你脑中闪现出的所有消极想法。

      \n
    • \n
    • 在“每日情绪日志”的“消极想法”一栏中列出你的消极想法,并评估你对每个想法的相信程度,

      \n
    • \n
    • 记录消极想法的过程做一些小提示。

      \n
    • \n
    • 第四步 识别出想法中的认知扭曲 在“认知扭曲”一列中记录每个消极想法中的扭曲认知。

      \n
    • \n
    • 第五步 想出积极想法 思考出一些更积极更现实的想法,让你的消极想法不攻自破。

      \n
    • \n
    • 认知行为疗法认为焦虑、抑郁和愤怒都是由当下的消极想法导致的。

      \n
    • \n
    • 自我攻击信念(

      \n
    • \n
    • 你的态度和价值观可以解释你的心理脆弱性。

      \n
    • \n
    • 自我攻击信念有两种基本类型:个人自我挫败和人际自我挫败。

      \n
    • \n
    • 个人自我挫败常常与自尊相关,

      \n
    • \n
    • 人际自我攻击信念可能更容易导致与其他人之间的冲突。

      \n
    • \n
    • 自我攻击信念其实始终存在,但消极想法只有在你感到不安时才会浮现在你的脑海中。

      \n
    • \n
    • 从“每日情绪日志”中选择一个消极想法,并在其下方画一个向下的箭头“↓”。

      \n
    • \n
    • 7-2 拉希德的向下箭头法

      \n
    • \n
    • 常见的自我攻击信念

      \n
    • \n
    • 在使用向下箭头法时,我们可以从“每日情绪日志”的消极想法开始。你选择哪一个消极想法都行,选择一个你感兴趣的就好。在它下面画一个向下的箭头“↓”并问自己:“如果这是真的,那对我来说意味着什么?为什么这会让我难过?”这时,一个新的消极想法将浮现在你的脑海中,你可以在箭头下把这个想法写下来。

      \n
    • \n
    • 最终找到自己最深层的担忧。

      \n
    • \n
    • 我们应如何改变自己的自我攻击信念呢?我认为这个过程可以分为三步。

      \n
    • \n
    • 进行成本效益分析。

      \n
    • \n
    • 修正自己的想法。

      \n
    • \n
    • 测试新的想法。

      \n
    • \n
    • 表8-1 行为完美主义:成本效益分析

      \n
    • \n
    • 另一个人的爱永远不会让我有价值,他们的拒绝也永远不会让我变得毫无价值。

      \n
    • \n
    • 如果法就可以帮助你发现引发焦虑的可怕幻想。

      \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
    • “重新归因”的目标不是使失败合理化,而是用一种更现实的角度来了解发生的每一件事。

      \n
    • \n
    • “非黑即白”的思维模式会引发表现焦虑,你会认为自己的表现必须非常完美,否则自己就是一无是处的。

      \n
    • \n
    • 情绪变化的必要和充分条件吗?必要条件是,这个积极想法必须是百分百真实的。充分条件是,你必须要能够让自己相信消极想法是个彻头彻尾的谎言。

      \n
    • \n
    • 准备和过程中的努力都在你的掌控之中,但结果往往并非如此。

      \n
    • \n
    • 即使整件事没有按照我预想的方向发展,但我的处理方式仍然是正确的,在这样复杂的情况下,我已经做得很好了。

      \n
    • \n
    • 当你使用语义法时,你只需用更友善和更温和的语言来代替你在感到不安时使用的那些带有极强感情色彩和伤害性的语言。

      \n
    • \n
    • 当你焦虑或沮丧的时候,你很可能对自己使用“你应该”“你必须”“你不得不”一类的句式。

      \n
    • \n
    • 指向自己的“应该”句式会引起抑郁、焦虑、自卑、内疚和羞耻的感觉。

      \n
    • \n
    • 而指向他人的“应该”句式则会导致怨恨与愤怒的情绪。

      \n
    • \n
    • 乱贴标签和“应该”句式往往相伴而来。而语义法就可以帮助我们扭转这两种想法。

      \n
    • \n
    • 当你将“应该”句式指向整个世界时,你会感到沮丧。

      \n
    • \n
    • “应该”句式是非常难以摆脱的,因为这种句式会让人上瘾,并让人感到一种道德优越感。

      \n
    • \n
    • 大多数情绪化的痛苦都源于我们对自己和他人的“应该”和绝对主义要求。

      \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
    • \n
    • 幽默法的目标是帮助你看到你的恐惧中的荒谬。

      \n
    • \n
    • “声音外化法”通常需要两个人在场。另一个人可以是朋友,也可以是家庭成员或是治疗师。

      \n
    • \n
    • 扮演消极想法角色的人听起来像是攻击你的另一个人,但我们必须学会抛开现象看本质。其实这“另一个人”正是你自己内心的消极想法。你其实是在和自己进行战斗。

      \n
    • \n
    • 扮演消极想法角色的人记得使用第二人称“你”,相反,扮演积极想法角色的人要以第一人称“我”来说话。

      \n
    • \n
    • 你也可以自己使用这种方法,而不需要其他人。你只需在纸上写下两个声音的对话,就像你在本章中读到的那些对话一样。

      \n
    • \n
    • 接受悖论法是一种反向运作的精神治疗方法。在使用接受悖论法的时候,你不是在一味攻击自己的消极思想,而是在这些消极想法中找到一些正确的地方。你同意这些消极的想法,但是要以一种幽默、平和和学习的方式。

      \n
    • \n
    • 如果你突然发现其实这些消极想法都是不实的,它们立刻就会失去力量。

      \n
    • \n
    • 接受悖论的目的不是隐瞒或否认你的缺点或瑕疵,也不是让你甘于平庸的生活,而是要把你的缺点暴露在光天化日之中,这样你才能不带一丝羞耻感地接受它们。如果你发现自己确实有问题,你就可以努力改善它。如果这些问题恰好是你无法改变的,你就可以简单地接受它并继续你的生活。

      \n
    • \n
    • 而当你使用激励治疗法时,则会问:“这种消极的想法或感觉对我有利吗?这种心态有什么好处?这样做对我来说有怎样的影响?”

      \n
    • \n
    • 虽然焦虑、抑郁和愤怒可能会给我们带来剧烈的痛苦,但它们往往会为我们提供可以让人上瘾的隐藏奖励。

      \n
    • \n
    • 成本效益分析有五种不同的形式: 1. 认知成本效益分析:评估一个消极思想的优点和缺点,

      \n
    • \n
      1. \n
      2. 态度成本效益分析:评估自我攻击信念的优点和缺点,
      3. \n
      \n
    • \n
      1. \n
      2. 情感成本效益分析:评估消极情绪的优缺点,
      3. \n
      \n
    • \n
      1. \n
      2. 行为(或习惯)成本效益分析:评估一个坏习惯的优点和缺点,
      3. \n
      \n
    • \n
      1. \n
      2. 关系成本效益分析:评估一种会让你的人际关系产生问题的态度的优点和缺点,
      3. \n
      \n
    • \n
    • 直接成本效益分析,

      \n
    • \n
    • 首先,我们将想要改变的想法、信念、感觉或习惯写在空白成本效益分析表的顶部(

      \n
    • \n
    • 矛盾成本效益分析法利用了这样一个事实:消极的思维模式、情绪和习惯可能会让你非常痛苦,也可能会给你带来好处。

      \n
    • \n
    • 恶魔建议法是为克服不良习惯和成瘾而开发的最强大的方法之一。这种方法基于一个简单而有力的想法——具有诱惑力的积极想法使我们屈服于习惯和成瘾。

      \n
    • \n
    • 大多数有坏习惯的人都不想改变。习惯和成瘾是会带来好处的,达到情绪的高潮状态也是一件有趣的事。

      \n
    • \n
    • 在上一章中,我给大家介绍了两种可以帮助我们克服拖延的方法:矛盾成本效益分析法和恶魔建议法。在本章中,你还会学习到另外四种技巧,它们可以帮助我们打破拖延的循环,提高工作效率和创造力,这四种方法分别是: 1. 快乐预测法 2. 任务拆解法 3. 反拖延法 4. 问题解决法

      \n
    • \n
    • 下文有一张“快乐程度预测表”。在“活动”一栏中,你可以记录下各种可能带来愉悦、促进学习或个人成长的活动。

      \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
    • 如果你想要彻底战胜你的焦虑,你就必须要直面你心中的怪兽,战胜你心中最深的恐惧。

      \n
    • \n
    • 如果你想使用图像替换法,那么当你感到焦虑的时候,你就可以试着调整脑内消极的图像和幻想,让你的思绪充满想象力。

      \n
    • \n
    • 当你使用恐惧幻想法时,你会进入一个噩梦般的世界,在这个世界中你最害怕的事情会成真。

      \n
    • \n
    • 现实中不存在敌对的批评者,这一切只是你自己内心最深处的恐惧的投射。你真的是在和自己做斗争。

      \n
    • \n
    • 认知疗法认为,是我们的想法创造出了所有的积极情绪和消极情绪。

      \n
    • \n
    • 感到害羞的人并不愚蠢。为什么他们会相信这些扭曲的信息?这是因为,消极想法在此时此刻变成了自我实现的预言,所以它们看起来才如此真实。

      \n
    • \n
    • 害羞之中的认知扭曲

      \n
    • \n
    • 你觉得自己是一个受害者,你永远不会想到整个场景都是你自己的扭曲思维的直接结果。是你在强迫对方以你害怕的方式对待你。

      \n
    • \n
    • 五种人际暴露法分别是:微笑打招呼练习、搭讪练习、拒绝练习、自我揭露法和大卫·莱特曼法。

      \n
    • \n
    • 如果你很容易害羞,你可以做同样的事情。你可以强迫自己微笑,每天向十个陌生人问好。通常你会发现人们比你预期的要友善得多。

      \n
    • \n
    • 如果某次搭讪有效的话,应该会有如下的效果:

      \n
    • \n
    • ·别人会感觉到你很特别,并且敬佩你。

      \n
    • \n
    • 搭讪的第一个秘诀是要记住这只是一场游戏。搭讪本身就是为了追求乐趣。但如果你认真对待它,你可能就会失败,因为这个世界上不存在魔法。很多人对自己的生活感到厌倦,希望能够偶尔分分心。

      \n
    • \n
    • 如果他们感觉到你是以一种非常轻松的方式在搭讪,而不是严肃、认真地进行对话,他们会更喜欢你。但如果他们觉得你很饥渴或是想追他们,他们就会拒绝你。

      \n
    • \n
    • 人们总是喜欢那些他们求而不得的东西,而从不想要唾手可得的东西。

      \n
    • \n
    • 成年人基本上都是小孩,他们只是长大了而已,并看起来有些严肃,但究其根本,我们这些成年人仍然想玩,而且想玩得开心。

      \n
    • \n
    • 大部分人的生活都是很无趣的,所以人们总是想要寻找笑料,

      \n
    • \n
    • 如果你害怕被拒绝,你就可以试着尽可能多地积累被拒绝的经验,这样你就会知道,即使被拒绝,这个世界也会照常运转。

      \n
    • \n
    • 习惯被拒绝是开展更激动人心的社交生活的第一步。

      \n
    • \n
    • 自我暴露法要求我们不再在社交场合隐藏自己的害羞或紧张感,而是公开披露它们。

      \n
    • \n
    • 你完全可以向外界展示你的羞怯,而不是试图隐藏它,然后让自己看起来很“正常”。

      \n
    • \n
    • 自我暴露法认为你因为害羞而产生的羞耻感才是你真正的敌人。如果没有这种羞耻感,害羞实际上可以成为一种资产,因为它可以让你看起来更加脆弱和有吸引力。

      \n
    • \n
    • 其实,大多数人都对谈论自己更感兴趣,给别人留下深刻印象的最好方法就是把“他人”放在聚光灯下。你可以让其他人谈他们自己,然后你带着敬意去听。这可以让你成为观众,而不是表演者,这就可以大大减轻你的压力。

      \n
    • \n
    • 使用有效沟通的五个秘诀,

      \n
    • \n
    • 解除武装法:即使对方的言论听起来非常荒谬,也要努力找到对方言论中可圈可点的部分,每个人都喜欢被肯定。

      \n
    • \n
    • 思想同理和感受同理:试着通过对方的眼睛看世界。

      \n
    • \n
    • 你可以换一种方式总结对方说过的话,然后再加以反馈,这样一来,对方就知道你在听,并且也了解到了你的想法。

      \n
    • \n
    • 质询法:提出简单的问题来吸引对方。

      \n
    • \n
    • EAR。在下面的图上,我们可以看到,EAR是三个词的首字母缩写,分别代表Empathy(同理心)、Assertiveness(肯定)和Respect(尊重)。

      \n
    • \n
    • 有效沟通的五个秘诀(EAR)

      \n
    • \n
    • 有效沟通的五个秘诀可以通过两种不同的方式帮助你在做公共演讲的时候摆脱焦虑。首先,因为你会发现你有一种神奇的方式处理人们在演讲期间对你说的任何话,所以焦虑自然而然就消失了。其次,如果某人确实提出了一个令人讨厌或困难的问题,并且你巧妙地使用了解除武装法和夸赞法,那么他们就会积极回应,因为他们会发现问问题是很安全的。这将让所有的观众情绪高涨。

      \n
    • \n
    • 大约75%的焦虑症患者都在隐藏自己的情绪和感觉。

      \n
    • \n
    • 只要我们把这些情绪问题拿到台面上来,焦虑很快就消失了,

      \n
    • \n
    • 大多数患有焦虑症的人都过于善良。我觉得,善良几乎是所有焦虑的原因。

      \n
    • \n
    • 你总是过于善良,而且你并不总是敢于展示你的真实感受。

      \n
    • \n
    • 这些感到焦虑的人甚至不知道自己的感受。

      \n
    • \n
    • 如果你感到焦虑,情感隐藏法绝对值得一试。这个方法有两个步骤: 1. 发现问题。

      \n
    • \n
    • 找到解决方案。

      \n
    • \n
    • 虑其实是你的身体在告诉你:“嘿,你对这件事感到不安,去查看一下吧。”

      \n
    • \n
    • 情感隐藏法涉及两个步骤: 1. 找出困扰你的问题或感觉。 2. 表达你的感受,并采取措施解决问题。

      \n
    • \n
    • 如果你很容易产生焦虑的情绪,你常常会无意识地忽略自己的感受,而被你忽视掉的感受会间接地出现,伪装成焦虑的样子。

      \n
    • \n
    • 当人们感到沮丧的时候,有些人会开始担心,有些人会产生恐惧症,有些人,比如特丽,会发生惊恐发作,还有一些人则可能会出现强迫症状。

      \n
    • \n
    • ·焦虑通常是对你的冲突或问题的象征性表现。这是你的大脑间接传达你的压抑感受的方式。

      \n
    • \n
    • 焦虑就像一个清醒的梦。焦虑的人就像艺术家和诗人一样,间接地用图像和隐喻来表达感情。

      \n
    • \n
    • 大多数人认为焦虑是一件坏事,不是好事。但我持相反的观点。没有人能一直感到幸福。我们都会不时地感到心碎和失望。

      \n
    • \n
    • 焦虑一定是人为的某些因素引起的,而焦虑背后真正的恐惧则是对真实情感和感受的恐惧。

      \n
    • \n
    • 战胜恐惧的40种方法

      \n
    • \n
    • 每日情绪日志”的五个步骤: 第一步:描述一件令人难受沮丧的事件,记录下任何一个让你觉得焦虑或沮丧的瞬间。 第二步:在表格中圈出符合你消极感受的词语,并按照从0(完全没有这样想)到100(完完全全是我的想法)的等级进行评分。 第三步:记录下你的消极想法,根据你对每个想法的相信程度在0~100之间进行打分。 第四步:找出每个消极想法中的扭曲认知。 第五步:用更积极、更现实的想法替换原有的消极想法。根据你对这些积极想法的相信程度对它们在0~100之间进行打分。现在,再次评估你对每个消极想法的相信程度。

      \n
    • \n
    • 如果康复圈中的消极想法让你感到焦虑,请确保你选择的方法中包含了三个种类的方法:认知治疗法、暴露治疗法和情感隐藏治疗法。这其实是一个很好的方法组合,你选择的方法中可以包括十二到十五种认知疗法、两种或三种暴露治疗法和情感隐藏治疗法。

      \n
    • \n
    • 平均而言,你必须尝试至少十到十五种方法,才能找到一个行之有效的识破消极想法的方法。

      \n
    • \n
    • 康复圈是为“每日情绪日志”提供动力的引擎。

      \n
    • \n
    • 在你想出一个能够满足情绪变化的必要和充分条件的积极想法之前,你的情况是不会有所改善的: ·必要条件:积极想法必须是百分百真实的。 ·充分条件:积极想法需要让消极想法不攻自破。

      \n
    • \n
    • 表22-15 基于消极想法中的认知扭曲选择方法

      \n
    • \n
    • 22-16 基于你正在克服的问题选择方法

      \n
    • \n
    • 战胜恐惧的40种方法

      \n
    • \n
    • 当你使用检查证据法时,你会问自己这样的问题:“有没有什么可靠的证据可以支持我的消极想法?我是怎么在第一时间得出这个结论的?”

      \n
    • \n
    • 没有公式或噱头可以盲目地应用于不同的问题或焦虑类型,相反,我给了你一些灵活、强大、个性化的方法,你可以用它们来克服困扰你的情绪问题。

      \n
    • \n
    • 如果你曾经因焦虑或抑郁而挣扎,你迟早会再次感到焦虑或沮丧。事实上,所有人的焦虑都会复发!

      \n
    • \n
    • 佛陀说,痛苦是人类的固有特征,这是不可避免的。没有人能够一直感到幸福,如果可能的话,一直开心其实也不会是件好事。如果我们一直很开心,我们的情绪就没有任何变化,也不存在任何挑战,生活很快就会变得无聊,因为我们总是感觉完全一样。古拉丁谚语说得好:饥饿是才最甜的酱汁。

      \n
    • \n
    • 复发时的认知扭曲

      \n
    • \n
    • 复发每日情绪日志

      \n
    • \n
    • 复发每日心情日志(续表)

      \n
    • \n
    • 要经常让自己面对心中的恐惧,这样你的信心就会增长。

      \n
    • \n
    • 焦虑或恐慌的感觉并不是一件坏事,而是一个重要的信号,表明有你需要注意的事情发生了。

      \n
    • \n
    \n"},{"title":"为什么好久没更新了","url":"/2024/Why-not-update-for-a-long-time/","content":"

    可以看到我在去年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

    并发 vs 并行

    介绍并发模型前,我们先来理解一下并发和并行的区别,下边这张图说明了两者之间的区别:

    \n

    \"\"

    \n
      \n
    • 并发:一个处理器同时处理多个任务。
    • \n
    • 并行:多个处理器或者是多核的处理器同时处理多个不同的任务.
    • \n
    \n

    并发性 vs 并行性

      \n
    • 并发性(concurrency),又称共行性,是指能处理多个同时性活动的能力,并发事件之间不一定要同一时刻发生。
    • \n
    • 并行(parallelism) 是指同时发生的两个并发事件。
    • \n
    \n
    \n

    并行具有并发的含义,而并发则不一定并行。

    \n
    \n

    并发模型

    并发模型分类

    并发编程模型按照实现方式可以分为两类:

    \n
      \n
    • 共享状态并发(Shared state concurrency)
    • \n
    • 消息传递并发(Message passing concurrency)
    • \n
    \n

    共享状态并发

    共享状态并发涉及到可变状态(Mutable state,即内存可修改)的概念。大多数语言,如 C、Java、C++ 等等,都有这个概念,即有一种叫内存的东西,我们可以修改它。

    \n

    在只有一个进程(或线程)工作的情况下,这个模式可以很好地运行。但如果有多个进程共享和修改相同的内存,就会产生问题和危险。

    \n

    为了防止同时修改共享内存,我们需要一个锁机制。你可以称它为互斥量或同步方法,但它本质上仍然是锁。

    \n

    如果程序在关键区域发生崩溃(例如,当它在持有锁的时候)就会有灾难的发生:其他所有的进程都不知道该做什么。

    \n

    多线程模型就是通过共享状态实现的并发,代表语言有:Java, C#, C++。

    \n

    同时,根据上边的内容可以推导出:

    \n

    不可变数据结构(Immutable) = 没有锁

    \n

    不可变数据结构(Immutable)= 易于并发

    \n

    消息传递并发

    在消息传递并发中,不存在共享状态。所有计算都是在进程中完成的,交换数据的唯一方法是通过异步消息传递。

    \n

    如何理解这句话?

    \n

    想象一群人,他们没有共享的状态。

    \n

    我有自己的记忆,同时你也有你的记忆。它们是不共享的。我们通过传递信息(如,说话)进行交流,我们根据接收到的这些消息更新私有状态(也就是自己的记忆)。

    \n

    Actors 模型CSP 模型 是通过消息传递实现的并发。

    \n
      \n
    • Actors 代表语言:Erlang, Scala, Rust
    • \n
    • CSP 代表语言:Golang
    • \n
    \n

    Actors vs CSP

    对于多线程模型大部分开发人员都是比较熟悉的,也知道它存在很多缺点,如:死锁、不易伸缩。

    \n

    接下来的内容我们重点对两个基于消息传递并发的模型来进行介绍和对比,Actors 和 CSP 是实现程序并行工作的两种最有效的模型。

    \n

    Actors 模型,顾名思义,侧重的是 Actor。每个 Actor 与其他 Actor 进行直接通信,不经过中介,且消息是异步发送和处理的。

    \n

    \"\"

    \n

    CSP 是 Communicating Sequential Processes(通信顺序进程)的简称。在 CSP 中,多了一个角色 Channel,Worker 之间不直接通信,而是通过 Channle 进行通信。

    \n

    Channel 是过程的中间媒介,Worker1 想要跟 Worker2 发信息时,直接把信息放到 Channel 里(在程序中其实就是一块内存),然后 Worker2 在方便的时候到 Channel 里获取。

    \n

    Channel 类似 Unix 中的 Pipe,后文将 Channel 称为通道

    \n

    \"\"

    \n

    CSP 是完全同步的。通道的写入方在接受方读取前会被一直阻塞。这种基于阻塞机制的优点是一个通道只需要保存一条消息,在很多方面也更容易推理。

    \n

    Actors 的发送方是异步的。无论消息接收方是否将消息读取出来,发送方都不会阻塞,而是将消息放入通常称为邮箱(mailbox)的队列中。这提供了很多的便利,但困难之处在于邮箱可能需要容纳大量的信息。

    \n

    CSP 进程使用通道(channel)进行通信。程序可以将通道作为第一类对象(first class objects)创建并传递。Actors 有地址系统和收件箱,每个进程只有一个地址。在耦合度上两者是有区别的,CSP 更加松耦合。

    \n

    在 CSP 中,发送和接收操作可能会阻塞。在 Actors 模型中,只有接收操作可能被阻塞。

    \n

    在 CSP 中,消息是按发送顺序传递的,而在 Actors 模型中不是这样。事实上,系统可能根本无法传递某些消息(意味着消息可能会丢失)。

    \n

    到目前为止,CSP 模型在一台机器上工作得最好,而 Actors 模型很容易实现跨多台机器的扩展。

    \n

    结论

    Actors 更适合于分布式系统。

    \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

    人到底要走向哪里去,什么是生命的本体。我们追逐的东西是不是生命里面真正想要的、觉得最重要的?我们误认了世俗里面虚拟出来的假象,把它们当成了故乡,努力地飞奔而去。其实那只是「他乡」而已,并不是生命本质的东西。

    \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

    乐观主义者往往不会杞人忧天,安下心来把事情做好,自然就能得到想要的结果。

    \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
    \n"},{"title":"自动备份数据库并上传到 S3","url":"/2020/auto-backup-database/","content":"
    \"\"
    \n\n

    我开发的老板管库虽然没太多收入,但是还是有不少的用户量,为了节约成本,我并没有使用厂商提供的云数据库,而是在服务器本地搭了一个 MariaDB 实例。考虑到用户数据安全还是第一位的,所以我每天会通过定时任务的方式进行全量备份,并上传到我的七牛云,脚本如下:

    \n
    #!/bin/bash

    dir=$(dirname $(readlink -f \"$0\"))

    filename=bossku_$(date +%Y%m%d%H%M).sql
    echo ${filename}
    cd ${dir}
    mysqldump -h{ipaddress} -P{port} -uroot -p{password} bossku > ${filename}

    qshell rput bosskudb ${filename} ${filename}
    \n

    上边的命令会生成一个以执行时间为后缀的 .sql 文件并上传到我的七牛云中名为 bosskudb 的bucket中,同时我还会配置这个 bucket 的生命周期,只保留近7天的数据。这实际上是套比较通用的流程,昨天恰好看到一个 repo:https://github.com/appleboy/docker-backup-database 就是用来提供这套流程的封装的,看到作者又是个自己比较崇拜的开发者,于是准备上手用一用。

    \n

    (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\n

    准备 docker-compose.yml 脚本

    version: '3'

    services:
    backup_mysql:
    image: appleboy/docker-backup-database:mysql-5.7
    logging:
    options:
    max-size: \"100k\"
    max-file: \"3\"
    environment:
    STORAGE_DRIVER: s3
    STORAGE_ENDPOINT: s3.amazonaws.com
    STORAGE_BUCKET: bossku-db-backup
    STORAGE_REGION: ap-east-1
    STORAGE_PATH: backup
    STORAGE_SSL: \"false\"
    STORAGE_INSECURE_SKIP_VERIFY: \"false\"
    ACCESS_KEY_ID: AKI*******UFT
    SECRET_ACCESS_KEY: 4u********************NU

    DATABASE_DRIVER: mysql
    DATABASE_HOST: {ip}:{port}
    DATABASE_USERNAME: root
    DATABASE_PASSWORD: {password}
    DATABASE_NAME: bossku
    DATABASE_OPTS:

    TIME_SCHEDULE: \"0 0 * * *\"
    TIME_LOCATION: Asia/Shanghai
    \n

    ACCESS_KEY_IDSECRET_ACCESS_KEY 获取方式可以查看:Where’s My Secret Access Key?

    \n

    因为我所启动的数据库实例为 MariaDB:10.2,根据官方介绍,其所对应的 MySQL 版本为 5.7,所以上边命令中的 image 我指定的是 appleboy/docker-backup-database:mysql-5.7

    \n
    \"\"
    \n\n

    测试

    测试的时候,为了方便查看效果,可以将 TIME_SCHEDULE 删掉,这样会立即执行,且执行一次后退出。

    \n
    docker-compose up -d

    # 然后观察日志
    docker-compose logs -f
    Attaching to bossku-db-backup_backup_mysql_1_6cc39ab97f2c
    backup_mysql_1_6cc39ab97f2c | $ mysqldump --version
    backup_mysql_1_6cc39ab97f2c | mysqldump Ver 10.13 Distrib 5.7.32, for Linux (x86_64)
    backup_mysql_1_6cc39ab97f2c | $ bash -c mysqldump -h {ip} -P {port} -u root bossku | gzip > dump.sql.gz

    bossku-db-backup_backup_mysql_1_6cc39ab97f2c exited with code 0
    \n

    可以看到成功了,再到 S3 中验证一下文件有没有上传上来:

    \n
    \"\"
    \n\n

    文件也传成功了!

    \n

    如果在最后的上传步骤遇到无权限的错误,可以通过尝试调整 bucket 权限来解决。

    \n

    写在最后

    通过日志和上传上来的文件名可以看出,其实他也是通过 mysqldump 先生成备份文件,然后通过 S3 的 SDK 进行上传,同时也是使用了日期最为文件名的命名方式。我也大致看了下代码,所使用的 SDK 为 minio 提供的,这样又可以同时支持上传到 minio 了。

    \n

    创新就是将一些已有的东西进行重新组合,比如这里只是将 dockermysqldumpS3 进行了组合,就创造出了这么一个好用且通用的工具,非常值得学习。

    \n"},{"title":"将重复工作自动化","url":"/2023/automated-audit/","content":"

    作为程序员,我们最擅长的事情就是用程序解决问题,恨不得天天拿着锤子找钉子。

    \n

    我们公司的服务上线流程是先由服务负责人审批,然后再由团队的 Leader 审批。如果自己就是服务负责人,则只需经过团队的 Leader 审批即可。因此,我们大部分服务负责人设置的是最经常改动上线的那个人。

    \n

    所以无论如何设置,最终都需要我来审批。每天平均要审批30多个上线单,不论是在医院看病、开车、吃饭、开会,随时都可能有审批。而且基本上都会伴随着一个「钉」,在公司里还好一些,一请假可就要了命了。上一次请假,下午四点之前我没有什么特别重要的事情,四点后准开车出去办点事,但是好巧不巧,四点之后开始不断有上线审批,换着人轮番上线。我一手握着方向盘,另一只手拿着手机审批,幸好我的车是自动挡,否则手动挡的话我真就忙不过来了。那个时候我真的有点火大,决定一定要写一个工具来自动帮我审批。

    \n

    第二天上班后,我就让旁边的同事提了个上线单。在审批这个上线单的整个流程中,我进行了抓包,以查看每个步骤的请求内容。最后,我梳理了整个流程发现:将5个接口请求配合起来,就可以实现自动化审批,具体细节这里不展开。

    \n
    \n

    这里不得不再吹一次 Python,从写第一行代码到完成,不到4个小时就实现了完整的功能。

    \n
    \n

    要实现这个自动审批功能,还需要解决两个问题:

    \n
      \n
    1. 接口鉴权
    2. \n
    3. 消息通知
    4. \n
    \n

    接口鉴权

    我们公司的开发平台支持两种认证方式。在内网环境下,可以通过域账户登录;在非内网环境下,可以使用钉钉登录。无论使用哪种方式,最终都是设置后端所需的 Cookies。两种方式本质上并没有太大区别。

    \n

    我需要一个稳定的机器来循环执行审批脚本,所以我将这个脚本放在了我的服务器上。为了快速验证第一版程序中的 Cookies 是否有效,我将 Cookies 写死在了代码内,并测试了它们的过期时间。经过验证,Cookies 的过期时间为12小时。因此,每天早上更新一次 Cookies 即可。

    \n

    更新 Cookies 需要手动操作。因为登录界面和登录接口有许多校验和加密逻辑,无法通过简单的模拟来完成。每天早上手动登录一次,然后提取 Cookies 即可。这比以前已经方便了许多。

    \n

    更新 Cookies

    接下来需要解决每天如何方便更新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
    • 我写了个脚本,当发现我的博客收到评论后,会自动给我手机发一条 push
    • \n
    • 我的博客是纯静态页,没有管理后台,之前只能用我的一台配好环境的电脑来发布,现在改成了任何电脑都可以发布(这个后边找时间专门写一篇文章来介绍)
    • \n
    \n

    最后

    为什么我这么看重审批,进而想要将其自动化。

    \n

    首先,我觉得它是一项重复性的工作,确实没有必要每一次都进行人工操作。每次操作对我来说都是一次打扰。

    \n

    其次,更重要的是,我不想因为上线审批不及时而成为团队效率的瓶颈。我一直提倡高效、不加班的工作方式,但是如果一个审批需要十多分钟,就有些大家的浪费时间了。

    \n"},{"title":"避免在 Go 中使用 append","url":"/2021/avoid-using-append-in-go/","content":"

    append 是我们向切片添加元素时的首选函数,但这可能不是最好用法。原因如下:

    \n

    首先,我们创建两个函数,功能是将字符 “x” 填充进一个字符串切片。

    \n

    WithAppend 调用 append 将 “x” 添加到一个字符串切片中

    \n
    func WithAppend() []string {
    var l []string
    for i := 0; i < 100; i++ {
    l = append(l, "x")
    }

    return l
    }
    \n

    WithAssignAlloc 通过用 make 来创建一个指定大小的字符串切片,之后赋值 “x” 给指定索引位而不是使用 append

    \n
    func WithAssignAlloc() []string {
    l := make([]string, 100)
    for i := 0; i < 100; i++ {
    l[i] = "x"
    }

    return l
    }
    \n

    这两个函数返回相同的结果,但其实现方式完全不同。

    \n

    现在,让我们对这些函数进行基准测试。

    \n
    func BenchmarkWithAppend(b *testing.B) {
    b.ReportAllocs()
    for i := 0; i < b.N; i++ {
    WithAppend()
    }
    }
    func BenchmarkWithAssignAlloc(b *testing.B) {
    b.ReportAllocs()
    for i := 0; i < b.N; i++ {
    WithAssignAlloc()
    }
    }
    \n

    结果如下:

    \n
    BenchmarkWithAppend
    BenchmarkWithAppend-8 863949 1322 ns/op 4080 B/op 8 allocs/op
    BenchmarkWithAssignAlloc
    BenchmarkWithAssignAlloc-8 2343424 523 ns/op 1792 B/op 1 allocs/op
    \n

    WithAppend 的性能最差,而 WithAssignAlloc 的性能最好,这个结论应该可以说服你应该避免 append 了吧?

    \n

    但先别急着走。

    \n

    我们再写一个使用 append 的函数,并通过指定大小和容量来创建一个字符串切片。

    \n
    func WithAppendAlloc() []string {
    l := make([]string, 0, 100)
    for i := 0; i < 100; i++ {
    l = append(l, "x")
    }
    return l
    }
    \n

    再次运行基准测试。

    \n
    BenchmarkWithAppend
    BenchmarkWithAppend-8 863949 1322 ns/op 4080 B/op 8 allocs/op
    BenchmarkWithAppendAlloc
    BenchmarkWithAppendAlloc-8 2543119 514 ns/op 1792 B/op 1 allocs/op
    BenchmarkWithAssignAlloc
    BenchmarkWithAssignAlloc-8 2343424 523 ns/op 1792 B/op 1 allocs/op
    \n

    现在我们在 WithAppendAllocWithAssignAlloc 上得到了同样好的性能。

    \n

    为什么 WithAppend 性能很差?在使用 WithAppend 往切片中添加元素时,当切片的容量不足时,需要创建一个新的更大的切片来对切片进行扩容,这导致多次分配。

    \n
    \n

    在你优化代码之前,应该通过基准测试来找到代码中的瓶颈。上面的例子过于简化,你可能并不总是知道应该提前分配切片的大小。

    \n

    另外,过早地进行性能调整可能会矫枉过正。

    \n"},{"title":"使用 awk 统计 p99","url":"/2022/awk-statistics-p99/","content":"

    最近在重构公司内的一个重要服务,目前已经把主要流程写完了,由于新写的服务对底层的存储组件进行了变更,所以要对性能进行一个对比。

    \n

    老服务的监控不是太完善,接口平均时延、p99 之类的都没有上报到 Prometheus 里,只在日志文件中进行了每次请求响应时间的统计,所以我写了一个 Python 脚本遍历所有日志,从中抽取出我需要的数值,然后将这些时间进行加和再除以数量就可以得到平均时间了。但是 p90、p95、p99 这些还需要我再去编写额外的代码逻辑进行统计。

    \n

    因为太懒了,不想去写那些统计逻辑,于是从网上搜了下有没有现成的脚本,找到了一个使用 awk 统计时延的脚本,如下:

    \n
    #! /usr/bin/awk -f  
    {variance=0;sumCount+=$1;sumCost+=($2*$1);count[NR]=$1;cost[NR]=$2}
    END {
    staticTotal[0]=50;
    staticTotal[1]=66;
    staticTotal[2]=80;
    staticTotal[3]=85;
    staticTotal[4]=90;
    staticTotal[5]=95;
    staticTotal[6]=98;
    staticTotal[7]=99;
    staticTotal[8]=99.9;
    staticFlag[0]=1;
    staticFlag[1]=1;
    staticFlag[2]=1;
    staticFlag[3]=1;
    staticFlag[4]=1;
    staticFlag[5]=1;
    staticFlag[6]=1;
    staticFlag[7]=1;
    staticFlag[8]=1;
    printf \"%3s %10s %15s %15s\\n\", \"static\", \"costt\", \"count\", \"diffPre\";
    averageCost = sumCost/sumCount;
    for(i=1; i <=length(count); i++) {
    diff = (cost[i] - averageCost);
    variance += (diff*diff*count[i]/(sumCount-1));
    #printf(\"diff %s, variance %s, count[%s]: %s, cost[%s]: %s \\n\", diff, variance, i, count[i], i, cost[i]);
    countTotal += count[i];
    for (j=0; j <length(staticTotal); j++) {
    if (countTotal >= sumCount*staticTotal[j]/100) if (staticFlag[j]==1) {
    staticFlag[j]=sprintf(\"P%-3s %10s %15s %15s\", staticTotal[j],cost[i],countTotal, countTotal - countTotalPre); countTotalPre = countTotal;
    }
    }
    };

    for( i=0;i<length(staticFlag);i++) print staticFlag[i];
    printf \"count total: %s\\n\", sumCount, countTotal;
    printf \"average cost: %s \\n\", averageCost;
    printf \"variance cost: %s \\n\", variance;
    }
    \n

    用法也很简单,准备好我们每次请求响应时间的数据,一行一条,如:

    \n
    1.803322
    12.561867
    3.819391
    0.468846
    23.792512
    0.362949
    0.347554
    2.739202
    12.407241
    39.385484
    ...
    \n

    假如我们的数据文件叫做 time.log,将上边的脚本保存为 cal.awk,用一下命令就可以得出时延的统计信息了:

    \n
    $ cat time.log | sort -n | uniq -c | awk -f cal.awk
    static costt count diffPre
    P50 3.154644 50000 50000
    P66 5.481086 66000 16000
    P80 9.649493 80000 14000
    P85 12.548806 85000 5000
    P90 17.208233 90000 5000
    P95 26.653718 95000 5000
    P98 42.952164 98000 3000
    P99 59.790145 99000 1000
    P99.9 102.811803 99900 900
    count total: 100000
    average cost: 7.03982
    variance cost: 128.59
    \n

    这条命令组合了 sort 对数据进行排序、uniq 对数据进行去重+次数统计,最后调用我们的 awk 脚本实现统计。

    \n

    可以看到统计的种类很全,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 的方法,最后找到一种有效的方式,记录在这里,为了方便自己查阅,也希望能帮助到其他人。

    \n

    运行下边这条命令:

    bash -c "$(curl -fsSL https://raw.githubusercontent.com/xzhih/one-key-hidpi/master/hidpi.sh)"
    \n

    执行路径为:

      \n
    1. 选择自己的外接显示器
    2. \n
    3. 开启HIDPI(同时注入EDID)
    4. \n
    5. 保持原样
    6. \n
    7. 手动输入分辨率
    8. \n
    \n

    最后手动输入我需要的分辨率:1920x1080 2560x1440,重启后就可以通过 RDM(https://github.com/avibrazil/RDM) 来开启 HiDPI 了。

    \n
    \"\"
    \n\n

    如果无效的话

    尝试删除 /Library/Displays/Contents/Resources/Overrides/DisplayVendorID-xxx 目录后再试一次(xxx 为你的 VendorID)。

    \n
    \n

    我之前一直失败就是用这个方式才成功的,估计是用其他方法写入了脏数据,hidpi.sh 脚本的数据一直写入失败,需要手动删除一下脏数据。

    \n
    \n

    参考:

    https://github.com/xzhih/one-key-hidpi/issues/136
    https://blog.chajian110.com/macOS/32.html
    https://blog.csdn.net/ymyz1229/article/details/109676446

    \n"},{"title":"让博客重定向到 https 的方法","url":"/2017/blog-redirect-https/","content":"

    前段时间将博客在七牛上部署了一份,并且为新的域名 jpanj.com 申请了 SSL 证书,但是发现一个问题,使用 http 请求还是可以访问的,想通过 https 的方式访问,需要手动将地址修改为 https,我想有没有什么办法能在用 http 访问时重定向到 https。

    \n

    所以我开了个工单请教七牛的工作人员,得到的结果是他们也无法做强制跳转。

    \n

    \"\"

    \n

    之前让 http 请求重定向到 https 的方法是通过 Nginx 的 rewrite 完成的,但是我现在的博客是一个纯静态站点,而且并没有托管在自己的服务器上,所以无法这样操作。

    \n

    今天得到了一个解决方法,是通过修改主题源码来实现的,就我现在用的这个主题来说,layout 目录下所有模板都会继承 _layout.swig,所以我只要在 <head> 标签中加入以下代码即可:

    \n
    <script type="text/javascript">
    var host = "jpanj.com";
    if ((host == window.location.host) && (window.location.protocol != "https:"))
    window.location.protocol = "https";
    </script>
    \n

    我只需要判断 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

    后边想想有没有什么方式可以让那些加密的文章在未来指定的某一天改为明文显示,或者将密码展示出来。

    \n"},{"title":"从零开始搭建一个全文检索引擎","url":"/2021/build-a-full-text-search-engine/","content":"

    全文检索是我们每天都使用的工具之一,在谷歌上搜索「golang 入门」或在淘宝上搜「智能音箱」,就会用到全文检索技术。

    \n

    全文检索(FTS full text search)是一种在文档集合中搜索文本的技术。文档可以指网页、报纸上的文章、电子邮件或任何结构化文本。

    \n

    今天我们将建立我们自己的 FTS 引擎。在这篇文章结束时,我们将实现一个能够在一毫秒内搜索数以百万计的文档的程序。我们从简单的搜索查询开始,比如:找出所有包含「cat」这个词的文档,然后我们将扩展这个引擎以支持更复杂的布尔查询。

    \n
    \n

    注:目前最著名的 FTS 引擎是 Lucene(以及建立在它之上的 Elasticsearch 和 Solr)。

    \n
    \n

    为什么是FTS

    在我们开始写代码之前,你可能会问:「我们就不能用 grep 或者用一个循环来检查每个文档是否包含我所要找的词吗?」。

    \n

    是的,可以这样做。但这并不是最好的解决方案。

    \n

    语料库

    我们将搜索英文维基百科的一部分摘要。最新的离线数据可以在 dumps.wikimedia.org 上找到。压缩包解压后的 XML 文件为 956MB(截止2021年05月17日),包含60多万个文档。

    \n

    文档的例子:

    \n
    <title>Wikipedia: Kit-Cat Klock</title>
    <url>https://en.wikipedia.org/wiki/Kit-Cat_Klock</url>
    <abstract>The Kit-Cat Klock is an art deco novelty wall clock shaped like a grinning cat with cartoon eyes that swivel in time with its pendulum tail.</abstract>
    \n

    加载文件

    首先,我们需要加载所有文件,这一步使用内置的 encoding/xml 包就就够了。

    \n
    import (
    \"encoding/xml\"
    \"os\"
    )

    type document struct {
    Title string `xml:\"title\"`
    URL string `xml:\"url\"`
    Text string `xml:\"abstract\"`
    ID int
    }

    func loadDocuments(path string) ([]document, error) {
    f, err := os.Open(path)
    if err != nil {
    return nil, err
    }
    defer f.Close()

    dec := xml.NewDecoder(f)
    dump := struct {
    Documents []document `xml:\"doc\"`
    }{}
    if err := dec.Decode(&dump); err != nil {
    return nil, err
    }

    docs := dump.Documents
    for i := range docs {
    docs[i].ID = i
    }
    return docs, nil
    }
    \n

    每个被加载的文档都被分配了一个唯一 ID。简单起见,第一个文档的 ID=0,第二个 ID=1,以此类推。

    \n

    第一次尝试

    搜索内容

    现在我们已经把所有的文档都加载到了内存中了,我们可以试着找到所有关于 cat 的文档。

    \n

    首先,让我们循环遍历所有的文档,检查它们是否包含 cat 这个子串。

    \n
    func search(docs []document, term string) []document {
    var r []document
    for _, doc := range docs {
    if strings.Contains(doc.Text, term) {
    r = append(r, doc)
    }
    }
    return r
    }
    \n

    在我的 Macbook Pro 上,搜索阶段耗时 68ms,还不错。

    \n

    我们抽查结果中的几个文件,会发现这个函数匹配了 caterpillar(毛毛虫)和 category,但没有匹配大写字母 C 开头的 Cat,这不太符合我们的预期。

    \n

    在继续前进之前,我们需要解决两件事:

    \n
      \n
    • 使搜索不区分大小写(Cat 要匹配)。

      \n
    • \n
    • 在单词边界而不是在子字符串上匹配(caterpillarcategory 不匹配)。

      \n
    • \n
    \n

    用正则表达式进行搜索

    一种可以快速想到并实现这两个要求的方案是使用正则表达式。

    \n

    在这里为 (?i)\\bcat\\b

    \n
      \n
    • (?i)使正则表达式不区分大小写

      \n
    • \n
    • \\b 匹配一个词的边界。

      \n
    • \n
    \n
    func search(docs []document, term string) []document {
    re := regexp.MustCompile(`(?i)\\b` + term + `\\b`) // 有安全风险,不要在生产环境中这样用
    var r []document
    for _, doc := range docs {
    if re.MatchString(doc.Text) {
    r = append(r, doc)
    }
    }
    return r
    }
    \n

    这次搜索花了近 2 秒。正如我们所看到的,即使只有 60 万个文档,搜索也开始变得缓慢。虽然这种方法很容易实现,但它不能很好地扩展。随着数据集的增大,我们需要扫描的文档越来越多。这种算法的时间复杂度是线性的——需要扫描的文档数量等于文档总数。如果我们有 600 多万个文档,搜索将需要 20 秒。

    \n

    倒排索引

    为了使搜索查询更快,我们要对文本进行预处理,并提前建立一个索引。

    \n

    FTS 的核心是一个叫做倒排索引的数据结构,倒排索引将文档中的每个单词与包含该单词的文档关联起来。

    \n

    例子:

    \n
    documents = {
    1: "a donut on a glass plate",
    2: "only the donut",
    3: "listen to the drum machine",
    }

    index = {
    "a": [1],
    "donut": [1, 2],
    "on": [1],
    "glass": [1],
    "plate": [1],
    "only": [2],
    "the": [2, 3],
    "listen": [3],
    "to": [3],
    "drum": [3],
    "machine": [3],
    }
    \n

    下面是倒排索引的现实世界的例子:一本书中的索引,其中每个术语都引用了一个页码。

    \n

    \n

    文本解析

    在我们开始建立索引之前,我们需要将原始文本分解成适合索引和搜索的单词(tokens)列表。

    \n

    文本解析器由一个分词器和多个过滤器组成。

    \n

    \n

    分词器(tokenizer)

    分词是文本解析的第一步,它的工作是将文本转换成一个单词列表。我们本次的实现是在一个词的边界上分割文本,并删除标点符号。

    \n
    func tokenize(text string) []string {
    return strings.FieldsFunc(text, func(r rune) bool {
    // Split on any character that is not a letter or a number.
    return !unicode.IsLetter(r) && !unicode.IsNumber(r)
    })
    }
    \n
    > tokenize("A donut on a glass plate. Only the donuts.")

    ["A", "donut", "on", "a", "glass", "plate", "Only", "the", "donuts"]
    \n

    过滤器

    大部数情况下,仅仅将文本转换为一个单词列表是不够的。为了使文本更容易被索引和搜索,我们还需要做额外的规范化处理。

    \n

    小写字母

    为了使搜索不区分大小写,小写过滤器将单词转换为小写。cAt、Cat 和 caT 被归一化为 cat。之后在我们查询索引时,也会将搜索词进行小写处理。这样就可以让搜索词 cAt 与文本 Cat 相匹配了。

    \n
    func lowercaseFilter(tokens []string) []string {
    r := make([]string, len(tokens))
    for i, token := range tokens {
    r[i] = strings.ToLower(token)
    }
    return r
    }
    \n
    > lowercaseFilter([]string{"A", "donut", "on", "a", "glass", "plate", "Only", "the", "donuts"})

    ["a", "donut", "on", "a", "glass", "plate", "only", "the", "donuts"]
    \n

    排除常用词

    几乎所有英语文本都包含常用的单词,如 a、I、the 或 be。这样的词被称为停词,我们要将它们删掉,因为几乎任何文档都会与这些停顿词相匹配。

    \n

    没有「官方」的停词表,这里我们把 OEC 排名的前10的词进行排除。

    \n
    var stopwords = map[string]struct{}{
    "a": {}, "and": {}, "be": {}, "have": {}, "i": {},
    "in": {}, "of": {}, "that": {}, "the": {}, "to": {},
    }

    func stopwordFilter(tokens []string) []string {
    r := make([]string, 0, len(tokens))
    for _, token := range tokens {
    if _, ok := stopwords[token]; !ok {
    r = append(r, token)
    }
    }
    return r
    }
    \n
    > stopwordFilter([]string{"a", "donut", "on", "a", "glass", "plate", "only", "the", "donuts"})

    ["donut", "on", "glass", "plate", "only", "donuts"]
    \n

    词干化

    由于语法规则的原因,文档中可能包括同一个词的不同形式。词干化将单词还原为其基本形式。例如,fishing、fished 和 fishe r可以被还原为基本形式(词干)fish。

    \n

    实现词干化是一项很大的任务,在本文中不进行涉及。我们将采用现有的一个模块。

    \n
    import snowballeng "github.com/kljensen/snowball/english"

    func stemmerFilter(tokens []string) []string {
    r := make([]string, len(tokens))
    for i, token := range tokens {
    r[i] = snowballeng.Stem(token, false)
    }
    return r
    }
    \n
    > stemmerFilter([]string{"donut", "on", "glass", "plate", "only", "donuts"})

    ["donut", "on", "glass", "plate", "only", "donut"]
    \n
    \n

    注:词干并不总是一个有效的词。例如,有些词干器可能会将 airline 简化为 airlin。

    \n
    \n

    将解析器组合在一起

    func analyze(text string) []string {
    tokens := tokenize(text)
    tokens = lowercaseFilter(tokens)
    tokens = stopwordFilter(tokens)
    tokens = stemmerFilter(tokens)
    return tokens
    }
    \n

    分词器和过滤器将句子转换为一个单词列表:

    \n
    > analyze("A donut on a glass plate. Only the donuts.")

    ["donut", "on", "glass", "plate", "only", "donut"]
    \n

    这个列表已经做好了索引的主板内。

    \n
    \n

    建立索引

    回到倒排索引,它把文档中的每个词都映射到文档 ID 上。内置的 map 是存储该映射很好的选择。map 中的键为单词(字符串),值为文档 ID 的列表。

    \n
    type index map[string][]int
    \n

    建立索引的过程包括解析文档(调用前边的 analyze 函数)并将其 ID 添加到 map 中。

    \n
    func (idx index) add(docs []document) {
    for _, doc := range docs {
    for _, token := range analyze(doc.Text) {
    ids := idx[token]
    if ids != nil && ids[len(ids)-1] == doc.ID {
    // Don't add same ID twice.
    continue
    }
    idx[token] = append(ids, doc.ID)
    }
    }
    }

    func main() {
    idx := make(index)
    idx.add([]document{{ID: 1, Text: "A donut on a glass plate. Only the donuts."}})
    idx.add([]document{{ID: 2, Text: "donut is a donut"}})
    fmt.Println(idx)
    }
    \n

    map 中的每个单词都指向包含该单词的文档 ID。

    \n
    map[donut:[1 2] glass:[1] is:[2] on:[1] only:[1] plate:[1]]
    \n

    查询

    为了对索引进行查询,我们对查询词使用与索引的相同分词器和过滤器:

    \n
    func (idx index) search(text string) [][]int {
    var r [][]int
    for _, token := range analyze(text) {
    if ids, ok := idx[token]; ok {
    r = append(r, ids)
    }
    }
    return r
    }
    \n
    > idx.search("Small wild cat")

    [[24, 173, 303, ...], [98, 173, 765, ...], [[24, 51, 173, ...]]
    \n

    最后,我们可以找到所有提到 cat 的文件。搜索 60 多万个文档只花了不到一毫秒的时间。

    \n

    有了倒排索引,搜索查询的时间复杂度与要搜索单词的数量成线性关系。在上面的例子中,解析完输入文本后,搜索只需要进行三次 map 查询。

    \n

    布尔查询

    上边的查询为每一个单词都返回了一个文档 ID 列表。当我们在搜索框中输入 small wild cat 时,我们通常期望找到的是一个同时包含 small、wild 和 cat 的结果列表。下一步是计算这些列表之间的集合交集,这样我们就可以得到一个与所有单词相匹配的文件列表。

    \n

    \n

    我们的倒排索引中的 ID 是以升序插入的。由于 ID 是有序的,所以可以在线性时间内计算两个列表之间的交集。intersection 函数同时遍历两个列表,并收集同时存在于两个列表中的 ID。

    \n
    func intersection(a []int, b []int) []int {
    maxLen := len(a)
    if len(b) > maxLen {
    maxLen = len(b)
    }
    r := make([]int, 0, maxLen)
    var i, j int
    for i < len(a) && j < len(b) {
    if a[i] < b[j] {
    i++
    } else if a[i] > b[j] {
    j++
    } else {
    r = append(r, a[i])
    i++
    j++
    }
    }
    return r
    }
    \n

    使用更新后的 search 方法解析给定的查询文本查找单词计算ID列表之间的集合交集

    \n
    func (idx index) search(text string) []int {
    var r []int
    for _, token := range analyze(text) {
    if ids, ok := idx[token]; ok {
    if r == nil {
    r = ids
    } else {
    r = intersection(r, ids)
    }
    } else {
    // Token doesn't exist.
    return nil
    }
    }
    return r
    }
    \n

    维基百科的离线数据中只有两个文档同时与 small、wild 和 cat 相匹配。

    \n
    > idx.search("Small wild cat")

    130764 The wildcat is a species complex comprising two small wild cat species, the European wildcat (Felis silvestris) and the African wildcat (F. lybica).
    131692 Catopuma is a genus containing two Asian small wild cat species, the Asian golden cat (C. temminckii) and the bay cat.
    \n

    搜索到了我们所预期的结果!

    \n

    总结

    我们刚刚建立了一个全文检索引擎,尽管它很简单,但它可以成为更高级项目的基础。

    \n

    我没有触及到太多可以显著提高性能和使引擎更友好的内容,下面是几个进一步改进的想法:

    \n
      \n
    • 扩展布尔查询,支持 OR 和NOT

      \n
    • \n
    • 在磁盘上存储索引

      \n
        \n
      • 在每次应用重启时重建索引可能需要一段时间
      • \n
      • 大型索引可能不适合放在内存中
      • \n
      \n
    • \n
    • 尝试用内存和 CPU 高效的数据格式来存储文档 ID 集合

      \n\n
    • \n
    • 支持对多个文档字段进行索引

      \n
    • \n
    • 按相关性对结果进行排序

      \n
    • \n
    \n"},{"title":"Redis 缓存常见异常处理","url":"/2021/cache-common-abnormal/","content":"

    \"\"

    \n
    缓存不一致
    \t先删除缓存,再更新数据库
    \t\t问题: A 删除缓存后,更新 db 前,B 查询数据,缓存中没有到 db 中读到了旧数据,将旧数据设置到了缓存中
    \t\t解决:在 A 更新完数据库值以后,让它先 sleep 一小段时间,再进行一次缓存删除操作。(延迟双删)
    \t先更新数据库值,再删除缓存值
    \t\t问题:A 更新完 db 还没删除缓存前,B 查询数据命中就缓存,获取到旧数据,这个问题无解,但对业务影响较小。
    \t\t推荐这种方式
    \t\t\t先删除缓存值再更新数据库,有可能导致请求因缓存缺失而访问数据库,给数据库带来压力
    \t\t\t如果业务应用中读取数据库和写缓存的时间不好估算,那么,延迟双删中的等待时间就不好设置。

    缓存血崩
    \t原因
    \t\t大量数据同时过期
    \t\t缓存实例宕机
    \t应对方案
    \t\t给过期时间加随机数
    \t\t服务降级、熔断、限流
    \t\t主从集群

    缓存击穿
    \t原因:热点数据过期
    \t应对方案
    \t\t不给热点数据设置过期时间
    \t\t将数据提升为本地缓存

    缓存穿透
    \t原因:缓存和数据库中都没有要访问的数据
    \t\t误删数据
    \t\t恶意攻击
    \t应对方案
    \t\t缓存空值或缺省值
    \t\t使用布隆过滤器
    \t\t前端合法性检查
    \n"},{"title":"关于 CDN 的灵魂 16 问","url":"/2021/cdn-question/","content":"

    如果你把你的站点部署在 CDN 的后面,网站的 IP 地址是 CDN 还是后端服务器?

    是 CDN 的 IP

    \n

    所有请求都会发到 CDN,然后 CDN 在需要的情况下向后端服务器发出请求。

    \n

    CDN 是否只有一个数据中心用于缓存内容?

    不是

    \n

    CDN 会将内容缓存在不同地区的多个服务器上,这样你的用户无论在哪里都能迅速得到响应。

    \n

    CDN 只会缓存 HTTP 响应的 body(比如图片)吗?

    不是

    \n

    它可以缓存整个 HTTP 响应,包括状态码和响应头。

    \n

    所以,举例来说,如果你的服务器不小心返回了 404,CDN 将这个响应进行了缓存,那么即使服务器已经恢复,你的网站仍然可能是 404。

    \n

    如果你不小心缓存了错误的内容,是否能清理掉?

    是的

    \n

    CDN 提供方通常有清除缓存的方法。不过有时需要几分钟才能完成(CDN 可能要去告诉世界各地的数百台服务器来清理它们的缓存)。

    \n

    你可以选择从缓存中只删除特定文件或者删除所有缓存的文件。

    \n

    CDN 如何知道它应该把 HTTP 响应放在缓存中?

    依赖于客户端的请求

    \n

    当 CDN 收到一个资源请求时,它会从你的服务器上请求资源,然后会把资源放在它的缓存中,这样下次就不用再到你的服务器上请求了。

    \n

    CDN 可以缓存任何类型的 HTTP 的响应吗?

    是的

    \n

    如果你要求 CDN 进行缓存,它通常可以缓存任何你想要的 HTTP 响应,比如可以将响应头设置为: Cache-Control: public; max-age=3600

    \n

    不过大多数 CDN 会对缓存内容的大小进行限制,所以你可能无法缓存一个电影。

    \n

    CDN 是否可以在你的服务器宕机的情况下继续为你的站点提供服务?

    也许

    \n

    即使你的服务器没有运行,CDN 也可以继续提供缓存页面。

    \n

    但是如果你告诉它只缓存一定时间(比如 1 小时),内容可能会在一段时间后过期,无法访问。而如果内容根本没有被缓存,CDN 也帮不了你。

    \n

    如果你在 CDN 后面的网站使用 TLS,CDN 能读取你未加密的网站流量吗?

    是的

    \n

    如果你想让CDN缓存内容,它需要能够解密和读取。

    \n

    通常人们处理这个问题的方法是,只把静态内容(如 CSS/JS/图片)放在 CDN 后面的域名上,而使用一个单独的域名来处理带有用户数据的请求。例如,https://github.githubassets.com/ 在 CDN 后面,但https://github.com 不是。

    \n

    CDN 总是对资源进行缓存吗?

    不是

    \n

    如果你想,可以配置你的 CDN 不进行缓存,只是代理每个请求到你的后端服务器。

    \n

    是否可以判断出某个网站使用了 CDN?

    是的

    \n

    你通常可以从 header 中找出答案:运行curl -I https://jiapan.me,看看我使用的是什么 CDN。

    \n

    是否能判断出你收到的是一个被缓存的响应?

    是的

    \n

    CDN 通常会设置一个响应头,比如 x-cache: HIT,你可以用它来判断是缓存命中还是缓存失效。

    \n

    及时没有缓存,CDN 是否可以使请求更快?

    是的

    \n
      \n
    1. CDN通常可以在离客户端更近的地方终止 TLS,这意味着 TLS 握手可以快很多。如果你的后端服务器离客户端很远,这可以节省一秒左右的时间。这样做的原因是,它经常会与后端服务器保持一个开放的 TLS 连接,所以它不必每次都重新建立一个新的连接。
    2. \n
    3. 它可能比客户机有更快的路由连接到你的后端服务器。
    4. \n
    5. CDN还可以通过更多的方式来提高性能!
    6. \n
    \n

    如果你的站点只支持 HTTP/1.1,CDN 可以接收 HTTP/2.0 的请求吗?

    大部分情况下可以

    \n

    许多CDN可以透明地将 HTTP/2 请求翻译成 HTTP/1 请求到你的后端服务器,所以你可以在不做任何工作的情况下获得 HTTP/2 的很多性能优势。

    \n

    是否可以让 CDN 对响应只缓存一段时间(如10分钟)?

    是的

    \n

    您可以通过设置 Cache-Control 响应头来实现,比如 Cache-Control: max-age=600

    \n

    是否允许资源只被浏览器缓存而不被 CDN 缓存?

    是的

    \n

    你可以通过设置 Cache-Control: private, max-age=3600 来实现。private 意味着内容只能存储在浏览器的缓存中,而不是 CDN 的缓存中。

    \n

    如果你用同一个 URL 请求 CDN,但 header 不同,是否会得到相同的缓存响应?

    视情况而定

    \n

    默认情况下,会得到相同的响应。但如果服务器设置了 Vary: 头,那么 CDN 将为该头的每个值存储不同的缓存值。

    \n

    例如,Vary: Accept-Encoding 将使 CDN 同时存储压缩和非压缩版本。

    \n","tags":["cdn"]},{"title":"解决 CentOS 安装 MySQL-python 报错","url":"/2017/centos-fix-python-mysql/","content":"

    今天在服务器上用 pip 安装 MySQL-python 时报错,虽然之前处理过很多次,但都没有记录,这次记录一下。

    \n

    报错如下:

    \n
    Downloading/unpacking MySQL-python
    Downloading MySQL-python-1.2.5.zip (108kB): 108kB downloaded
    Running setup.py egg_info for package MySQL-python
    sh: mysql_config: command not found
    Traceback (most recent call last):
    File "<string>", line 16, in <module>
    File "/home/jiapan/.virtualenvs/appstore-crawler/build/MySQL-python/setup.py", line 17, in <module>
    metadata, options = get_config()
    File "setup_posix.py", line 43, in get_config
    libs = mysql_config("libs_r")
    File "setup_posix.py", line 25, in mysql_config
    raise EnvironmentError("%s not found" % (mysql_config.path,))
    EnvironmentError: mysql_config not found
    Complete output from command python setup.py egg_info:
    sh: mysql_config: command not found

    Traceback (most recent call last):

    File "<string>", line 16, in <module>

    File "/home/jiapan/.virtualenvs/appstore-crawler/build/MySQL-python/setup.py", line 17, in <module>

    metadata, options = get_config()

    File "setup_posix.py", line 43, in get_config

    libs = mysql_config("libs_r")

    File "setup_posix.py", line 25, in mysql_config

    raise EnvironmentError("%s not found" % (mysql_config.path,))

    EnvironmentError: mysql_config not found
    \n

    解决方法:

    \n

    sudo yum install python-devel mysql-devel

    \n"},{"title":"Centos 安装并启用 EPEL 源","url":"/2018/centos-install-epel-repo/","content":"

    Centos 默认提供的软件源资源很少,很多常用软件都没有:如 nginx,htop 等。

    \n

    EPEL(Extra Packages for Enterprise Linux) 是由 Fedora Special Interest Group 维护的 Enterprise Linux(RHEL、CentOS)中经常用到的包。

    \n

    通过 EPEL 可以很容易地通过 yum 命令从 EPEL 源上获取在 CentOS 自带源上没有的软件。

    \n

    首先安装 epel-release:

    \n
    yum install epel-release
    \n

    大多数网站到了这一步就告诉你安装好了,但是在我尝试的时候,发现这种方式 EPEL 源默认并不会生效,可以通过下边的命令进行验证:

    \n
    yum repolist | grep epel
    \n

    如果发现有类似下边的结果,说明 EPEL 源已生效:

    \n
    epel/x86_64           Extra Packages for Enterprise Linux 7 - x86_64      12,716
    \n

    如果没有输出这条命令,说明 EPEL 源默认没有开启,在安装软件时还需要手动指定源:

    \n
    yum --enablerepo=epel install nginx
    \n

    这种方式使用时比较麻烦,我们可以通过修改 EPEL 的配置文件来启用它。

    \n
    vi /etc/yum.repos.d/epel.repo
    \n

    可以看到里边有多个组,将 [epel] 组内的 enabled=0 改成 enabled=1

    \n

    这样就可以开启 EPEL 源了。

    \n
    \n

    参考:https://unix.stackexchange.com/questions/165916/trying-to-enable-epel-on-centos-6-and-it-wont-show-in-repolist

    \n
    \n"},{"title":"ChatGPT 参数介绍","url":"/2023/chatgpt-parameters/","content":"

    好久好久没有写博客了,说因为忙肯定是借口,主要还是没什么动力。

    \n

    最近我也追了追 AI 的风潮。不过相对来说,我开始使用 AI 的时间还算比较早,从前年开始就用 Github Coplit 辅助写代码,去年 12 月就用上了 ChatGPT 的网页版。ChatGPT 真正出圈的时间是在今年 1 月底。我还用 ChatGPT 的 API 写了几个小工具,甚至在公司的项目中也有使用。

    \n

    在使用 GPT 的 API 期间,遇到了几个参数读不懂官方文档在说什么的情况,网上能查到的内容也不多。因此,我结合几个查到的资料和自己的使用体验,对这三个参数做下说明。

    \n

    Temperature

    温度参数控制着生成文本的随机性

    \n
      \n
    • 当温度值为0时,表示引擎是预定义的,这意味着无论输入文本如何,它都会创建相同的输出。
    • \n
    • 当温度值为1时,会使引擎变得非常有创造力,但同时也承担了更大的风险。
    • \n
    \n

    Frequency penalty

    频率惩罚参数控制模型重复预测的趋势,减少已生成单词的概率。惩罚取决于一个词在预测中已出现的次数,降低了一个词被多次选择的概率。该惩罚不考虑词频,只考虑词是否出现在文本中。

    \n

    Presence penalty

    存在惩罚参数鼓励模型做出新颖的预测。如果某个词已经出现在预测文本中,则存在惩罚会降低该词的概率。与频率惩罚不同,存在惩罚不依赖于单词在过去预测中出现的频率。

    \n

    总结:

    本文总结了 ChatGPT 的三个主要参数:Temperature(温度)、Frequency penalty(频率惩罚)和Presence penalty(存在惩罚)。

    \n
      \n
    • Temperature 控制模型生成的多样性。
    • \n
    • 频率惩罚存在惩罚分别用于防止单词和主题的重复。
        \n
      • 不同文章对这两个惩罚的解释略有不同。
      • \n
      • 可以将 Frequency Penalty 视为避免单词重复的方法,将 Presence Penalty 视为避免主题重复的方法。
      • \n
      \n
    • \n
    \n"},{"title":"选择合适的日志级别","url":"/2022/choice-log-level/","content":"

    \"1.png\"

    \n"},{"title":"如何解决代码中存在的循环依赖问题","url":"/2020/circular-dependence/","content":"
    \"\"
    \n\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 / <当前的等级> + <任务初始积分>,健康等级越低,做任务所能得到的积分也就越高,这样可以鼓励用户多做任务。

    \n

    背景就介绍到这里,对于这个常见我们可以抽象出两个类:一个是代表档案的 HealthRecord 类、另一个是代表健康任务的 HealthTask 类。

    \n
    \"\"
    \n\n

    其中 HealthRecord 类中提供了一个获取健康等级的方法 getHealthLevel() 来计算健康等级,同时也提供了添加任务的方法 addTask()

    \n
    public class HealthRecord {

    private List<HealthTask> tasks = new ArrayList<>();

    public Integer getHealthLevel() {
    if (tasks.size() > 5) {
    return 1;
    }
    if (tasks.size() < 2) {
    return 3;
    }
    return 2;
    }

    public void addTask(String taskName, Integer initialHealthPoint) {
    HealthTask task = new HealthTask(this, taskName, initialHealthPoint);
    tasks.add(task);
    }

    public List<HealthTask> getTasks() {
    return tasks;
    }

    }
    \n

    对应的 HealthTask 中,显然应该包含对 HealthRecord 的引用,同时也实现了计算任务积分的方法 calculateHealthPointForTask()calculateHealthPointForTask() 方法中用到了 HealthRecord 中的健康等级信息 getHealthLevel()

    \n
    public class HealthTask {

    private HealthRecord record;

    private String taskName;

    private Integer initialHealthPoint;

    public HealthTask(HealthRecord record, String taskName, Integer initialHealthPoint) {
    this.record = record;
    this.taskName = taskName;
    this.initialHealthPoint = initialHealthPoint;
    }

    public Integer calculateHealthPointForTask() {
    Integer healthPointFromHealthLevel = 12 / record.getHealthLevel();

    return initialHealthPoint + healthPointFromHealthLevel;
    }

    public String getTaskName() {
    return taskName;
    }

    public Integer getInitialHealthPoint() {
    return initialHealthPoint;
    }
    }
    \n

    不难看出,HealthRecordHealthTask 之间存在明显的相互依赖关系。

    \n

    我们可以使用 IDEA 自带的 Analyze Dependency Matrix 对包含 HealthRecordHealthTask 类的包进行分析,得出系统中存在循环依赖代码的提示。

    \n
    \"\"
    \n\n

    Analyze Dependency Matrix 的使用细节可以参考官方文档:https://www.jetbrains.com/help/idea/dsm-analysis.html,这里我们只关心是否存在循环依赖,也就是那个红色的框框。

    \n

    通过上边的例子,我们了解了如何有效识别代码中存在循环依赖的问题,下边再来看看如何消除代码中的循环依赖。

    \n

    软件行业有一句非常经典的话:「当我们在碰到一个问题无从下手时,不妨考虑一下是否可以通过加一层的方法来解决」。消除循环依赖的基本思路也是一样的,有三种常见的方法:提取中介者、转移业务逻辑、采用回调接口。

    \n

    提取中介者

    提取中介者方法也被称为关系上移,其核心思想就是把两个相互依赖的组件中的交互部分抽象出来形成一个新的组件,而这个新的组件包含着原有两个组件的引用,这样就把循环依赖关系剥离出来,并提取到一个专门的中介者的组件中。

    \n
    \"\"
    \n\n

    这个中介者组件的实现也不难,可以通过提供一个计算积分的方法对循环依赖进行剥离,这个方法同时依赖 HealthRecordHealthTask 对象,并实现了原有 HealthTask 中根据 HealthRecord 的健康等级信息计算积分的业务逻辑。

    \n
    public class HealthPointMediator {
     
        private HealthRecord record;
     
        public HealthPointMediator(HealthRecord record) {
            this.record = record;
        }
     
        public Integer calculateHealthPointForTask(HealthTask task) {
            Integer healthPointFromHealthLevel = 12 / record.getHealthLevel();
            return task.getInitialHealthPoint() + healthPointFromHealthLevel;
        }
    }
    \n

    可以看到上边的 calculateHealthPointForTask() 方法中,我们从 HealthRecord 中获取了等级,然后再从传入的 HealthTask 中获取初始积分,从而完成了对整个积分的计算过程,这个时候的HealthTask 就变得非常简单了,因为已经不包含任何有关 HealthRecord 的依赖。

    \n
    public class HealthTask {

    private String taskName;

    private Integer initialHealthPoint;

    public HealthTask(String taskName, Integer initialHealthPoint) {
    this.taskName = taskName;
    this.initialHealthPoint = initialHealthPoint;
    }

    public String getTaskName() {
    return taskName;
    }

    public Integer getInitialHealthPoint() {
    return initialHealthPoint;
    }
    }
    \n

    下边针对「提取中介者」这种消除循环依赖的实现方法来编写一个测试用例:

    \n
    public class HealthPointTest {

    public static void main(String[] args) {
    HealthRecord record = new HealthRecord();
    record.addTask("忌烟酒", 5);
    record.addTask("每周跑步3次", 4);
    record.addTask("每天喝2升水", 4);
    record.addTask("晚上10点按时睡觉", 3);
    record.addTask("晚上8点后不再吃东西", 1);

    HealthPointMediator mediator = new HealthPointMediator(record);

    for (HealthTask task : record.getTasks()) {
    System.out.println(mediator.calculateHealthPointForTask(task));
    }
    }

    }
    \n

    HealthRecord 中我们创建了 5 个 HealthTask,并赋予了不同的初始积分。然后通过 HealthPointMediator 这个中间者分别对每个 Task 进行积分计算。最后我们再次使用 Analyze Dependency Matrix 分析下当前的代码是否有循环依赖。

    \n
    \"\"
    \n\n

    可以发现这次代码中已经不存在任何的环了。

    \n

    转移业务逻辑

    转移业务逻辑也被称为关系下移,其实现思路在于提取一个专门的业务组件 HealthLevelHandler 来完成对健康等级的计算过程,HealthTask 原有的对 HealthRecord 的依赖,就转移到了对 HealthLevelHandler 的依赖,而 HealthLevelHandler 本身是不需要依赖任何业务对象的。

    \n
    \"\"
    \n\n
    public class HealthLevelHandler {

    private Integer taskCount;

    public HealthLevelHandler(Integer taskCount) {
    this.taskCount = taskCount;
    }

    public Integer getHealthLevel() {
    if (taskCount > 5) {
    return 1;
    }
    if (taskCount < 2) {
    return 3;
    }
    return 2;
    }
    }
    \n

    HealthLevelHandler 的实现也不难,包含了对等级的计算过程,具体到这里就是实现 getHealthLevel() 方法,随着业务组件的提取,HealthRecord 需要做相应的改造,getHealthPointHandler 就封装了对 HealthLevelHandler 的创建过程:

    \n
    public class HealthRecord {

    private List<HealthTask> tasks = new ArrayList<>();

    public HealthLevelHandler getHealthLevelHandler() {
    return new HealthLevelHandler(tasks.size());
    }

    public void addTask(String taskName, Integer initialHealthPoint) {
    HealthTask task = new HealthTask(taskName, initialHealthPoint);
    tasks.add(task);
    }

    public List<HealthTask> getTasks() {
    return tasks;
    }

    }
    \n

    对应的 HealthTask 也需要进行改造:

    \n
    public class HealthTask {
     
        private String taskName;
     
        private Integer initialHealthPoint;
     
        public HealthTask(String taskName, Integer initialHealthPoint) {
            this.taskName = taskName;
            this.initialHealthPoint = initialHealthPoint;
        }
     
        public Integer calculateHealthPointForTask(HealthLevelHandler handler) {
            Integer healthPointFromHealthLevel = 12 / handler.getHealthLevel();
     
            return initialHealthPoint + healthPointFromHealthLevel;
        }
     
        public String getTaskName() {
            return taskName;
        }
     
        public Integer getInitialHealthPoint() {
            return initialHealthPoint;
        }
    }
    \n

    calculateHealthPointForTask() 方法中,传入一个 HealthLevelHandler 来获取等级,然后根据获取的等级计算最终的积分。

    \n

    最后我们对测试类进行改造:

    \n
    public class HealthPointTest {

    public static void main(String[] args) {
    HealthRecord record = new HealthRecord();
    record.addTask("忌烟酒", 5);
    record.addTask("每周跑步3次", 4);
    record.addTask("每天喝2升水", 4);
    record.addTask("晚上10点按时睡觉", 3);
    record.addTask("晚上8点后不再吃东西", 1);

    HealthLevelHandler handler = record.getHealthPointHandler();

    for (HealthTask task : record.getTasks()) {
    System.out.println(task.calculateHealthPointForTask(handler));
    }
    }

    }
    \n

    现在 HealthTaskHealthRecord 都已经只剩下对 HealthLevelHandler 的依赖了。

    \n

    采用回调接口

    所谓的回调本质上就是一种双向的调用关系,也就是说被调用方在调用别人的同时也会被别人所调用。

    \n

    我们可以提取一个用于计算健康等级的业务接口(HealthLevelHandler),然后让 HealthRecord 去实现这个接口,HealthTask 在计算积分的时候只需要依赖这个业务接口而不需要关心这个接口的具体实现类。

    \n
    \"\"
    \n\n

    我们同样将这个接口命名为 HealthLevelHandler,包含一个计算健康等级的方法定义。

    \n
    public interface HealthLevelHandler {

    Integer getHealthLevel();

    }
    \n

    有了这个接口,HealthTask 就再不存在对 HealthRecord 的依赖,而是在构造函数中注入 Handler 接口:

    \n
    public class HealthTask {

    private String taskName;

    private Integer initialHealthPoint;

    private HealthLevelHandler handler;

    public HealthTask(String taskName, Integer initialHealthPoint, HealthLevelHandler handler) {
    this.taskName = taskName;
    this.initialHealthPoint = initialHealthPoint;
    this.handler = handler;
    }

    public Integer calculateHealthPointForTask() {
    Integer healthPointFromHealthLevel = 12 / handler.getHealthLevel();

    return initialHealthPoint + healthPointFromHealthLevel;
    }

    public String getTaskName() {
    return taskName;
    }

    public Integer getInitialHealthPoint() {
    return initialHealthPoint;
    }
    }
    \n

    在这里的 calculateHealthPointForTask() 方法中,我们也只会使用 Handler 接口所提供的方法来获取所需的健康等级,并计算积分。

    \n

    现在的 HealthRecord 需要实现 HealLevelHandler 接口,并提供计算健康等级的具体业务逻辑:

    \n
    public class HealthRecord implements HealthLevelHandler {

    private List<HealthTask> tasks = new ArrayList<>();

    @Override
    public Integer getHealthLevel() {
    if (tasks.size() > 5) {
    return 1;
    }
    if (tasks.size() < 2) {
    return 3;
    }
    return 2;
    }

    public void addTask(String taskName, Integer initialHealthPoint) {
    HealthTask task = new HealthTask(taskName, initialHealthPoint, this);
    tasks.add(task);
    }

    public List<HealthTask> getTasks() {
    return tasks;
    }

    }
    \n

    addTask() 方法中,当创建 HealthTask 时,HealthRecord 需要把自己作为一个参数传入到 HealthTask 的构造函数中,这样我们就通过回调方法完成了对系统的改造。

    \n

    采用回调方法,测试用例的代码业务变得更加简洁:

    \n
    public class HealthPointTest {

    public static void main(String[] args) {
    HealthRecord record = new HealthRecord();
    record.addTask("忌烟酒", 5);
    record.addTask("每周跑步3次", 4);
    record.addTask("每天喝2升水", 4);
    record.addTask("晚上10点按时睡觉", 3);
    record.addTask("晚上8点后不再吃东西", 1);

    for (HealthTask task : record.getTasks()) {
    System.out.println(task.calculateHealthPointForTask());
    }
    }

    }
    \n

    我们没有发现除了 HealthRecordHealthTask 之外的任何第三方对象,同样也可以使用 Analyze Dependency Matrix 来验证当前系统中是否存在循环依赖关系。

    \n

    最后我放一张整体分析结果,从上到下依次为:回调接口、不采用任何措施、提取中介者、转移业务逻辑。

    \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
      \n
    • 访问速度
    • \n
    • 绑定自定义域名
    • \n
    • 便于部署
    • \n
    • 费用
    • \n
    • 自动配置 https
    • \n
    \n

    我目前使用的就是静态博客,托管在了 3 个地方,而且各有一些弊端:

    \n
      \n
    1. Github Pages:国内的访问速度一般
    2. \n
    3. 七牛云:会收取少量费用、无法绑定未在国内备案的域名、需要手动配置 https 证书
    4. \n
    5. VPS + Cloudflare CDN:需提前购买 VPS、配置 Nginx,上手难度略大
    6. \n
    \n

    今天我们就利用 Cloudfalre Works 来部署一个满足上边所有条件的博客。

    \n

    Cloudflare CDN

    在使用 VPS + Cloudflare CDN 方案时,我们将博客的静态文件放在 VPS 上,并通过 Nginx 搭起一个静态站点,然后前置一个 Cloudflare CDN 来做静态资源加速和 https 的处理,即我们的 VPS 来作为静态文件的源站。

    \n

    \"\"

    \n

    Cloudflare Workers

    使用 Cloudflare Workers 方案可以无需准备 VPS。

    \n

    \"\"

    \n

    Cloudflare Workers 本质上是一个边缘计算服务,举几个例子:

    \n
      \n
    • 将不同类型的请求按路线发送到不同的源服务器。
    • \n
    • 在边缘网络展开HTML模板,以降低原始带宽成本。
    • \n
    • 将访问控制应用于缓存的内容。
    • \n
    • 将一小部分用户重定向到开发用服务器。
    • \n
    • 在两个完全不同的后端之间执行A / B测试。
    • \n
    • 构建完全依赖Web API的“无服务器”应用程序。
    • \n
    • ……
    • \n
    \n

    了解更多可以参考:https://blog.cloudflare.com/zh-cn/cloudflare-workers-unleashed-zh-cn/

    \n

    生成静态站点

    目前,生成静态站点的方案有很多,比如 HugoHexoJekyll 等,下边我以 Hugo 为例来生成一个站点,其他方案可以参考对应的官方文档。

    \n

    安装 hugo 命令

    brew install hugo

    # 验证 hugo 是否安装成功
    hugo version
    \n

    生成新站点

    hugo new site quickstart
    \n

    执行这个命令后,会在执行的目录下创建出一个名为 quickstart 的目录,里边就是我们新站点的内容。

    \n

    修改站点主题

    下载主题:

    \n
    cd quickstart
    git init
    git submodule add https://github.com/theNewDynamic/gohugo-theme-ananke.git themes/ananke
    \n

    配置主题:

    \n
    echo theme = \\\"ananke\\\" >> config.toml
    \n

    创建一篇文章

    hugo new posts/my-first-post.md
    \n

    修改 content/posts 下的文章源文件,将 draft 改为 false,就可以正常发布了,正文随便点什么。

    \n
    ---
    title: \"My First Post\"
    date: 2021-10-04T21:45:54+08:00
    draft: false
    ---

    这是我们的第一篇文章
    \n

    浏览效果

    hugo server -D
    \n

    \"\"

    \n

    此时我们可以访问:http://localhost:1313/ 看下效果。

    \n

    生成静态文件

    使用 hugo -D 命令生成静态文件用来发布到 Cloudflare Workers 上,hugo 生成的静态文件在项目目录的 public 下。

    \n

    发布到 Cloudflare Workers

    发布前需要先注册自己的 Cloudflare 账号,开通 Workers 服务,在 Workers 页面右侧可以修改自己的子域名,比如我的子域名为 panmax.workers.dev,即我发布的服务都是以 panmax.workers.dev 结尾,比如:https://hugo.panmax.workers.dev/

    \n

    安装 wrangler 命令

    wrangler 是 Cloudflare workers 为开发人员提供的 CLI 工具。使用 npm 进行安装:

    \n
    npm i @cloudflare/wrangler -g
    \n

    如果提示 node 版本太低,可以通过 nvm 来切换版本:

    \n
    brew install nvmnvm install 12
    \n

    初始化 cloudflare workers 项目

    在刚才生成的 quickstart 目录下执行以下命令来初始化 cloudflare workers 项目,这个命令会在当前目录下生成 wrangler.toml 文件和 workers-site 目录。

    \n
    wrangler init --site hugo
    \n

    编辑 wrangler.toml

    将 wrangler.toml 中的 bucket 改为我们静态目录的路径:

    \n
    # 根据你的项目,将 bucket 改成生成静态文件的目录
    site = {bucket = \"./public\",entry-point = \"workers-site\"}
    \n

    其他参数暂时无需修改。

    \n

    用户登录

    使用 wrangler login 来完成登录,在弹出的页面中点击 Allow 即可。当命令行打印出 「✨ Successfully configured. 」就说明我们登录成功了,会在 home 下生成 .wrangler 目录,里边记录了我们的用户信息。

    \n
    \n

    这个操作只需进行一次,后续发布时就不用再执行了。

    \n
    \n

    发布

    最后,使用 wrangler publish 即可将我们的静态站点发布到 cloudflare workers 上了。同时还会将我们站点的地址打印出来:

    \n

    \"\"

    \n

    用浏览器访问这个地址就能看到效果了:

    \n

    \"\"

    \n

    配置自定义域名

    如果你在 cloudflare 上托管了自己的域名,还可以将自己的域名映射到 workers 上。

    \n

    配置 CNAME

    在你的 DNS 配置中新增一条 CNAME 规则,名称是你想关联的子域名,目标为 workers 为你提供给的域名。

    \n

    比如,我要将 hugo.jiapan.me 关联到刚才发布的站点上,此时我的名称填写 hugo,目标填写 hugo.panmax.workers.dev

    \n

    \"\"

    \n

    关联 workers

    在域名管理页面上边的菜单中点击 workers,点击「添加路由」,还是以我刚才配置的域名为例,路由填写 hugo.jiapan.me/* ,Workers 选择 hugo,点击保存。

    \n

    \"\"

    \n

    之后我们就可以使用自定义域名来访问我们的站点了:

    \n

    \"\"

    \n"},{"title":"非常好用的代码速查工具 cheat.sh","url":"/2020/code-serch-cheat-sh/","content":"
    \"\"
    \n\n

    介绍

    cheat.sh 号称自己提供了世界优质技术社区中代码速查表的统一访问。

    \n

    安装

    curl https://cht.sh/:cht.sh | sudo tee /usr/local/bin/cht.sh
    chmod +x /usr/local/bin/cht.sh
    \n

    使用

    基本的查询命令为:cht.sh 语言 问题关键词

    \n

    例1:查命令

    比如,你不知道上边安装命令中 tee 是什么意思,可以尝试用下边的命令查看提示:

    \n
    cht.sh linux tee
    \n
    \"\"
    \n\n

    由此可以看出 cht.sh 不限于查代码,还可以查命令的用法。

    \n

    例2:查函数

    再举个例子,假如我不知道 go 中有没有能够判断字符串中是否包含某个字符的函数,可以使用:

    \n
    cht.sh go string contain
    \n
    \"\"
    \n\n

    例3:查实现

    或者我想知道 go 中如何反转一个 list

    \n
    cht.sh go reverse list
    \n
    \"\"
    \n\n

    进入交互模式

    想进入 cht.sh 的交互模式,需要先安装 rlwrap 这个工具。

    \n

    Mac 安装方式:brew install rlwrap

    \n

    进入交互模式的命令为:cht.sh --shell

    \n

    进入交互模式后,就不用再输入 cht.sh 的命令了,直接问问题就可以,比如我想知道 go 中如何将 int 转为 string:

    \n
    \"\"
    \n\n

    更进一步,如果想在后续的查询者固定查询某个语言,可以通过 cd 命令,这样在后续的查询中连语言都可以省掉:

    \n
    \"\"
    \n\n

    也可以在进入交互界面时指定语言 cht.sh --shell go

    \n
    \"\"
    \n\n

    在指定了语言的交互界面中,如果想在不 cd 到其他语言的情况下临时查询其他语言的用法,可以通过以 / 开头 临时指定语言:

    \n
    \"\"
    \n\n

    更多用法可以参考项目的 README.md

    https://github.com/chubin/cheat.sh

    \n"},{"title":"collections.Counter源码阅读笔记","url":"/2016/collections-Counter%E6%BA%90%E7%A0%81/","content":"

    这几天用到 collections.Counter 的次数挺多的(比如热门标签、热门愿望、热门城市),看文档的时候就很好奇,这个类初始化的时候是如何实现既可以不传参数,可以传一个可迭代的值,可以传字典,而且还可以传关键字。

    \n

    文档范例:

    \n
    >>> c = Counter()                           # a new, empty counter
    >>> c = Counter('gallahad') # a new counter from an iterable
    >>> c = Counter({'a': 4, 'b': 2}) # a new counter from a mapping
    >>> c = Counter(a=4, b=2) # a new counter from keyword args
    \n

    先来看他的 __init__方法

    \n
    def __init__(*args, **kwds):
    if not args:
    raise TypeError("descriptor '__init__' of 'Counter' object "
    "needs an argument")
    self = args[0]
    args = args[1:]
    if len(args) > 1:
    raise TypeError('expected at most 1 arguments, got %d' % len(args))
    super(Counter, self).__init__()
    self.update(*args, **kwds)
    \n

    刚开始不明白,明明判断了 args 不存在的话就抛出异常,但是却可以是使用无参来初始化这个类,后来想明白了,类初始化的时候会默认给一个参数,我们通常把这个参数命名为 self,所以才有了下边的语句,将 selfargs[0] 中取出。所以 args[1:] 就是剩下的参数,注意这时候 args 还是一个元组,现在来判断 args 的长度,因为除了关键字参数外,只能使用一个可迭代的值或者一个字典来初始化,所以如果args长度大于1,说明除了关键字参数外,给了超过一个以上的参数,这时候程序抛出异常。

    \n

    这时候我就想既然是一个参数,为什么不直接给个固定值,比如像这样:

    \n

    __init(self, iterable_or_mapping, **kwds)__

    \n

    而是非要作为一个可变参数传入,然后再判断长度,后来想到因为还需要接受无参调用,所以不能用这样写。

    \n

    这时候又想到能不能这样写呢:

    \n

    __init(self, iterable_or_mapping=None, **kwds)__

    \n

    答案是不能,因为你不知道调用者在调用时关键字参数要传什么,万一调用者要传 iterable_or_mapping 作为关键字参数怎么办。

    \n

    最后想到能不能这样:

    \n

    __init(self, *args, **kwds)__

    \n

    觉得好像没什么问题。。。不知道阅读者有没有看出来问题,如果看出这样写有什么问题麻烦告诉我,我把评论关闭了,可以通过邮箱: `jiapan.china@gmail.com`

    \n

    接下来然后调用他的 update 方法:

    \n
    def update(*args, **kwds):
    if not args:
    raise TypeError("descriptor 'update' of 'Counter' object "
    "needs an argument")
    self = args[0]
    args = args[1:]
    if len(args) > 1:
    raise TypeError('expected at most 1 arguments, got %d' % len(args))
    iterable = args[0] if args else None
    if iterable is not None:
    if isinstance(iterable, Mapping):
    if self:
    self_get = self.get
    for elem, count in iterable.iteritems():
    self[elem] = self_get(elem, 0) + count
    else:
    super(Counter, self).update(iterable) # fast path when counter is empty
    else:
    self_get = self.get
    for elem in iterable:
    self[elem] = self_get(elem, 0) + 1
    if kwds:
    self.update(kwds)
    \n

    刚开始和 __init__ 一样,就不说了,从 iterable = args[0] if args else None 开始说。

    \n

    因为此时 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) + countself 中尝试获取 elem 的数量,如果没有就初始化为0然后加上字典的值,这样写而不是直接 self[elem] = count是有原因的,因为我们还可能给关键字参数,这样写的好处是,一会用关键字参数再递归调用一下这个方法,就能把关键字参数的值也更新上去啦。

    \n

    iterable 不是字典的时候,就会把这个值进行迭代取出每个元素计算出现的次数,具体代码就不解释了。

    \n
    \n

    最后的疑惑:我不明白的是为什么要判断 self 存不存在,在什么情况下初始化一个类会没有 self

    \n
    \n

    下一篇准备写写 Counter 中两个常用方法 most_commonelements。因为现在我还没看。。。

    \n","categories":["源码"],"tags":["源码","Python"]},{"title":"collections.Counter的使用","url":"/2016/collections-Counter%E7%9A%84%E4%BD%BF%E7%94%A8/","content":"

    有一个需求是获取所有存在愿望的城市,并且找出热门城市。

    \n

    刚好前几天看到了Python的计数器:collections.Counter, 于是就拿来用了用。

    \n
      \n
    • 首先导入 Counter:
    • \n
    \n
    from collections import Counter
    \n
      \n
    • 通过查询数据库获取到所有满足条件的愿望放在wishes数组中,然后遍历所有数据,把所有城市的adcode存在一个数组中:
    • \n
    \n
    cities = [wish.adcode for wish in wishes]
    \n
    \n

    (我发现我最近越来越用推导式了。。。

    \n
    \n
      \n
    • 然后使用Counter进行操作:
    • \n
    \n
    c = Counter(cities).most_common()
    \n

    这样得到的结果是一个数组,每个元素是一个长度为2的元组,元组第0位保存的是数据,第1位保存的是这个数据出现的次数,整个数组是按照元素出现次数排序的,most_common()还可以带int型的参数,表示获取排名前n个的结果。不带参数表示获取所有结果。

    \n

    以上得到的结果为:

    \n
    [(110000, 4), (510800, 3)],...
    \n

    获取热门城市只需拿前4个数据就行(需求是4个热门城市),其他城市拿剩下的数据即可(注意,c 里每个元素是一个元组,元组的一个0位才是我们要的数据):

    \n
    top4 = [_[0] for _ in c[:4]]
    others = [_[0] for _ in c[4:]]
    \n

    现在 top4 里存放的就是排名前四的城市adcode,others 里存放的是其他有愿望地区的城市adcode按照愿望数量排序的数组。

    \n","categories":["Code"],"tags":["Python","魔镜"]},{"title":"常用命令记录","url":"/2019/common-use-commands/","content":"

    在性能非常有限的机器上启动一个 MySQL

    docker run --env TZ='Asia/Shanghai' \\
    --name daily-goals-mysql \\
    -v /data/daily-goals-mysql:/var/lib/mysql \\
    -e MYSQL_ROOT_PASSWORD=xxxx -d \\
    --restart=always -p 13307:3306 \\
    --memory=512m mariadb:10.2 \\
    --character-set-server=utf8mb4 \\
    --collation-server=utf8mb4_unicode_ci \\
    --performance_schema=off \\
    --key_buffer_size=32M \\
    --query_cache_size=16M --query-cache-limit=32M \\
    --tmp_table_size=4M \\
    --innodb_buffer_pool_size=32M --innodb_log_buffer_size=2M \\
    --max_connections=50 --sort_buffer_size=32M \\
    --read_buffer_size=2m --read_rnd_buffer_size=2m \\
    --join_buffer_size=128K \\
    --thread_stack=196K
    \n

    YouTube 下载

    # 查看所有支持下周的格式

    docker run --rm --user $UID:$GID \\
    -v $PWD:/downloads wernight/youtube-dl \\
    -F https://www.youtube.com/watch?v=gxj96RCun_k
    \n
    # 下载指定格式

    docker run --rm --user $UID:$GID \\
    -v $PWD:/downloads wernight/youtube-dl \\
    -f code https://www.youtube.com/watch?v=gxj96RCun_k
    \n

    ss

    docker run -e PASSWORD=xxxxxx -e METHOD=aes-256-cfb \\
    -p 8443:8388 -d --restart=always \\
    --name ssserver shadowsocks/shadowsocks-libev
    \n

    七牛

    qshell account ak sk panmax

    qshell rput bucket name filepath
    \n

    CPU

    查看物理CPU个数

    \n
    cat /proc/cpuinfo| grep "physical id"| sort| uniq| wc -l
    \n

    查看每个物理CPU中core的个数(即核数)

    \n
    cat /proc/cpuinfo| grep "cpu cores"| uniq
    \n

    查看逻辑CPU的个数

    \n
    cat /proc/cpuinfo| grep "processor"| wc -l
    \n

    Docker 删除所有 none 镜像

    docker images|grep none|awk '{print $3 }'|xargs docker rmi
    \n

    Docker 停止所有容器

    docker stop $(docker ps -aq)
    \n

    Docker 删除所有容器

    docker rm $(docker ps -aq)
    \n

    Docker 进入交互式容器

    sudo docker exec -it {{containerName or containerID}} bash
    \n

    TODO…

    "},{"title":"配置不规范导致的 bug 复盘","url":"/2021/config-file-bug-review/","content":"

    昨天晚上我们的推荐服务出现故障,排查到很晚。影响了50多个主播和用户的曝光卡的使用效果,虽然没有产生特别大的事故,但我觉得自己还是有必要做个复盘,毕竟有自己做的不好的地方。

    \n

    时间轴

      \n
    • 大概21:20分,服务开始有大量错误报警,推荐帧 v2接口和附近动态的 rpc 请求全部进入降级状态。

      \n
    • \n
    • 22:00 报错突然降成 0

      \n
    • \n
    • 22:10 又开始报错,23:10 分报错消失
    • \n
    \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分报错?

    \n

    ai 所使用的配置文件在 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);
    \n

    可以理解为,key 是主播的 ID,value 是要扶持的量,这段代码当有重复 key 时会报错,解决方法是传入第3个参数,告诉程序当 key 冲突时的 merge 逻辑,因为我这里不关心太具体保留哪个 value,可以简单实现:

    \n
    .collect(Collectors.toMap(Pair::getKey, Pair::getValue,(value1, value2) -> value2));
    \n

    后续优化

      \n
    • 避免7点后(非上线时间)更改线上配置
    • \n
    • 运营配置尽量做到 admin 和自动化
    • \n
    • 服务出故障后优先想想最近有哪些改动(即使只修改了配置文件)
    • \n
    • 配置刷新频率不宜过低
    • \n
    \n"},{"title":"配置中心的加密与解密功能","url":"/2017/config-server-encrypt-and-decrypt/","content":"

    有时候我们会放一些敏感信息到配置中心里,比如线上数据库密码等,我们直接将敏感信息以明文的方式存储于微服务应用的配置文件中是非常危险的,Spring Cloud Config 提供了对属性进行加密解密的功能,以保护配置文件中的信息安全。

    \n

    在 Spring Cloud Config 中通过在属性值前使用 {cipher} 前缀来标注该内容是一个加密值,当微服务客户端加载配置时,配置中心就会自动为带有 {cipher} 前缀的值进行解密。这里有个需要注意的地方,如果配置文件使用的是 yml 格式的话,一定要用引号将内容包裹起来,properites 的配置文件不需要。如:

    \n
    spring:
    datasource:
    password: '{cipher}c400dd5d44f112518bbf870894e4b8a60fbc64680073aa535d363c28f038bb77'
    \n

    Spring Cloud Config 同时支持对称加密和非对称加密,下边我们只介绍对称加密的使用方式,一般来说只要密钥不被泄露,对称加密的方式就足够了。

    \n

    首先我们访问配置中心的 /encrypt/status 路径,可以看到返回结果提示我们还没有设置密钥,需要我们在配置文件中进行设置。

    \n

    \"\"

    \n

    在配置中心项目的配置文件 application.yml 中加入以下配置即可(密钥根据根据需要自行修改):

    \n
    encrypt:
    key: my-encrypt-key
    \n

    然后重新编译后运行,再次访问 /encrypt/status 看到如下错误:

    \n

    \"\"

    \n

    这是因为在 JRE 中,自带的 JCE 默认是有长度限制的版本,我们需要从 Oracle 官网下载不限长度的版本:http://www.oracle.com/technetwork/java/javase/downloads/jce8-download-2133166.html ,下载解压后可以看到下边三个文件:

    \n

    \"\"

    \n

    我们需要将 local_policy.jarUS_export_policy.jar 两个文件复制到
    $JAVA_HOME/jre/lib/security
    目录下,在复制前,最好将之前的两个文件进行备份,我将这两个文件放到了 lc0 机器的相应目录中,现在目录中的文件如下:

    \n

    \"\"

    \n

    重新运行配置中心,访问 /encrypt/status 可以看到密钥已经生效了,并且配置中心已经支持对配置进行加密了。

    \n

    \"\"

    \n

    此时,我们配置中心的加密解密功能就已经可以使用了,可以访问 /encrypt/decrypt 来使用加密和解密功能。这两个端点都是 POST 请求,我们来用 curl 测试下:

    \n

    \"\"

    \n

    我们用 my-password 为明文生成了
    49db5b628a4b722ef776262d67c0fa9676de7767e54ecfb2be70df5157677d20
    这个密文,同时又测试了解密功能。

    \n

    下边我们在实际情景中运用一下加密解密功能

    在 GitLab 中修改 app-a 的 dev 配置文件,加上如下配置:

    \n
    password: '{cipher}49db5b628a4b722ef776262d67c0fa9676de7767e54ecfb2be70df5157677d20'
    \n

    然后在 app-a 中新加一个 Controller:

    \n
    @Value("${password}")
    private String password;

    @RequestMapping(value = "/password", method = RequestMethod.GET)
    public String password() {
    return password;
    }
    \n

    重新编译后运行,访问它的 /password 路径,可以看到结果:

    \n

    \"\"

    \n

    我们通过配置 encrypt.key 参数来指定密钥的实现方式采用了对称加密。这种方式实现起来比较简单,只需要配置一个参数即可。另外,我们也可以使用环境变量 ENCRYPT_KEY 来进行配置,让密钥信息外置。

    \n"},{"title":"连续写了30天流水账","url":"/2023/continue-write-30-days/","content":"

    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":"

    快速回答

    \n
      \n
    • Expires 为 Cookie 的删除设置一个过期的日期
    • \n
    • Max-age 设置一个 Cookie 将要过期的秒数
    • \n
    • IE 浏览器(ie6、ie7 和 ie8) 不支持 max-age,所有的浏览器都支持 expires
    • \n
    \n

    深入一些来说明

    \n

    expires 参数是当年网景公司推出 Cookies 原有的一部分。在 HTTP1.1 中,expires 被弃用并且被更加易用的 max-age 所替代。你只需说明这个 Cookie 能够存活多久就可以了,而不用像之前那样指定一个日期。设置二者中的一个,Cookie 会在它过期前一直保存,如果你一个都没有设置,这个 Cookie 将会一直存在直到你关闭浏览器,这种称之为 Session Cookie

    \n

    举个栗子

    \n

    expires 的方式设置 foo=bar 在5分钟后过期

    \n
    var d = new Date();
    d.setTime(d.getTime() + 5*60*1000); // in milliseconds
    document.cookie = 'foo=bar;path=/;expires='+d.toGMTString()+';';
    \n

    max-age 来做同样的事情

    \n
    document.cookie = 'foo=bar;path=/;max-age='+5*60+';';
    \n

    不幸的是,IE 浏览器 不支持 max-age,如果你想跨浏览器存放 Cookie,应该坚持用 expires

    \n

    下边我们来进行几个假设的问答

    \n

    问:如果我在 Cookie 中同时设置了 expiresmax-age 会发生什么?

    \n

    答:所有支持 max-age 的浏览器会忽略 expires 的值,只有 IE 另外,IE 会忽略 max-age 只支持 expires

    \n

    问:如果我只设了 max-age 会怎样?

    \n

    答:除了 IE 之外的所有浏览器会正确的使用它。在 IE 浏览器中,这个 Cookie 将会作为一个 Session Cookie(当你关闭浏览器时它会被删除)。

    \n

    问:如果我只设了 expires

    \n

    答:所有浏览器会正确使用它来保存 Cookie,只需要记得像上边示例那样设置它的 GMT 时间就行了。

    \n

    问:这篇文章的寓意是什么?

    \n

    答:如果你关心你的 Cookies 功能在大多数 Web 用户下正常工作,不要用正确的方式(max-age)存储你的 Cookies,应该用 expires 的方式让他们工作。

    \n"},{"title":"洞洞鞋","url":"/2023/crocs/","content":"

    最近进入了雨季,穿普通的鞋子上下班在路上不小心灌上水会很不舒服,一家之主打算给我从网上买一双叫 Crocs 牌子的洞洞鞋,但是上周六,突然预告北京会有大暴雨,现在从网上买已经不赶趟了,正好家附近挨着燕莎奥特莱斯,一家之主告诉我那里,那里有 Crocs 的门店。

    \n

    到了之后我看到价格后直接劝退,一双洞洞鞋竟然要399,这不是坑人吗。但是一家之主执意要让我买,告诉我这鞋特别舒适,而且能穿好多年。我对于穿衣装打扮没有什么追求,但是禁不住劝最后还是买了,买了一双白色的。

    \n

    \n

    这双鞋穿起来确实非常舒适,但是最吸引我的并不是它的舒适性和外观,而是这鞋居然可以搞 DIY,鞋子上的洞洞是一个个插槽,可以自由组装自己喜欢的饰品,这真的是又让我眼前一亮。

    \n

    洞洞鞋的样子千篇一律,但鞋上的每个洞洞都是标准尺寸,饰品厂只需要按照洞洞尺寸生产标准的饰品,就可以打造出一个生态。每个人都可以像玩乐高一样拼装出自己得意的作品,这卖点一下子就出来了。

    \n

    每个人都有追求个性的愿望,买再好的鞋子也可能会撞鞋,而且也无法突出我的个性。而买洞洞鞋,我可以根据自己的心情来做搭配,比如今天我往上边搭配一个爱心,明天装饰一个咖啡杯,后天装饰一个皮卡丘,这都可以代表我这一天的心情。

    \n

    看看我的搭配,猜猜我今天心情如何?

    \n

    \n

    有没有可能,只要某种东西做到既方便携带,又可以通过组合的方式来折腾,佩戴上后还能展现一个人的性格和个性,人们就愿意来购买,进而会形成一个成规模的市场?我能想到的另一个例子是手串儿。

    \n

    \n"},{"title":"大观园记","url":"/2022/da-guan-yuan/","content":"

    \"20220630100907.png\"

    \n
    \n

    这篇小记写于早上上班的地铁上,我之前都是正襟危坐在电脑旁用 Obsidian 写,你这次尝试在手机上用 Drafts 写,然后到公司后再用电脑做些调整、配上图片后发表。

    \n
    \n

    上上个周末去了一趟大观园,也算圆了我这几年的一个愿望,其实大观园离我住的地方并不远,开车也就 15 分钟,只是由于疫情再加上自己的行动力不足一直拖到现在。

    \n

    行动力不足的一个原因是大观园 40 每人的票价比其他公园高出很多。可是虽然贵,但它又包含在了公园通票中,所以我去年的时候就想今年办个通票,到时候去个痛快,然后一晃半年多就过去了。这次让我行动的一个触发点是前几天高考的作文题目中出现了大观园,又唤起了我去大观园的念头,刚好疫情也没那么紧张,索性就来了。

    \n

    \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

    可串行化(Serializable)

    可串行化使用了最全的锁:写锁、读锁、范围锁。

    \n

    读写锁平时比较常见,这里简单介绍下范围锁。

    \n

    范围锁的定义为:对于某个范围直接加排他锁,在这个范围内的数据不能被写入。

    \n

    要注意的时这里的访问内的数据不止包括已有的数据,即使不存在的数据也会被加锁,可以理解为不允许在这个范围内新增数据。

    \n

    我举个例子,比如我现在有这样一些数据:

    \n
    id\tprice
    1\t10
    2\t30
    3\t70
    4\t90
    5\t120
    \n

    当我们在一个事务中使用范围查询 price<100 时,在这个事务还未结束的情况下,其他事务无法在新增一个 price 为 20 的数据。

    \n

    可串行化保障了最好的隔离级别,但也是这几种隔离级别中性能最差的。

    \n

    可重复读(Repeatable Read)

    可重复读只使用了读锁和写锁,未使用范围锁。

    \n

    还用上边的数据举例,这种情况下会产生的一个问题是:当一个事务在第一次查询 price<100 时返回了 4 条数据,这时候另一个事务新增了一条 price 为 20 的数据,当第一个事务再次查询 price<100 的数据时发现变成了 5 条,也就是说出现了幻读

    \n

    幻读:在事务执行过程中,两个完全相同的范围查询得到了不同的结果集。

    \n

    读已提交(Read Committed)

    读已提交表面上看和可重复度使用的锁相同,都使用了读锁和写锁,但在读锁的加锁粒度上和之前有所区别

    \n

    在上边的可重复读中,读锁是一直锁到事务结束,但在读已提交中,读锁在查询完成后会立即释放,下边我写两个 Go 程序来演示下这两种情况的区别。

    \n

    可重复读程序(读锁锁到事务结束)

    package main

    import (
    \t\"sync\"
    \t\"time\"
    )

    func main() {
    \tmutex := new(sync.RWMutex)

    \ti := 1

    \t// 事务1
    \tgo func() {
    \t\t// 使用 defer 确保事务结束后再释放锁
    \t\tdefer mutex.RUnlock()
    \t\tmutex.RLock()
    \t\t// 先查询1次
    \t\tprintln(i)

    \t\t// 2秒后再查询一次
    \t\ttime.Sleep(2 * time.Second)
    \t\tprintln(i)
    \t}()

    \t// 事务2
    \tgo func() {
    \t\tdefer mutex.Unlock()

    \t\t// 在1秒后进行数据更新
    \t\ttime.Sleep(1 * time.Second)
    \t\tmutex.Lock()
    \t\ti += 1
    \t}()

    \ttime.Sleep(3 * time.Second)
    }
    \n

    输出:

    \n
    1
    1
    \n

    读已提交程序(读锁锁到查询完成)

    package main

    import (
    \t\"sync\"
    \t\"time\"
    )

    func main() {
    \tmutex := new(sync.RWMutex)

    \ti := 1

    \t// 事务1
    \tgo func() {
    \t\t// 读锁加锁
    \t\tmutex.RLock()
    \t\tprintln(i)
    \t\t// 读锁释放
    \t\tmutex.RUnlock()

    \t\ttime.Sleep(2 * time.Second)

    \t\t// 读锁加锁
    \t\tmutex.RLock()
    \t\tprintln(i)
    \t\t// 读锁释放
    \t\tmutex.RUnlock()
    \t}()

    \t// 事务2
    \tgo func() {
    \t\tdefer mutex.Unlock()
    \t\ttime.Sleep(1 * time.Second)
    \t\tmutex.Lock()
    \t\ti += 1
    \t}()

    \ttime.Sleep(3 * time.Second)
    }
    \n

    输出:

    \n
    1
    2
    \n

    这个程序和上边的程序区别在于读锁是锁了整个事务还是只锁了查询的瞬间,在读已提交的情况下,第一个事务读取数据并打印出 1 后就释放了读锁,这时候另一个事务可以拿到写锁并将数据修改为 2,之后第一个事务再次读取时就读到了另一个事务修改后的数据。

    \n

    这种情况我们称之为不可重复读问题

    \n

    不可重复读问题:在事务执行过程中,对同一行数据的两次查询得到了不同的结果。

    \n

    读未提交(Read Uncommitted)

    读未提交只使用了写锁,同样我们也通过一个 Go 程序观察下这个情况。

    \n
    package main

    import (
    \t\"sync\"
    \t\"time\"
    )

    func main() {
    \tmutex := new(sync.Mutex) // 这里将读写锁换成普通的互斥锁

    \ti := 1

    \tgo func() {
    \t\ttime.Sleep(1 * time.Second)
    \t\tprintln(i) // 可以读到另一个事务第一次修改后的数据
    \t\ttime.Sleep(3 * time.Second)
    \t\tprintln(i) // 可以读到另一个事务第二次修改后的数据
    \t}()

    \tgo func() {
    \t\tdefer mutex.Unlock()
    \t\tmutex.Lock()
    \t\ti += 1 // 在事务中修改了数据
    \t\ttime.Sleep(2 * time.Second)
    \t\ti += 1 // 在事务中再次修改了数据
    \t}()

    \ttime.Sleep(5 * time.Second)
    }
    \n

    输出:

    \n
    2
    3
    \n

    这里演示的是,一个事务对数据进行修改,另一个事务只是读取数据,由于在读未提交下不存在读锁,可以直接读数据。

    \n
      \n
    • 数据初始值为 1,写事务将值修改为 2(但并未释放写锁)
    • \n
    • 读事务在 1 秒后读到了 2
    • \n
    • 写事务在 2 秒后又将数据修改为 3(由于后边的 sleep,也并没有立即释放写锁)
    • \n
    • 读事务在 3 秒后又读到了 3
    • \n
    \n

    可以看到,我们的只读事务读到了写事务还没有提交的数据,我们称之为脏读

    \n

    脏读:在事务执行过程中,一个事务读取到了另一个事务未提交的数据。

    \n

    总结一下

    \n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\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":"/2020/database-isolation-levels/","content":"
    \"\"
    \n\n

    数据库行业有四种常见的隔离级别,分别是 RU、RC、RR、SERIALIZABLE,其中用到最多的是 RR 和 RR。下边分别看一下这四种隔离级别的异同。

    \n

    RU(READ-UNCOMMITTED) - 能读到未提交的数据

    RU 级别,实际上就是完全不隔离。每个进行中事务的中间状态,对其他事务都是可见的,所以有可能会出现「脏读」。

    \n

    RU 举例

    \"\"
    \n\n

    用户1设置 x=3,在用户1的事务未提交之前,用户2 执行 get x 时却看到了 x=3

    \n

    RC(READ-COMMITEED) - 能读到已提交的数据

    RC 举例

    \"\"
    \n\n\n

    用户1设置 x=3,在用户1的事务未提交之前,用户2 执行 get x 操作依旧返回的时旧值 2

    \n

    RR(REPEATABLE-READ) - 可重复读

    RC 和 RR 唯一的区别在于“是否可重复读”:在一个事务执行过程中,它能不能读到其他已提交事务对数据的更新,如果能读到数据变化,就是“不可重复读”,否则就是“可重复读”。

    \n

    RR 举例

    继续上边的例子,如果用户2 读取 x 是在同一个事务内,那么永远读到的都是事务开始前x的值。也就是说每个事务都从数据库的一致性快照中读取数据。

    \n
    \"\"
    \n\n

    在 RR 隔离级别下,在一个事务进行过程中,对于同一条数据,每次读到的结果总是相同的,无论其他会话是否已经更新了这条数据,这就是「可重复读」。

    \n

    不可重复读导致的问题

    \"\"
    \n\n

    假设用户在银行有 1000 块钱,分别存放在两个账户上,每个账户 500。现在有这样一笔转账交易从账户1转 100 到账户2。如果用户在他提交转账请求之后而银行系统执行转账的过程中间,来查看两个账户的余额,他有可能看到帐号1收到转账前的余额(500元),和帐号2完成钱款转出后的余额(400元)。对于用户来说,貌似他的账户总共只有 900 元,有 100 元消失了。

    \n

    这种异常现象称为不可重复读(nonrepeatable read)或读倾斜(read skew)

    \n

    SERIALIZABLE - 串行化

    串行化隔离通常被认为是最强的隔离级别。它保证即使事物可能会并行执行,但最终的结果与每次一个即串行执行结果相同。不过由于这种隔离级别性能较差,所以在实际开发中很少被用到,以下是三种实现串行化的技术方案:

    \n
      \n
    • 严格按照串行顺序执行
    • \n
    • 两阶段锁定
    • \n
    • 乐观并发控制技术
    • \n
    \n

    隔离级别的要点:

    脏读

    客户端读到了其他客户端未提交的写入。

    \n

    脏写

    客户端覆盖了另一个客户端尚未提交的写入。

    \n

    读倾斜(不可重复读)

    客户端在不同时间点看到了不同值。

    \n

    更新丢失

    两个客户端同时执行读-修改-写入操作序列,出现了其中一个覆盖了另一个的写入,但又没有包含对方最新值的情况,最终导致了部分修改发生了丢失。

    \n

    写倾斜

    事务首先查询数据,根据返回的结果而作出某些决定,然后修改数据库。当事务提交时,支持决定的前提条件已不再成立。

    \n

    幻读

    事务读取了某些符合查询条件的对象,同时另一个客户端执行写入,改变了先前的查询结果。

    \n

    幻读这个概念有些抽象,举例说明一下:

    \n
    \"\"
    \n\n
      \n
    • 用户1在一个会话中开启一个事务,准备插入一条 ID 为 1000 的流水记录。查询一下当前流水,不存在 ID 为 1000 的记录,可以安全地插入数据。
    • \n
    • 这时候,另外一个会话抢先插入了这条 ID 为 1000 的流水记录。
    • \n
    • 然后用户1再执行相同的插入语句时,就会报主键冲突错误,但是由于事务的隔离性,它执行查询的时候,却查不到这条 ID 为 1000 的流水,就像出现了“幻觉”一样,这就是幻读。
    • \n
    \n

    在实际业务中,很少能遇到幻读,即使遇到,也基本不会影响到数据准确性。

    \n

    最后用一张表格总结一下上边的内容:

    \"\"
    \n\n
      \n
    • RU 级别隔即没有任何隔离,存在脏读、不可重复读、幻读的风险
    • \n
    • RC 可以避免脏读,还是会存在不可重复读和幻读
    • \n
    • NR 可以避免脏读和不可重复读(通常通过一致性快照),但无法避免幻读
    • \n
    • 只有 SERIALIZABLE 才可以避免幻读
    • \n
    \n"},{"title":"DDD “黑话”指南","url":"/2022/ddd-cant/","content":"

    背景

    开始前声明,我不相信技术上存在任何银弹,包括微服务、DDD,不要指望用一套方法论或者架构能解决所有问题,能够根据当时的情况(资源、人才、业务)权衡出最符合当时场景的架构,才是一个合格的架构师的价值所在。

    \n

    在讨论 DDD 时经常会一起讨论的是两种模型,传统贫血模型和 DDD 所推崇的充血模型。对于业务不复杂的系统开发来说,基于贫血模型的传统开发模式简单够用,基于充血模型的 DDD 开发模式有点大材小用,无法发挥作用。比如数据统计和分析,DDD 很多方法可能都用不上,或用得并不顺手,而传统的方法很容易就解决了。

    \n
    \n

    我在大概 19 年左右看过一些 DDD 相关的内容,也在上家公司团队内推行过 DDD 的调研,但限于上家公司对技术升级、培养没有那么看重,并且推行 DDD 对人员的技术要求也比较高,只做了一次内部分享就没再有下文了。

    \n

    最近来到新的部门后,这边在推行使用 DDD 来梳理我们的业务架构,从更高的视角审视我们目前的技术架构,用于评估架构是否合理、服务是否应当做一些拆分或者将应该归在一起的业务进行合并。我觉得这个做法是合理并且正确的,DDD 更推崇的是设计思想,可以用这个思想来指导我们做业务建模和服务设计。我们没必要为了 DDD 而 DDD,更不能脱离领域模型来空谈微服务设计。

    \n

    由于时间久远,当时看过的内容已经忘的七七八八了,翻开之前看文章时做的一些记录,将 DDD 中常用的 “黑话” 回顾一下,记录在下文,尽量和其他人的认知对齐,在交流时能更通畅一些。

    \n

    八股图镇楼

    \"20220624105137.png\"

    \n

    DDD 与微服务的关系:

      \n
    • DDD 是一种架构设计方法,微服务是一种架构风格,两者从本质上都是为了追求高响应力,而从业务视角去分离应用系统建设复杂度的手段。
    • \n
    • 两者都强调从业务出发,其核心要义是强调根据业务发展,合理划分领域边界,持续调整现有架构,优化现有代码,以保持架构和代码的生命力,也就是我们常说的演进式架构
    • \n
    • DDD 主要关注:从业务领域视角划分领域边界,构建通用语言进行高效沟通,通过业务抽象,建立领域模型,维持业务和代码的逻辑一致性。
    • \n
    • 微服务主要关注:运行时的进程间通信、容错和故障隔离,实现去中心化数据管理和去中心化服务治理,关注微服务的独立开发、测试、构建和部署。
    • \n
    \n

    领域

    DDD 的领域就是这个边界内要解决的业务问题域

    \n

    \"20220624144225.png\"

    \n

    子域

    我们把划分出来的多个子领域称为子域,每个子域对应一个更小的问题域或更小的业务范围。

    \n

    子域可以根据自身重要性功能属性划分为三类子域:

    \n
      \n
    • 核心域:决定了产品和公司核心竞争力,它是业务成功的主要因素和公司的核心竞争力。
    • \n
    • 通用域:没有太多个性化的诉求,同时被多个子域使用的通用功能子域,如认证、权限。
    • \n
    • 支持域:不包含决定产品和公司核心竞争力的功能,也不包含通用功能的子域,如数据代码类的数据字典系统
    • \n
    \n

    限界上下文

    用来封装通用语言和领域对象,提供上下文环境,保证在领域之内的一些术语、业务相关对象等(通用语言)有一个确切的含义,没有二义性。

    \n

    可以将限界上下文拆解为两个词:限界和上下文

    \n
      \n
    • 限界就是领域的边界
    • \n
    • 而上下文则是语义环境
    • \n
    \n

    理论上限界上下文就是微服务的边界。

    \n

    聚合

    聚合就是由业务和逻辑紧密关联的实体和值对象组合而成的,聚合是数据修改和持久化的基本单元,每一个聚合对应一个仓储,实现数据的持久化。

    \n

    聚合的特点:

    高内聚、低耦合,它是领域模型中最底层的边界,可以作为拆分微服务的最小单位。

    \n

    一个微服务可以包含多个聚合,聚合之间的边界是微服务内天然的逻辑边界。

    \n

    聚合的一个设计原则:在边界之外使用最终一致性。一次事务最多只能更改一个聚合的状态。如果一次业务操作涉及多个聚合状态的更改,应采用领域事件的最终一致性。

    \n

    聚合根

      \n
    • 根据业务单一职责和高内聚原则,定义了聚合内部应该包含哪些实体和值对象
    • \n
    • 主要目的是为了避免由于复杂数据模型缺少统一的业务规则控制,而导致聚合、实体之间数据不一致性的问题。
    • \n
    • 如果把聚合比作组织,那聚合根就是这个组织的负责人。聚合根也称为根实体,它不仅是实体,还是聚合的管理者。
    • \n
    \n

    聚合根的特点:

    聚合根是实体,有实体的特点,具有全局唯一标识,有独立的生命周期。

    \n
      \n
    • 一个聚合只有一个聚合根,聚合根在聚合内对实体和值对象采用直接对象引用的方式进行组织和协调。
    • \n
    • 聚合根与聚合根之间通过 ID 关联的方式实现聚合之间的协同。
    • \n
    \n

    领域服务

    如果一个业务动作或行为跨多个实体,我们就需要设计领域服务。

    \n
      \n
    • 领域服务通过对多个实体和实体方法进行组合,完成核心业务逻辑
    • \n
    • 领域服务是位于实体方法之上应用服务之下的一层业务逻辑。
    • \n
    \n

    在微服务内部,实体的方法被领域服务组合和封装,领域服务又被应用服务组合和封装。

    \n

    仓储

    每一个聚合都有一个仓储,仓储主要用来完成数据查询和持久化操作。

    \n

    领域事件

    领域事件是领域模型中非常重要的一部分,用来表示领域中发生的事件。一个领域事件将导致进一步的业务操作,在实现业务解耦的同时,还有助于形成完整的业务闭环。

    \n

    如果发生某种事件后,会触发进一步的操作,那么这个事件很可能就是领域事件

    \n

    领域事件驱动设计可以切断领域模型之间的强依赖关系,事件发布完成后,发布方不必关心后续订阅方事件处理是否成功,这样可以实现领域模型的解耦,维护领域模型的独立性和数据的一致性。

    \n

    在领域模型映射到微服务系统架构时,领域事件可以解耦微服务,微服务之间的数据不必要求强一致性,而是基于事件的最终一致性

    \n

    通过领域事件驱动的异步化机制,可以推动业务流程和数据在各个不同微服务之间的流转,实现微服务的解耦,减轻微服务之间服务调用的压力,提升用户体验。

    \n

    实体

    在 DDD 中有这样一类对象,它们拥有唯一标识符,且标识符在历经各种状态变更后仍能保持一致。对这些对象而言,重要的不是其属性,而是其延续性和标识,对象的延续性和标识会跨越甚至超出软件的生命周期。我们把这样的对象称为实体。

    \n

    在代码模型中,实体的表现形式是实体类,这个类包含了实体的属性和方法,通过这些方法实现实体自身的业务逻辑。

    \n

    聚合根是一种特殊的实体,它有自己的属性和方法。聚合根可以实现聚合之间的对象引用,还可以引用聚合内的所有实体。

    \n

    实体的特点:有 ID 标识,通过 ID 判断相等性,ID 在聚合内唯一即可。

      \n
    • 状态可变,它依附于聚合根,其生命周期由聚合根管理。
    • \n
    • 实体一般会持久化,但与数据库持久化对象不一定是一对一的关系。
    • \n
    • 实体可以引用聚合内的聚合根、实体和值对象。
    • \n
    \n

    值对象

    通过对象属性值来识别的对象,它将多个相关属性组合为一个概念整体。在 DDD 中用来描述领域的特定方面,并且是一个没有标识符的对象,叫作值对象。

    \n

    值对象创建后就不允许修改了,只能用另外一个值对象来整体替换。

    \n

    值对象的特点:无 ID,不可变,无生命周期,用完即扔。

      \n
    • 值对象之间通过属性值判断相等性。
    • \n
    • 它的核心本质是值,是一组概念完整的属性组成的集合,用于描述实体的状态和特征。
    • \n
    • 值对象尽量只引用值对象。
    • \n
    \n

    实体 vs 值对象

    实体和值对象是组成领域模型的基础单元。

    \n
      \n
    • 实体一般对应业务对象,它具有业务属性和业务行为
    • \n
    • 值对象主要是属性集合,对实体的状态和特征进行描述
    • \n
    \n

    实体是看得到、摸得着的实实在在的业务对象,实体具有业务属性、业务行为和业务逻辑。而值对象只是若干个属性的集合,只有数据初始化操作和有限的不涉及修改数据的行为,基本不包含业务逻辑。值对象的属性集虽然在物理上独立出来了,但在逻辑上它仍然是实体属性的一部分,用于描述实体的特征。

    \n

    聚合与实体、值对象的关系

    领域模型内的实体和值对象就好比个体,而能让实体和值对象协同工作的组织就是聚合,它用来确保这些领域对象在实现共同的业务逻辑时,能保证数据的一致性。

    \n

    聚合就是由业务和逻辑紧密关联的实体和值对象组合而成的,聚合是数据修改和持久化的基本单元,每一个聚合对应一个仓储,实现数据的持久化。

    \n

    聚合之间通过聚合根 ID 关联引用,如果需要访问其它聚合的实体,就要先访问聚合根,再导航到聚合内部实体,外部对象不能直接访问聚合内实体。

    \n

    领域事件基本属性至少包括:

      \n
    • 事件唯一标识
    • \n
    • 发生时间
    • \n
    • 事件类型
    • \n
    • 事件
    • \n
    \n

    数据一致性

    聚合内数据强一致性,而聚合之间数据最终一致性。

    \n

    在一次事务中,最多只能更改一个聚合的状态。如果一次业务操作涉及多个聚合状态的更改,应采用领域事件的方式异步修改相关的聚合,实现聚合之间的解耦。

    \n

    产品愿景

    产品愿景是对产品顶层价值设计,对产品目标用户、核心价值、差异化竞争点等信息达成一致,避免产品偏离方向。

    \n

    场景分析

    场景分析是从用户视角出发,探索业务领域中的典型场景,产出领域中需要支撑的场景分类、用例操作以及不同子域之间的依赖关系,用以支撑领域建模。

    \n

    领域建模

    领域建模是通过对业务和问题域进行分析,建立领域模型。

    \n
      \n
    • 向上通过限界上下文指导微服务边界设计
    • \n
    • 向下通过聚合指导实体对象设计
    • \n
    \n

    DDD 战略设计和战术设计

    战略设计主要从业务视角出发,建立业务领域模型,划分领域边界,建立通用语言的限界上下文,限界上下文可以作为微服务设计的参考边界。

    \n

    战术设计则从技术视角出发,侧重于领域模型的技术实现,完成软件开发和落地,包括:聚合根、实体、值对象、领域服务、应用服务和资源库等代码逻辑的设计和实现。

    \n

    服务的封装和调用方式

    1. 应用服务的组合和编排

    应用服务会对多个领域服务进行组合和编排,暴露给用户接口层,供前端应用调用。

    \n

    2. 领域服务的组合封装

    领域服务会对多个实体和实体方法进行组合和编排,供应用服务调用。

    \n

    3. 实体方法的封装

    实体方法是最底层的原子业务逻辑。

    \n

    \"20220624142308.png\"

    \n

    DDD 的设计过程:

      \n
    1. 在事件风暴中,我们会梳理出业务过程中的用户操作、事件以及外部依赖关系等,根据这些要素梳理出实体等领域对象
    2. \n
    3. 根据实体对象之间的业务关联性,将业务紧密相关的多个实体进行组合形成聚合,聚合之间是第一层边界。
    4. \n
    5. 根据业务及语义边界等因素将一个或者多个聚合划定在一个限界上下文内,形成领域模型,限界上下文之间的边界是第二层边界。
    6. \n
    \n

    领域对象设计过程

      \n
    1. 设计实体
    2. \n
    3. 找出聚合根
    4. \n
    5. 设计值对象
    6. \n
    7. 设计领域事件
    8. \n
    9. 设计领域服务
    10. \n
    11. 设计仓储
    12. \n
    \n

    DDD 分层架构从上到下依次是

    用户接口层

    用户接口层负责向用户显示信息和解释用户指令。这里的用户可能是:用户、程序、自动化测试和批处理脚本等等。

    \n

    应用层

    应用层是很薄的一层,理论上不应该有业务规则或逻辑,主要面向用例和流程相关的操作。

    \n

    领域层

    领域层的作用是实现企业核心业务逻辑,通过各种校验手段保证业务的正确性。

    \n

    领域层包含聚合根、实体、值对象、领域服务等领域模型中的领域对象。

    \n

    基础层

      \n
    • 基础层是贯穿所有层的,它的作用就是为其它各层提供通用的技术和基础服务,包括第三方工具、驱动、消息中间件、网关、文件、缓存以及数据库等。比较常见的功能还是提供数据库持久化。
    • \n
    \n

    \"20220624145253.png\"

    \n

    参考

      \n
    • DDD 实战
    • \n
    • 设计模式之美
    • \n
    \n"},{"title":"deadline 是第一生产力","url":"/2022/deadline-is-the-first-power/","content":"

    我在今年六月份转岗后被邀请加入了公司的架构组,这个组织的意图是提升公司后端整体技术能力。我们规划了每周一次公司内技术分享,一般在周五进行。

    \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 做些微调,然后再找个会议室做做练习。

    \n

    Keynote 可以将文稿转成 PDF、HTML 等格式,我尝试转成 HTML 后发现就是一个标准的前端项目,有一个 index.html 作为入口,这不就可以放到 Cloudflare 上做一个静态站了吗,于是我把这些文件上传到了 Cloudflare,并关联了我的域名:https://codestyle.jiapan.me/,这样就得到了一个可以在线浏览的版本,看了下效果分辨率比原始文档偏低,但又不是不能用🤷🏻‍♂️。

    \n

    最后再感叹一声,Deadline 是第一生产力!

    \n"},{"title":"常见部署策略介绍","url":"/2020/deployment-strategy-introduce/","content":"
    \"\"
    \n\n

    在持续交付和大规模互联网应用流行前,企业一般采用手动的方式进行部署,通常选择活动用户最少的时间(如周末或者凌晨),并且告诉大家需要一段时间来维护系统。在这段时间内,运维团队会停止旧的版本,部署新的版本并检查一切是否恢复正常。

    \n

    但是现在,由于微服务的出现,我们会不断地将各服务的新版本部署到生产环境中,不能简单的认为部署就意味着停机,因为这样系统会一直有不同部分处于停机状态,我们需要考虑新的部署策略。

    \n

    本文将介绍 6 种常见的部署策略:单目标部署、一次性全部部署、最小服务部署、滚动部署、蓝/绿部署、金丝雀部署,每种策略背后的理念都如其名。

    \n

    术语

    为了更好地介绍和对比这些策略,我们先来说明一组术语:

    \n

    期望的实例数量

    这是当服务功能完全正常时,预计将运行的服务副本数量。

    \n

    期望的实例数量简写为:desired

    \n

    如:desired=3,表示在任何一次部署中,需要将 3 个旧版本的服务实例更新为 3 个新版本的服务实例。

    \n

    最小的健康实例数量

    当删除旧实例、启动新实例的过程中,我们希望至少有一些实例是处于健康状态(无论是旧实例还是新实例),这样可以保证系统能够最低限度地提供服务。

    \n

    最小的健康实例数量简写为:minimum

    \n

    最大的实例数量

    有时我们希望在删除旧实例前,先启动一些新版本的服务实例,以便减少服务部署过程中的停机时间,这意味着我们需要更多的资源。通过限制最大实例数量,我们同时也限制了部署过程中最大资源使用量。

    \n

    最大的实例数量简写为:maximum

    \n

    图表元素说明

    \n

    对于每个策略,我们通过一张图来表示部署过程中的事件连续性。

    \n

    表示旧版本的服务实例

    \n
    \"\"
    \n\n

    表示正在启动、尚不可用的新版本实例

    \n
    \"\"
    \n\n

    表示新版本的实例

    \n
    \"\"
    \n\n\n

    单目标部署

    这是最简单的策略,也是需要资源最少的策略。在这种策略下,我们可以假设服务只有一个正在运行的实例,无论何时都必须先停止它,然后再部署新的实例。这意味着服务会存在中断,但是不需要额外的资源

    \n

    单目标部署策略配置参数为:

    \n
      \n
    • desired: 1
    • \n
    • minimum: 0%
    • \n
    • maximum: 0%
    • \n
    \n

    下图展示了单目标部署策略的实施步骤:

    \n
    \"\"
    \n\n
      \n
    • 开始:存在一个旧版本的实例。
    • \n
    • 步骤1:该实例被新版本实例替换,在新实例完全启动前,服务实际上不可用。
    • \n
    • 结束:新实例启动完成,可以开始接收外部请求。
    • \n
    \n

    一次性全部部署

    改策略类似于单目标部署策略,唯一的区别是我们可以拥有任意固定数量的实例,而不是只有一个实例。和单目标部署的情况一样,一次性全部部署策略升级期间不需要额外的资源,但是也存在服务中断

    \n

    一次性全部部署策略配置参数为:

    \n
      \n
    • desired: 5
    • \n
    • minimum: 0%
    • \n
    • maximum: 0%
    • \n
    \n
    \"\"
    \n\n
      \n
    • 开始:存在 5 个旧版本的实例。
    • \n
    • 步骤1:同时停止所有 5 个实例,替换为 5 个新版本的实例,在新版本启动之前,该服务实际上不可用。
    • \n
    • 结束:5个新实例启动完成,可以开始接收外部请求。
    • \n
    \n

    最小服务部署

    前边两个策略的问题在于,它们都会中断服务。我们可以调整策略来改善这一点。

    \n

    最小服务部署表示确保始终存在着一部分健康的服务实例,我们可以先将一部分实例进行更新,等它们完成启动后再去更新另一部分旧实例。可以不断重复这个过程,直到所有的旧实例都被新的实例所替换。

    \n

    这种方式可以在不需要额外资源的情况下避免服务中断,但风险是这些存活的实例需要能够承受住额外的流量。

    \n

    最小服务部署策略配置参数为:

    \n
      \n
    • desired: 5
    • \n
    • minimum: 40%(也可以用绝对值:2)
    • \n
    • maximum: 0%
    • \n
    \n
    \"\"
    \n\n
      \n
    • 开始:存在 5 个旧版本的实例。
    • \n
    • 步骤1:由于我们要求 minimum 最小值为 40%,也就是至少要保留 2 个实例一直提供服务,所以只能先停止 3 个实例并将它们升级成新版本。
    • \n
    • 步骤2:新实例完全启动后,可以停止之前的 2 个旧实例,并将它们升级为新的版本。
    • \n
    • 结束:所有新实例都处于正常运行状态。
    • \n
    \n

    滚动部署

    我们可以将滚动部署看作最小服务部署的另一种形式,不过它的重点不在于健康实例的最小数量,而在于停止实例的最大数量。

    \n

    滚动部署最典型的情况是将停止实例的最大数量设置为 1,也就是任意时刻只有 1 个实例处于更新过程中。

    \n

    与最小服务部署相比,滚动部署的最主要有点在于,通过限制同时停止实例的数量,我们可以控制需要保留多少实例来承接额外的负载,它的缺点是部署需要更长的时间

    \n

    最小服务部署策略配置参数为:

    \n
      \n
    • desired: 5
    • \n
    • minimum: 80%(也可以用绝对值:4)
    • \n
    • maximum: 0%
    • \n
    \n
    \"\"
    \n\n
      \n
    • 开始:存在 5 个旧版本实例。
    • \n
    • 步骤1:停止其中一个实例并将它替换为新的实例。
    • \n
    • 步骤2:当步骤1中启动的实例完成启动后,停止另一个旧实例,将其也替换为新的实例。
    • \n
    • 步骤3、4、5:对其余实例重复相同的过程。
    • \n
    • 结束:所有的新实例现在都可以正常运行。
    • \n
    \n

    注:因为滚动部署相当于最小服务部署的另一种形式,所以 minimum = desired - 1

    \n

    蓝/绿部署

    蓝/绿部署是微服务领域种最受欢迎的一种部署策略,之前介绍过的最小服务和滚动部署存在两个缺点:1)升级期间承担总负载的健康实例数量会减少。2)部署期间,生产环境种会混合新旧两个版本的应用程序。

    \n

    蓝/绿部署无法简单地通过组合 desired、minimum、maximum 这几个参数来完成,它要求只有当所有的新实例都准备好的时候,用户才能访问新版本的服务,同时所有的旧实例立即变为不可用。为了实现这一目标,我们需要控制请求路由服务编排

    \n
    \"\"
    \n\n
      \n
    • 开始:存在多个旧版本的实例,请求通过负载均衡器/路由器被发送到旧版本的服务。
    • \n
    • 步骤1:创建多个新实例,这些实例不可访问,负载均衡器/路由器仍将所有请求发送到旧的实例。
    • \n
    • 步骤2:新实例启动完成可以处理请求了,但是尚未向他们发送任何请求。
    • \n
    • 步骤3:重新配置负载均衡器/路由器,将所有接收的请求转发到新版本的服务。这个过程几乎是瞬间完成的,此时出了正在处理的请求外,没有新请求再被发送到旧版本的服务。
    • \n
    • 结束:旧实例将已有请求处理完不再有用后,将他们停止。
    • \n
    \n

    蓝/绿部署提供了最佳的用户体验,但是代价是增加了复杂性,以及占用了更多资源

    \n

    金丝雀部署

    金丝雀部署是另一种无法通过组合 desired、minimum、maximum 参数实现的策略。这种策略允许我们尝试新版本的服务,但不完全承诺切换到新版本。这样我们只需在原来的旧版本的实例中添加一个新版本的实例,而不必停止其中的旧版本。负载均衡器会将一部分请求转发到金丝雀实例上,我们可以通过检查日志、指标来了解新实例的运行情况。

    \n

    金丝雀部署可以分为两个步骤执行:

    \n
    \"\"
    \n\n
      \n
    • 开始:存在多个旧版本的实例。
    • \n
    • 步骤1:创建一个新版本的实例,不删除任何旧版本的实例。
    • \n
    • 结束:新的实例启动完成并正常运行,可以与旧的实例一起提供服务。
    • \n
    \n

    金丝雀实例有时需要较长的时间,才能充分观察到它在新环境下的运行状况,在这个过程中,我们可能会部署其他的新版本,这时需要确保只重新部署金丝雀意外的实例。

    \n

    真正用到金丝雀的实例情况非常少,如果我们只想向一部分用户开放新功能,可以使用功能开关来实现。金丝雀部署的另一个好处是可以测试一些底层的配置变化,例如日志、指标框架、垃圾回收或者新版 jvm 等。

    \n

    不同策略的特点及代价总结

    \"\"
    \n\n\n"},{"title":"如何用事件溯源模式设计一个系统","url":"/2022/design-system-using-event-sourcing/","content":"

    事件溯源模式用于设计一个具有确定性的系统,这改变了普通系统设计的理念。我们通过一个电商系统来演示一下普通的 CRUD 和事件溯源模式的区别。

    \n

    事件溯源模式不是在数据库中记录订单状态,而是将导致状态变化的事件保存在事件存储中,事件存储是一个仅附加的日志,类似于数据库中的 undo log。

    \n

    事件必须有递增的主键 ID,以保证其顺序。订单状态通过回放事件来构建,并在订单视图(OrderView)中维护。如果订单视图发生故障,我们总是可以依赖事件存储进行修正,它是恢复订单状态的真实来源。

    \n

    让我们来看看详细的步骤。

    \n

    非事件溯源模式

    \"20220722094137.png\"

    \n
      \n
    • 步骤 1 和 2:Bob 想买一个产品,订单被创建并插入到数据库中。
    • \n
    • 步骤 3 和 4:Bob 想把数量从 5 改为 6。该订单被修改为新的状态。
    • \n
    • 步骤 5 和 6:Bob 为该订单支付了 6 元,订单完成,状态改为已支付(PAID)。
    • \n
    • 步骤 7 和 8:Bob 查询最新的订单状态,查询服务从数据库中检索状态。
    • \n
    \n

    事件溯源

    \"20220722094154.png\"

    \n
      \n
    • 步骤 1 和 2:Bob 想买一个产品:一个 NewOrderEvent 被创建,按序存储在事件仓库中,此时 eventID=2001。
    • \n
    • 步骤 3 和 4:Bob 想把商品数量从 5 改为 6:一个 ModifyOrderEvent 被创建,并以 eventID=2002 的形式按序保存在事件仓库中。
    • \n
    • 步骤 5 和 6:Bob 为这个订单支付了 60 元:一个 OrderPaymentEvent 被创建,并以 eventID=2003 的形式存储在事件仓库中。注意不同的事件类型有不同的事件属性。
    • \n
    • 第 7 步:订单视图(OrderView)监听从事件仓库中发布的事件,并建立订单的最新状态。虽然订单视图收到了 3 个事件,但是它一个一个按序应用这些事件,并保持订单最新的状态。
    • \n
    • 第 8 步:Bob 从订单服务(OrderService)查询订单状态,订单服务可以通过订单视图(OrderView)来获取订单状态。
        \n
      • 订单视图可以在内存或缓存中,不需要被持久化,因为它可以从事件存储中恢复。
      • \n
      • 订单视图表也可以持久化到其他数据库引擎中,如 ElasticSearch 来支持订单搜索,这就用到了另一种模式:CQRS。
      • \n
      \n
    • \n
    \n"},{"title":"聊聊软件架构和软件设计","url":"/2019/difference-between-architecture-and-design/","content":"

    \"\"

    \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

    需要记住的是,事件生产者并不知道有哪些事件消费者在监听哪些事件。

    \n

    微服务架构

    微服务架构已成为近几年最受欢迎的架构。它依赖于开发小而独立的模块化服务,其中每个服务都可以解决特定的问题或执行独特的任务,这些模块通过定义明确的 API 互相通信来实现业务目标。对于微服务架构无需介绍太多,来看看下边这张图:

    \n

    \"\"

    \n

    软件设计

    软件架构负责软件框架和基础设施的选型,软件设计负责代码级别的设计,例如每个模块的作用、类的范围和函数用途等。

    \n

    作为一名开发人员,了解 SOLID 原则和如何通过设计模式来解决常规问题是非常重要的。

    \n

    SOLID 指的是单一职责原则(Single Responsibility)、开闭原则(Open Closed)、里式替换原则(Liskov substitution)、接口隔离原则(Interface Segregation)和依赖反转原则(Dependency Inversion)。

    \n
      \n
    • 单一职责原则意味着每个类只有一个单一的目的和责任。
    • \n
    • 开闭原则:一个类应该对扩展开放、对修改关闭。详细表述一下就是,添加一个新的功能应该是,在已有代码基础上扩展代码(新增模块、类、方法等),而非修改已有代码(修改模块、类、方法等)。
    • \n
    • 里式替换原则:这个原则指导开发人员任何情况下使用继承都不要破坏应用逻辑。举个例子,如果子类 XyClass 继承自 AbClass,那么 XyClass 不可以改变父类已实现功能的行为。因此你可以放心地使用 XyClass 对象而不是 AbClass 对象,而不用担心破坏应用的逻辑。
    • \n
    • 接口隔离原则:简单来说,因为一个类可以实现多个接口,所以应该合理的组织代码,使一个类无需被迫实现与其目的无关的方法。因此,要把接口分好类。
    • \n
    • 依赖反转原则:如果你遵循 TDD(测试驱动开发Test-Driven Development)的方式进行应用开发,那么你就会知道将代码解耦对于可测试性和模块化是多么重要。举个例子,如果 Order 类依赖于 User 类,那么 User 对象应该在 Order 类之外进行实例化。
    • \n
    \n

    设计模式

    工厂模式

    工厂模式是 OOP 世界中最常用的设计模式,因为通过这个模式,我们在创建对象时不会对客户端暴露创建逻辑,并且是通过使用一个共同的接口来指向新创建的对象。来看个例子:

    \n

    我们现在有一个接口和三个实现了这个接口的类:

    \n
    public class Rectangle implements Shape {

    @Override
    public void draw() {
    System.out.println("Inside Rectangle::draw() method.");
    }
    }

    public class Square implements Shape {

    @Override
    public void draw() {
    System.out.println("Inside Square::draw() method.");
    }
    }

    public class Circle implements Shape {

    @Override
    public void draw() {
    System.out.println("Inside Circle::draw() method.");
    }
    }
    \n

    假如你现在要实例化一个方形 Shape,有两种方式可以实现:

    \n

    第一种:

    \n
    Shape shape = new Square();
    \n

    第二种:

    \n
    Shape shape = ShapeFactory.getShape("SQUARE");
    \n
    public class ShapeFactory {
    public static Shape getShape(String shapeType){
    if(shapeType == null){
    return null;
    }
    if(shapeType.equalsIgnoreCase("CIRCLE")){
    return new Circle();
    } else if(shapeType.equalsIgnoreCase("RECTANGLE")){
    return new Rectangle();
    } else if(shapeType.equalsIgnoreCase("SQUARE")){
    return new Square();
    }
    return null;
    }
    }
    \n

    我更喜欢第二种方式,有三点原因。首先一个调用者想创建一个对象,只要知道其名称就可以了。其次扩展性高,如果想增加一个新的形状,只要扩展 ShapeFactory 工厂类就可以。最后屏蔽了具体的实现,调用者只用关心 Shape 接口,即使需要传额外参数来进行实例化,调用者也无需去关心。

    \n

    适配器模式

    适配器模式是结构设计模式之一。根据这个名字可以判断出,我们可以期望它把类的意外用法转为我们所预期的用法。

    \n

    假如我们的应用要调用了百度的 API,需要在发起请求前需要调用 getBaiduToken() 获取 token。我们在 20 多个不同的地方调用了这个函数。之后百度在发布的新版本中把这个函数改名为了 getAccessToken()

    \n

    现在我们必须在应用代码的所有位置找到并替换这个函数名,或者可以创建一个适配器类:

    \n
    public class BaiduAdapter {

    public static String getToken() {
    Baidu baidu = new Baidu();
    String token = baidu.getBaiduToken;
    return token;
    }

    }
    \n

    应用中的调用改为:

    \n
    token = BaiduAdapter.getToken();
    \n

    这种情况下,即使百度修改了函数名,我们也只需修改一行代码,应用程序的其余部分将保持正常工作。

    \n
    public class BaiduAdapter {

    public static String getToken() {
    Baidu baidu = new Baidu();
    String token = baidu.getAccessToken;
    return token;
    }

    }
    \n

    本文没有详细讨论各种设计模式,如果你想了解更多,我推荐 2 本关于设计模式的书:

    \n
      \n
    • 《设计模式》
        \n
      • 学习设计模式,不知道 GoF 的《设计模式》估计会被人笑话的。这本书比较晦涩难懂,对于初学者不建议从这本书看起。
      • \n
      \n
    • \n
    • 《Head First 设计模式》
        \n
      • 这本书最大的特点就是口语化、场景化。整本书围绕几个人的对话来展开。里面的例子比较脱离实践,但比较容易看懂。
      • \n
      \n
    • \n
    \n

    架构师 vs 程序员

    最后再来说说软件架构师和软件开发人员之间的区别。

    \n

    架构师通常是具有丰富经验的 team leader,他们对现有解决方案有很好的了解,这些方案可以帮助他们在计划阶段做出正确的决策。软件开发人员应该去了解更多的软件设计,并对软件架构有足够的了解,以使团队内部的沟通更佳高效。

    \n"},{"title":"NotNull、NotEmpty、NotBlank 的区别","url":"/2017/different-notnull-notempty-notblank/","content":"

    @NotNull

    The CharSequence, Collection, Map or Array object is not null, but can be empty.

    \n

    @NotEmpty

    The CharSequence, Collection, Map or Array object is not null and size > 0.

    \n

    @NotBlank

    The string is not null and the trimmed length is greater than zero.

    \n

    Here are a few examples:

    String name = null;
    \t@NotNull: false
    \t@NotEmpty: false
    \t@NotBlank: false
    String name = "";
    \t@NotNull: true
    \t@NotEmpty: false
    \t@NotBlank: false
    String name = " ";
    \t@NotNull: true
    \t@NotEmpty: true
    \t@NotBlank: false
    String name = "Great answer!";
    \t@NotNull: true
    \t@NotEmpty: true
    \t@NotBlank: true
    \n"},{"title":"理解人与人大不同","url":"/2019/different-of-people/","content":"
    \n

    本文是对《原则》一书的第二部分,第 4 章节的概括。

    \n
    \n

    \"\"

    \n

    作者认为,由于不同人的大脑构造不同,所以每个人的行为方式(原文中使用的是体验现实的方式)也千差万别。

    \n

    开头处作者讲了一个真实案例,他交给同事鲍勃一项宏大的任务,鲍勃可以任意挑选自己的团队成员,但在事情进行一段时间后他们发现在具体落实方面毫无进展,经过长时间的讨论和研究,发现问题在于鲍勃挑选的每个角色跟他自己的长处、短板相似。

    \n

    作者就此对人们不同的思维方式产生了浓厚的兴趣,开始探寻不同思维方式所带来的不同的力量。

    \n

    我们很多的心理差异实际上是生理差异,大脑就像高矮胖瘦一样存在着的差异,从而影响到我们的心理能力。

    \n

    天性

    我们都有过这样的情况:对其他人做出的决策感到愤怒或者沮丧,但在了解每个人的大脑在生理上就存在不同后,就会逐渐明白他们并不是有意识地采取在我们看来低效的做法,他们只不过是依据自己认为正确的做法来做事,而这种做事方式又是由他们大脑的运行方式决定的,人与人之间出现的分歧不是因为沟通不良导致的,而是因为我们不同的思维方式导致了沟通的不良。

    \n

    每个人的天性各不相同,这些天性即可能帮助我们又可能伤害我们,取决于我们如何运用我们的天性。这也是我们为什么经常说,具有创造力的天才和疯子往往只有一步之遥的原因。很多杰出的有创造力的人都曾患有双相障碍,如贝多芬、海明威、柴可夫斯基、丘吉尔。

    \n

    合作

    科学研究发现,人脑的构造先天地使人需要并享受社会合作。所以做有意义的工作和进行有意义的社交活动,不仅会让我们的生活更美好,更是我们天生就需要的生理需求。从社会合作中获得有意义的人际关系使我们更快乐、更健康、更有创造力,同时也会让我们的大脑发育得更好。我们的祖先进化出了支持合作功能的大脑,并以此支持狩猎等需要合作的活动,随着群体变得比个体更强大,大脑不断进化出管理更大群体的能力,这一进化使得利他意识、伦理观、良知和尊严意识发展起来。

    \n

    斗争

    我们的头脑中永远会存在两股势力间,分别是情绪和理性思考。

    \n

    情绪主要是由潜意识性的杏仁核控制的,而理性思考主要是由意识性的前额皮层控制的。

    \n

    杏仁核是一个小小的杏仁状构造,深深地隐藏在大脑底部,是大脑最强有力的区域之一。尽管你感觉不到它,但它控制着你的行为。作者把人们被情绪控制时的状态称为「杏仁核绑架」。如果你放任自己做出本能反应的话,你就很可能会反应过度,你也可以安慰自己,因为你已经知道,你经历的任何精神痛苦不久后都会自动消失。

    \n

    大部分情况下,「杏仁核绑架」来得快去得也快,杏仁核产生的反应是一阵爆发然后平息,而前额皮层产生的反应更为稳定和持久。

    \n

    我们所面临最大的挑战是让深思熟虑的较高层次的自我管理情绪性的较低层次的自我,做到这一点的最佳途径是有意识地养成习惯。习惯是大脑中最强有力的工具。习惯本质上是一种惯性,一种继续把你一直做的事情做下去(或者继续不做你一直不做的事情)的强烈倾向。研究显示,如果你能坚持某种行为约 18 个月,你就会形成一种几乎要永远做下去的强烈倾向。

    \n

    潜意识

    在我们的大脑中有两种潜意识,一种就是我们上边提到的情绪性的潜意识,它们具有危险的动物性,但我们还有一部分潜意识比意识更聪明、反应更快。

    \n

    人们所说的灵感就是来自这部分潜意识,你会发现大部分情况下我们是在放松、不试图刻意去思考的时候会产生创造性突破。这也解释了为什么我们经常在淋浴的时候产生创意。

    \n

    很多人认为只要往我们大脑中(也就是意识里)不断地塞入东西才能让我们进步,这样做可能会适得其反,有时候清理我们的头脑可能是取得进展的最佳途径。

    \n

    左右脑

    我们听说过一种说法,有的人是左脑思维者,有的人是右脑思维者。

    \n

    简单来说左右脑的分工如下:

    \n
      \n
    • 左脑按顺序推理,分析细节,并擅长线性分析。
    • \n
    • 右脑思考不同类别,识别主题,综合大局。
    • \n
    \n

    通常左脑思维者人们称为「明智」的人,右脑思维者被称为「机灵」的人。

    \n

    \"\"

    \n

    如果我们了解到自己和其他人的思维倾向,认识到这两种思维方式都各有所长,并按照思维方式的不同来对每个人分配他更加擅长的工作,可以产生很好的结果。

    \n

    大自然塑造万物皆是有目的的,每个人都有自己的长处和短处,每个人都在他们的生活中扮演着重要的角色。我们所需要的并不是战胜其他人的勇气,而是坚持做最真实自我的勇气,不必太过在意其他人对你的冀望。

    \n

    各司其职

    在我们生活和工作中会遇到各式各样性格的人,有的人内向,有的人外向;有的人喜欢井然有序的生活方式,另一些喜欢灵活随性的方式;一些人理性分析客观事实,考虑所有与具体情况相关的已知、可证明因素,富有逻辑性地决定如何行动,而另一些偏好感觉者关注人与人之间的和谐;一些人可以看到全局,另一些人看到的是细节;一些人关注日常任务,另一些人关注目标及其实现途径。

    \n

    可以把团队中的成员识别为5种类型,创造者、推进者、改进者、贯彻者和变通者。

    \n
      \n
    • 创造者提出新想法、新概念。他们喜欢非结构化、抽象的活动,喜欢创新和不走寻常路。
    • \n
    • 推进者传递这些新想法并推进。他们喜欢处理人与人之间的关系。他们非常善于激发工作热情。
    • \n
    • 改进者挑战想法。他们分析计划以寻找缺陷,然后以很客观、符合逻辑的方式改进计划。他们喜欢事实和理论,以系统性的方式工作。
    • \n
    • 贯彻者也可以叫作执行者。他们确保重要的工作得到执行,目标被实现。他们关注细节和结果。
    • \n
    • 变通者是以上4种类型的结合。他们能根据特定需求调整自身,并能从各种各样的视角看待问题。
    • \n
    \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 的使用场景并不适用于数据库组件

    上边那些并不是最核心的问题,我认为最核心的是 Docker 的使用场景并不适用于数据库组件。

    \n

    我们来看一下使用 Docker 带来优势:

    \n
      \n
    1. 标准化应用发布:Docker包含了运行环境和可执行程序,可以跨平台和主机使用;
    2. \n
    3. 节约时间,快速部署和启动:VM 启动一般是分钟级,Docker容器启动是秒级;
    4. \n
    5. 方便构建基于SOA架构或微服务架构的系统:通过服务编排,更好的松耦合;
    6. \n
    7. 节约成本:以前一个虚拟机至少需要几个G的磁盘空间,Docker容器可以减少到MB级;
    8. \n
    9. 方便持续集成:通过与代码进行关联使持续集成非常方便;
    10. \n
    11. 可以作为集群系统的轻量主机或节点:在IaaS平台上,已经出现了CaaS,通过容器替代原来的主机。
    12. \n
    \n

    以上提到的大多数优势并不适用于数据库的运行环境:数据库通常是长期运行的,数据完整性是重中之重。我们不需要数据库自动扩容(在 Docker 中水平伸缩只能用于无状态计算服务,而不是数据库)、也不需要持续更新数据库的代码来进行持续集成。

    \n

    数据库版本升级

    除了场景不适合之外,另一个问题是数据库软件版本升级。对于无状态应用或者数据库的小版本更新来说,直接修改 Dockerfile 中的版本号并重新构建、重启即可完成升级,但数据库的大版本升级就没有这么简单了,大版本升级数据库版本会伴随数据存储结构的更新,仅仅升级版本是不行的。通常数据库提供商会提供相应的命令来让我们对数据库进行升级,但这样做的前提是数据库不能运行在容器中(需要进入容器才能执行命令、软件版本无法持久化)。

    \n

    在开发环境中通过 Docker 运行数据库

    凡事也不是绝对的,在开发环境中通过 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 来部署,以下是我积累的一些常用组件的启动命令。

    \n

    MySQL

    docker run -d --restart=always --env TZ='Asia/Shanghai' \\
    \t--name mysql \\
    \t-v /data/mysql:/var/lib/mysql \\
    \t-e MYSQL_ROOT_PASSWORD=MSIfUCuL5XCfGIS0SF6y \\
    \t-p 13306:3306 \\
    \tmariadb:10.2 --character-set-server=utf8mb4 --collation-server=utf8mb4_unicode_ci --max_connections=1024
    \n

    Redis

    docker run -d --restart=always \\
    \t--name redis \\
    \t-v /data/redis:/data \\
    \t-p 16379:6379 \\
    \tredis:4.0 \\
    \t--requirepass "D0mD4dGLXdSo3rFOz7kG8"
    \n

    Minio

    docker run -d --restart=always \\
    \t--name minio \\
    \t-p 19000:9000 \\
    \t-e "MINIO_ACCESS_KEY=Yltu9cY5Fa6T7BimY9" \\
    \t-e "MINIO_SECRET_KEY=Af94ajEW3qyxvXR5pVLiNTJWwY3V" \\
    \t-v /data/minio:/data \\
    \tminio/minio:RELEASE.2020-03-14T02-21-58Z server /data
    \n

    Neo4j

    docker run -d --restart=always \\
    \t--memory 2g --cpus 2 \\
    \t--name neo4j \\
    \t-v /data/neo4j:/data \\
    \t-e NEO4J_AUTH=neo4j/DPHSr195sqVQMDmKya \\
    \t-e NEO4J_ACCEPT_LICENSE_AGREEMENT=yes \\
    \t-p 17474:7474 \\
    \t-p 17687:7687 \\
    \tneo4j:3.5.16-enterprise
    \n

    Zookeeper

    docker-compose.yml

    version: '3'
    services:
    zookeeper:
    image: wurstmeister/zookeeper
    restart: always
    ports:
    - "12181:2181"
    kafka:
    image: wurstmeister/kafka:2.11-1.1.1
    restart: always
    depends_on: [ zookeeper ]
    ports:
    - "19092:9092"
    environment:
    KAFKA_ADVERTISED_HOST_NAME: 192.168.5.58
    KAFKA_ZOOKEEPER_CONNECT: zookeeper:2181
    KAFKA_LOG_DIRS: /logs
    volumes:
    - /var/run/docker.sock:/var/run/docker.sock
    - /data/kafka/logs:/logs
    \n

    YAPI

    docker-compose.yml

    version: '2.1'
    services:
    yapi:
    image: mrjin/yapi:latest
    container_name: yapi
    environment:
    - VERSION=1.5.14
    - LOG_PATH=/tmp/yapi.log
    - HOME=/home
    - PORT=3000
    - ADMIN_EMAIL=yapi@posbao.net
    - DB_SERVER=mongo
    - DB_NAME=yapi
    - DB_PORT=27017
    restart: always
    ports:
    - 3000:3000
    volumes:
    - /data/yapi/log:/home/vendors/log
    depends_on:
    - mongo
    entrypoint: "bash /wait-for-it.sh mongo:27017 -- entrypoint.sh"
    networks:
    - yapi
    mongo:
    image: mongo
    container_name: yapi_mongo
    restart: always
    volumes:
    - /data1/yapi/mongodb:/data/db
    networks:
    - yapi

    networks:
    yapi: {}
    \n

    zipkin

    docker run --restart=always --name zipkin -d -p 9411:9411 openzipkin/zipkin
    \n

    xxl-job-admin

    docker-compose.yml

    version: "2.1"

    services:
    admin:
    environment:
    PARAMS: "--server.port=7995 --spring.datasource.url=jdbc:mysql://mysql:3306/xxl_job?Unicode=true&characterEncoding=UTF-8 --spring.datasource.username=root --spring.datasource.password=123456"
    image: xuxueli/xxl-job-admin:2.1.2
    restart: always
    ports:
    - 7995:7995
    mysql:
    image: mariadb:10.2
    restart: always
    volumes:
    - ./tables_xxl_job.sql:/docker-entrypoint-initdb.d/tables_xxl_job.sql
    - /data/xxl-job/mysql:/var/lib/mysql
    environment:
    TZ: "Asia/Shanghai"
    MYSQL_ROOT_PASSWORD: 123456
    command:
    [
    "--character-set-server=utf8mb4",
    "--collation-server=utf8mb4_unicode_ci",
    "--max_connections=1024",
    ]
    \n
    tables_xxl_job.sql

    CREATE database if NOT EXISTS `xxl_job` default character set utf8mb4 collate utf8mb4_unicode_ci;
    use `xxl_job`;

    SET NAMES utf8mb4;

    CREATE TABLE `xxl_job_info` (
    `id` int(11) NOT NULL AUTO_INCREMENT,
    `job_group` int(11) NOT NULL COMMENT '执行器主键ID',
    `job_cron` varchar(128) NOT NULL COMMENT '任务执行CRON',
    `job_desc` varchar(255) NOT NULL,
    `add_time` datetime DEFAULT NULL,
    `update_time` datetime DEFAULT NULL,
    `author` varchar(64) DEFAULT NULL COMMENT '作者',
    `alarm_email` varchar(255) DEFAULT NULL COMMENT '报警邮件',
    `executor_route_strategy` varchar(50) DEFAULT NULL COMMENT '执行器路由策略',
    `executor_handler` varchar(255) DEFAULT NULL COMMENT '执行器任务handler',
    `executor_param` varchar(512) DEFAULT NULL COMMENT '执行器任务参数',
    `executor_block_strategy` varchar(50) DEFAULT NULL COMMENT '阻塞处理策略',
    `executor_timeout` int(11) NOT NULL DEFAULT '0' COMMENT '任务执行超时时间,单位秒',
    `executor_fail_retry_count` int(11) NOT NULL DEFAULT '0' COMMENT '失败重试次数',
    `glue_type` varchar(50) NOT NULL COMMENT 'GLUE类型',
    `glue_source` mediumtext COMMENT 'GLUE源代码',
    `glue_remark` varchar(128) DEFAULT NULL COMMENT 'GLUE备注',
    `glue_updatetime` datetime DEFAULT NULL COMMENT 'GLUE更新时间',
    `child_jobid` varchar(255) DEFAULT NULL COMMENT '子任务ID,多个逗号分隔',
    `trigger_status` tinyint(4) NOT NULL DEFAULT '0' COMMENT '调度状态:0-停止,1-运行',
    `trigger_last_time` bigint(13) NOT NULL DEFAULT '0' COMMENT '上次调度时间',
    `trigger_next_time` bigint(13) NOT NULL DEFAULT '0' COMMENT '下次调度时间',
    PRIMARY KEY (`id`)
    ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

    CREATE TABLE `xxl_job_log` (
    `id` bigint(20) NOT NULL AUTO_INCREMENT,
    `job_group` int(11) NOT NULL COMMENT '执行器主键ID',
    `job_id` int(11) NOT NULL COMMENT '任务,主键ID',
    `executor_address` varchar(255) DEFAULT NULL COMMENT '执行器地址,本次执行的地址',
    `executor_handler` varchar(255) DEFAULT NULL COMMENT '执行器任务handler',
    `executor_param` varchar(512) DEFAULT NULL COMMENT '执行器任务参数',
    `executor_sharding_param` varchar(20) DEFAULT NULL COMMENT '执行器任务分片参数,格式如 1/2',
    `executor_fail_retry_count` int(11) NOT NULL DEFAULT '0' COMMENT '失败重试次数',
    `trigger_time` datetime DEFAULT NULL COMMENT '调度-时间',
    `trigger_code` int(11) NOT NULL COMMENT '调度-结果',
    `trigger_msg` text COMMENT '调度-日志',
    `handle_time` datetime DEFAULT NULL COMMENT '执行-时间',
    `handle_code` int(11) NOT NULL COMMENT '执行-状态',
    `handle_msg` text COMMENT '执行-日志',
    `alarm_status` tinyint(4) NOT NULL DEFAULT '0' COMMENT '告警状态:0-默认、1-无需告警、2-告警成功、3-告警失败',
    PRIMARY KEY (`id`),
    KEY `I_trigger_time` (`trigger_time`),
    KEY `I_handle_code` (`handle_code`)
    ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

    CREATE TABLE `xxl_job_log_report` (
    `id` int(11) NOT NULL AUTO_INCREMENT,
    `trigger_day` datetime DEFAULT NULL COMMENT '调度-时间',
    `running_count` int(11) NOT NULL DEFAULT '0' COMMENT '运行中-日志数量',
    `suc_count` int(11) NOT NULL DEFAULT '0' COMMENT '执行成功-日志数量',
    `fail_count` int(11) NOT NULL DEFAULT '0' COMMENT '执行失败-日志数量',
    PRIMARY KEY (`id`),
    UNIQUE KEY `i_trigger_day` (`trigger_day`) USING BTREE
    ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

    CREATE TABLE `xxl_job_logglue` (
    `id` int(11) NOT NULL AUTO_INCREMENT,
    `job_id` int(11) NOT NULL COMMENT '任务,主键ID',
    `glue_type` varchar(50) DEFAULT NULL COMMENT 'GLUE类型',
    `glue_source` mediumtext COMMENT 'GLUE源代码',
    `glue_remark` varchar(128) NOT NULL COMMENT 'GLUE备注',
    `add_time` datetime DEFAULT NULL,
    `update_time` datetime DEFAULT NULL,
    PRIMARY KEY (`id`)
    ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

    CREATE TABLE `xxl_job_registry` (
    `id` int(11) NOT NULL AUTO_INCREMENT,
    `registry_group` varchar(50) NOT NULL,
    `registry_key` varchar(255) NOT NULL,
    `registry_value` varchar(255) NOT NULL,
    `update_time` datetime DEFAULT NULL,
    PRIMARY KEY (`id`),
    KEY `i_g_k_v` (`registry_group`,`registry_key`,`registry_value`)
    ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

    CREATE TABLE `xxl_job_group` (
    `id` int(11) NOT NULL AUTO_INCREMENT,
    `app_name` varchar(64) NOT NULL COMMENT '执行器AppName',
    `title` varchar(12) NOT NULL COMMENT '执行器名称',
    `order` int(11) NOT NULL DEFAULT '0' COMMENT '排序',
    `address_type` tinyint(4) NOT NULL DEFAULT '0' COMMENT '执行器地址类型:0=自动注册、1=手动录入',
    `address_list` varchar(512) DEFAULT NULL COMMENT '执行器地址列表,多地址逗号分隔',
    PRIMARY KEY (`id`)
    ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

    CREATE TABLE `xxl_job_user` (
    `id` int(11) NOT NULL AUTO_INCREMENT,
    `username` varchar(50) NOT NULL COMMENT '账号',
    `password` varchar(50) NOT NULL COMMENT '密码',
    `role` tinyint(4) NOT NULL COMMENT '角色:0-普通用户、1-管理员',
    `permission` varchar(255) DEFAULT NULL COMMENT '权限:执行器ID列表,多个逗号分割',
    PRIMARY KEY (`id`),
    UNIQUE KEY `i_username` (`username`) USING BTREE
    ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

    CREATE TABLE `xxl_job_lock` (
    `lock_name` varchar(50) NOT NULL COMMENT '锁名称',
    PRIMARY KEY (`lock_name`)
    ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;


    INSERT INTO `xxl_job_group`(`id`, `app_name`, `title`, `order`, `address_type`, `address_list`) VALUES (1, 'xxl-job-executor-sample', '示例执行器', 1, 0, NULL);
    INSERT INTO `xxl_job_info`(`id`, `job_group`, `job_cron`, `job_desc`, `add_time`, `update_time`, `author`, `alarm_email`, `executor_route_strategy`, `executor_handler`, `executor_param`, `executor_block_strategy`, `executor_timeout`, `executor_fail_retry_count`, `glue_type`, `glue_source`, `glue_remark`, `glue_updatetime`, `child_jobid`) VALUES (1, 1, '0 0 0 * * ? *', '测试任务1', '2018-11-03 22:21:31', '2018-11-03 22:21:31', 'XXL', '', 'FIRST', 'demoJobHandler', '', 'SERIAL_EXECUTION', 0, 0, 'BEAN', '', 'GLUE代码初始化', '2018-11-03 22:21:31', '');
    INSERT INTO `xxl_job_user`(`id`, `username`, `password`, `role`, `permission`) VALUES (1, 'admin', 'e10adc3949ba59abbe56e057f20f883e', 1, NULL);
    INSERT INTO `xxl_job_lock` ( `lock_name`) VALUES ( 'schedule_lock');

    commit;
    \n

    nexus

    docker run -d --restart=always -p 8081:8081 \\
    --name nexus -v /data/nexus-data:/nexus-data sonatype/nexus3
    \n

    gitlab-runner

    version: "3"
    services:
    app:
    image: gitlab/gitlab-runner
    container_name: gitlab-runner-docker
    restart: always
    volumes:
    - ./config:/etc/gitlab-runner
    - /var/run/docker.sock:/var/run/docker.sock
    - ./id_rsa:/home/gitlab-runner/.ssh/id_rsa
    - ./known_hosts:/home/gitlab-runner/.ssh/known_hosts
    \n"},{"title":"使用 docker-compose 一键部署 ELK","url":"/2018/docker-compose-ELK/","content":"

    前两天使用docker 通过一个一个启动的方式,将 ELK 部署了起来,但是逐个启动的方式有些麻烦,所以写了个 docker-compose.yml 来一键启动:

    \n
    version: '2'
    services:
    elasticsearch:
    image: docker.elastic.co/elasticsearch/elasticsearch:6.4.0
    environment:
    - discovery.type=single-node
    volumes:
    - /etc/localtime:/etc/localtime
    - /data01/docker-es/data:/usr/share/elasticsearch/data
    # ports:
    # - "9200:9200"
    # - "9300:9300"
    logstash:
    image: docker.elastic.co/logstash/logstash:6.4.0
    volumes:
    - ./logstash.conf:/usr/share/logstash/pipeline/logstash.conf
    ports:
    - "4560:4560"
    links:
    - elasticsearch
    kibana:
    image: docker.elastic.co/kibana/kibana:6.4.0
    environment:
    - ELASTICSEARCH_URL=http://elasticsearch:9200
    volumes:
    - /etc/localtime:/etc/localtime
    ports:
    - "5601:5601"
    links:
    - elasticsearch
    \n

    logstash.conf

    \n
    input {
    tcp {
    mode => "server"
    host => "0.0.0.0"
    port => 4560
    codec => json
    }
    }
    output {
    elasticsearch {
    hosts => ["http://elasticsearch:9200"]
    index => "%{[service]}-%{+YYYY.MM.dd}"
    }
    stdout { codec => rubydebug }
    }
    \n"},{"title":"利用 docker-compose,搭建本地 HBase 集群","url":"/2021/docker-compose-hbase/","content":"

    最近在重构公司的直播推荐服务,特征数据的存储使用的是 Hbase,但有个问题是我们的开发环境并没有搭建 HBase 集群,开发环境和生产环境网络又不通,这样本地调试就很不方便,所以我需要在本地搭一个 HBase 集群。

    \n

    第一时间想到的就是使用 docker-compose

    \n

    牛顿老师说过,轮子还是别人的圆,于是我在 gayhub 上找到了这个轮子:https://github.com/big-data-europe/docker-hbase

    \n

    但是在搭建过程中发现了它的问题,有两个重要的端口没有在 docker-compose.yml 文件中开放,并且说明中没有提示要修改 hosts,再看了下这个仓库的更新时间,已经有 3 年没有更新,所以我也就不提 pr 了,而是将仓库进行了 fork 并修改了源码,可以直接 clone 我的仓库来搭建,流程如下:

    \n

    1. 将代码 clone 到本地

    git clone git@github.com:Panmax/docker-hbase.git
    cd docker-hbase
    \n

    2. /etc/hosts 中添加以下两项

    0.0.0.0 hbase-master
    0.0.0.0 hbase-region
    \n

    3. 启动

    docker-compose -f docker-compose-distributed-local.yml up
    \n

    等待所有服务完全启动后,就可以让我们的程序通过监听在本地 2181 端口的 zookeeper 去发现并访问 hbase 了。

    \n

    Hbase 数据录入

    \n

    为了验证代码逻辑,我还需要写一些数据到 hbase 中,操作如下:

    \n

    1. 进入容器

    docker exec -it hbase-master bash
    \n

    2. 进入 hbase 安装目录

    cd /opt/hbase-1.2.6/
    \n

    3. 运行 hbase shell

    bin/hbase shell
    \n

    然后就可以使用SQL语句进行操作了,例如:

    \n
    > create 'mods_model_storage', 'f'
    > put 'mods_model_storage','model1','f:model', 'model content1'
    > put 'mods_model_storage','model2','f:model', 'model content2'

    > scan 'mods_model_storage'
    ROW COLUMN+CELL
    model1 column=f:model, timestamp=1617605399565, value=model content1
    model2 column=f:model, timestamp=1617605400576, value=model content2
    2 row(s) in 0.0330 seconds
    \n"},{"title":"使用 Docker 部署 RabbitMQ","url":"/2017/docker-install-rabbitmq/","content":"

    为了更加熟悉我们现在所使用的微服务架构,了解每一个组件的特性,我将部署在 lc0 上的一系列微服务组件(网关、注册中心、配置中心、熔断监控等)尝试重新在 lc7 机器上再部署一遍。

    \n

    在部署配置中心时,需要依赖一个 MQ 组件,项目中用的是 RabbitMQ,所以我需要在 lc7 上安装它。

    \n

    RabbitMQ 是用 Erlang 编写的,直接部署的话需要先部署 Erlang 环境,比较麻烦。在 docker 环境下部署就比较简单了,直接使用 RabbitMQ 官方提供的镜像即可。

    \n

    直接安装的方式可以参考 http://blog.didispace.com/spring-boot-rabbitmq/ 这篇文章,下边我主要来说下如何使用 Docker 部署 RabbitMQ。

    \n

    运行 docker pull rabbitmq:management 从官方下载镜像到本地,这里使用的是带 Web 管理插件的镜像。

    \n

    启动容器:

    \n
    docker run -d --name rabbitmq --publish 5671:5671 \\
    --publish 5672:5672 --publish 4369:4369 --publish 25672:25672 --publish 15671:15671 --publish 15672:15672 \\
    rabbitmq:management
    \n

    容器启动之后就可以访问 Web 管理界面了 http://IP:15672

    \n

    \"\"

    \n

    默认创建了一个 guest 用户,密码也是 guest。

    \n

    \"\"

    \n

    通过这种方式来部署 RabbitMQ 非常方便,今后可以在部署 测试环境 时用起来,因为我们还没有大规模使用 Docker,所以暂时不建议在 生产环境 来使用。

    \n

    AMQP 协议中的几个重要概念

      \n
    • Queue 是 RabbitMQ 的内部对象,用于存储消息。RabbitMQ 中的消息只能存储在 Queue 中,消费者从 Queue 中获取消息并消费。
    • \n
    • Exchange 生产者将消息发送到 Exchange,由 Exchange 根据一定的规则将消息路由到一个或多个 Queue 中(或者丢弃)。
    • \n
    • Binding RabbitMQ 中通过 Binding 将 Exchange 与 Queue 关联起来。
    • \n
    • Binding key 在绑定(Binding) Exchange 与 Queue 的同时,一般会指定一个 binding key。
    • \n
    • Routing key 生产者在将消息发送给 Exchange 的时候,一般会指定一个 routing key,来指定这个消息的路由规则。 Exchange 会根据 routing key 和 Exchange Type 以及 Binding key 的匹配情况来决定把消息路由到哪个 Queue。
    • \n
    • Exchange Types RabbitMQ 常用的 Exchange Type 有 fanout、 direct、 topic、 headers 这四种。
        \n
      • fanout 这种类型的 Exchange 路由规则非常简单,它会把所有发送到该 Exchange 的消息路由到所有与它绑定的 Queue 中,这时 Routing key 不起作用。
      • \n
      • direct 这种类型的 Exchange 路由规则也很简单,它会把消息路由到那些 binding key 与 routing key完全匹配的 Queue 中。
      • \n
      • topic 这种类型的 Exchange 的路由规则支持 binding key 和 routing key 的模糊匹配,会把消息路由到满足条件的 Queue。 binding key 中可以存在两种特殊字符 与 #,用于做模糊匹配,其中 用于匹配一个单词,# 用于匹配多个单词(可以是零个),单词以 . 为分隔符。
      • \n
      • headers 这种类型的 Exchange 不依赖于 routing key 与 binding key 的匹配规则来路由消息,而是根据发送的消息内容中的 headers 属性进行匹配。
      • \n
      \n
    • \n
    \n"},{"title":"docker 部署 MySQL 使用 utf8mb4 字符集","url":"/2018/docker-mysql-utf8mb4/","content":"

    背景

    \n

    utf8 是 MySQL 早期版本中支持的一种字符集,只支持最长三个字节的 UTF-8 字符,也就是 Unicode 中的基本多文本平面。这可能是因为在 MySQL 发布初期,基本多文种平面之外的字符确实很少用到。而在 MySQL5.5.3 版本后,要在 MySQL 中保存 4 字节长度的 UTF-8 字符,就可以使用 utf8mb4 字符集了。例如可以用 utf8mb4 字符编码直接存储 emoj 表情,而不是存表情的替换字符。

    \n
    \n

    正文

    相信好多人都被 emoji 表情在 MySQL 中存储的问题坑过,被坑过的人都记住了在 MySQL 中创建库表时要使用 utf8mb4,而不是 utf8。

    \n

    但如果 MySQL 的服务端没有设置为 utf8mb4 的话,使用 jdbc 往里写 emoji 同样会报错,即便是指定了 characterEncoding=utf8 也不会起作用,并且 characterEncoding 不支持设置为 utf8mb4

    \n

    报错通常为:

    \n
    Incorrect string value: '\\xF0\\x9F...' for column 'xxx' at row 1
    \n

    解决办法是将服务端的默认字符集改为 utf8mb4。我们的 MySQL 是使用 docker 启动的,需要把配置文件映射进去。在这里我们踩过一次坑,之前我们是将 my.conf 文件映射到容器的 /etc/my.cnf,实际使用时发现配置文件并没有生效,需要映射到 /etc/mysql/my.cnf 才能生效。

    \n

    解决问题:

    先查看一下当前数据库的字符集:

    \n
    > show variables like '%char%';

    +--------------------------+----------------------------+
    | Variable_name | Value |
    +--------------------------+----------------------------+
    | character_set_client | latin1 |
    | character_set_connection | latin1 |
    | character_set_database | latin1 |
    | character_set_filesystem | binary |
    | character_set_results | latin1 |
    | character_set_server | latin1 |
    | character_set_system | utf8 |
    | character_sets_dir | /usr/share/mysql/charsets/ |
    +--------------------------+----------------------------+
    \n

    发现默认都是 latin1,现在我们在 my.conf 中的相应模块中加入如下配置,然后重启 MySQL:

    \n
    [client]
    default-character-set=utf8mb4

    [mysql]
    default-character-set=utf8mb4

    [mysqld]
    character-set-server=utf8mb4
    collation-server=utf8mb4_general_ci
    \n

    再次查看字符集:

    \n
    > show variables like '%char%';

    +--------------------------+----------------------------+
    | Variable_name | Value |
    +--------------------------+----------------------------+
    | character_set_client | utf8mb4 |
    | character_set_connection | utf8mb4 |
    | character_set_database | utf8mb4 |
    | character_set_filesystem | binary |
    | character_set_results | utf8mb4 |
    | character_set_server | utf8mb4 |
    | character_set_system | utf8 |
    | character_sets_dir | /usr/share/mysql/charsets/ |
    +--------------------------+----------------------------+
    \n

    发现已经更改为了 utf8mb4,再次尝试插入 emoji 成功,问题解决。

    \n

    补充说明:

    jdbc 在连接 MySQL 时,如果不指定 characterEncoding 会默认使用 MySQL 服务端 的字符集,因为之前我们的 MySQL 服务端字符集为 latin1,所以手动指定了一下 characterEncoding=utf8,但这样使用的是 utf8 编码建立连接,所以依旧不能插入 emoji。

    \n

    在修改为 utf8mb4 之后也就不用设置 characterEncoding=utf8useUnicode=true 参数了(我尝试了下,不去掉也没有什么问题)。

    \n

    docker MySQL 启动命令

    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
    \n"},{"title":"Docker 与虚拟化技术查漏补缺","url":"/2020/docker-leak-filling/","content":"

    我的 Docker 使用经验都是通过在项目中的运用学到的,实际上已经可以满足日常所需了,但是自认为缺乏一些细节方面的知识,所以这几天通过阅读一本掘金小册《开发者必备的 Docker 实践指南》,进行了一次系统性学习,以下是我记录的一些我认为的重点和我之前不太了解或不熟悉的内容。

    \n

    本文不适合作为 Docker 初学者学习的指南,适合于查漏补缺时的参考。

    \n
    \n

    \"\"

    \n

    容器技术

    所谓容器技术,指的是操作系统自身支持一些接口,能够让应用程序间可以互不干扰的独立运行,并且能够对其在运行中所使用的资源进行干预。

    \n

    由于没有指令转换,运行在容器中的应用程序自身必须支持在真实操作系统上运行,也就是必须遵循硬件平台的指令规则。

    \n
      \n
    • 容器技术提供了相对独立的应用程序运行的环境,也提供了资源控制的功能,所以我们依然可以归纳其为一种实现不完全的虚拟化技术
    • \n
    \n

    虚拟机 VS 容器

    \"\"

    \n
      \n
    • 由于没有了虚拟操作系统虚拟机监视器这两个层次,大幅减少了应用程序运行带来的额外消耗。
    • \n
    • 运行在容器虚拟化中的应用程序,在运行效率上与真实运行在物理平台上的应用程序不相上下。
    • \n
    \n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n
    属性虚拟机Docker
    启动速度分钟级秒级
    硬盘使用GB 级MB 级
    性能较低接近原生
    普通机器支撑量几个数百个
    \n

    Docker 技术实现

    Docker 的实现,主要归结于三大技术:

    \n
      \n
    • 命名空间 ( Namespaces )
        \n
      • Linux 核心在 2.4 版本后逐渐引入的一项用于运行隔离的模块。
      • \n
      \n
    • \n
    • 控制组 ( Control Groups )
        \n
      • Linux 内核在 2.6 版本后逐渐引入的一项对计算机资源控制的模块。
      • \n
      • CGroups 主要做的是硬件资源的隔离。
      • \n
      \n
    • \n
    • 联合文件系统 ( Union File System )
        \n
      • 联合文件系统 ( Union File System ) 是一种能够同时挂载不同实际文件或文件夹到同一目录,形成一种联合文件结构的文件系统。
      • \n
      • 在 Docker 中,提供了一种对 UnionFS 的改进实现,也就是 AUFS ( Advanced Union File System )
      • \n
      \n
    • \n
    \n

    Docker 的理念

    与其他虚拟化实现甚至其他容器引擎不同的是,Docker 推崇一种轻量级容器的结构

    \n
      \n
    • 即一个应用一个容器
    • \n
    \n

    我们能用 Docker 做些什么

      \n
    1. 更快、更一致的交付你的应用程序
    2. \n
    3. 跨平台部署和动态伸缩
    4. \n
    5. 让同样的硬件提供更多的产出能力
    6. \n
    \n

    Docker 核心

    四大组成对象

    \n
      \n
    • 镜像 ( Image )
        \n
      • 可以理解为一个只读的文件包,其中包含了虚拟环境运行最原始文件系统的内容。
      • \n
      \n
    • \n
    • 容器 ( Container )
        \n
      • 如果把镜像理解为编程中的类,那么容器就可以理解为类的实例。
      • \n
      \n
    • \n
    • 网络 ( Network )
    • \n
    • 数据卷 ( Volume )
        \n
      • 在 Docker 中,通过这几种方式进行数据共享或持久化的文件或目录,我们都称为数据卷 ( Volume )。
      • \n
      \n
    • \n
    \n

    Docker Engine

    在 Docker Engine 中,实现了 Docker 技术中最核心的部分,也就是容器引擎这一部分。

    \n

    docker daemon 和 docker CLI

    Docker Engine 是由多个独立软件所组成的软件包。最核心的是 docker daemondocker CLI

    \n

    \"\"

    \n

    在 docker daemon 管理容器等相关资源的同时,它也向外暴露了一套 RESTful API

    \n

    \"\"

    \n

    docker daemon 和 docker CLI 所组成的,正是一个标准 C/S ( Client-Server ) 结构的应用程序。衔接这两者的,正是 docker daemon 所提供的这套 RESTful API

    \n

    搭建 Docker 运行环境

    Docker Engine 的稳定版固定为每三个月更新一次,而预览版则每月都会更新。

    \n

    \"\"

    \n

    不论是稳定版还是预览版,它们都会以发布时的年月来命名版本号,例如如 17 年 3 月的版本,版本号就是 17.03。

    \n
      \n
    • 在主要版本之外,Docker 官方也以解决 Bug 为主要目的,不定期发布次要版本。次要版本的版本号由主要版本和发布序号组成
        \n
      • 如:17.03.2 就是对 17.03 版本的第二次修正。
      • \n
      \n
    • \n
    \n

    Docker 的环境依赖

    以目前 Docker 官方主要维护的版本为例,我们需要使用基于 Linux kernel 3.10 以上版本的 Linux 系统来安装 Docker。

    \n

    在Mac 和 Windows 中使用 Docker

    Docker Desktop

    Docker 官方为 Windows 和 macOS 系统单独开辟了一条产品线,名为 Docker Desktop,其定位是快速为开发者提供在 Windows 和 macOS 中运行 Docker 环境的工具。

    \n

    Docker Desktop 的实现原理

    既然 Windows 和 macOS 中没有 Docker 能够利用的 Linux 环境,那么我们需要提供一个 Linux 环境

    \n
      \n
    • 在 Windows 中,通过 Hyper-V 实现虚拟化
        \n
      • 对于 Windows 系统来说,安装 Docker for Windows 需要符合以下条件:
          \n
        • 必须使用 Windows 10 Pro ( 专业版 )
        • \n
        • 必须使用 64 bit 版本的 Windows
        • \n
        \n
      • \n
      \n
    • \n
    • 在 macOS 中,通过 HyperKit 实现虚拟化
    • \n
    \n

    \"\"

    \n

    镜像与容器

    容器的生命周期

    \"\"

    \n

    Docker 容器的生命周期里分为五种状态:

    \n
      \n
    • Created:容器已经被创建,容器所需的相关资源已经准备就绪,但容器中的程序还未处于运行状态。
    • \n
    • Running:容器正在运行,也就是容器中的应用正在运行。
    • \n
    • Paused:容器已暂停,表示容器中的所有程序都处于暂停 ( 不是停止 ) 状态。
    • \n
    • Stopped:容器处于停止状态,占用的资源和沙盒环境都依然存在,只是容器中的应用程序均已停止。
    • \n
    • Deleted:容器已删除,相关占用的资源及存储在 Docker 中的管理信息也都已释放和移除。
    • \n
    \n

    主进程

    在 Docker 的设计中,容器的生命周期其实与容器中 PID 为 1 这个进程有着密切的关系。

    \n
      \n
    • 当我们启动容器时,Docker 其实会按照镜像中的定义,启动对应的程序,并将这个程序的主进程作为容器的主进程 ( 也就是 PID 为 1 的进程 )。
    • \n
    • 而当我们控制容器停止时,Docker 会向主进程发送结束信号,通知程序退出。
    • \n
    • 而当容器中的主进程主动关闭时 ( 正常结束或出错停止 ),也会让容器随之停止。
    • \n
    \n

    写时复制 ( Copy on Write ) 机制

    Docker 的写时复制与编程中的相类似,也就是在通过镜像运行容器时,并不是马上就把镜像里的所有内容拷贝到容器所运行的沙盒文件系统中,而是利用 UnionFS 将镜像以只读的方式挂载到沙盒文件系统中。只有在容器中发生对文件的修改时,修改才会体现到沙盒环境上。

    \n
      \n
    • 也就是说,容器在创建和启动的过程中,不需要进行任何的文件系统复制操作,也不需要为容器单独开辟大量的硬盘空间
    • \n
    • 采用写时复制机制来设计的 Docker,既保证了镜像在生成为容器时,以及容器在运行过程中,不会对自身造成修改。又借助剔除常见虚拟化在初始化时需要从镜像中拷贝整个文件系统的过程,大幅提高了容器的创建和启动速度。
    • \n
    • 可以说,Docker 容器能够实现秒级启动速度,写时复制机制在其中发挥了举足轻重的作用。
    • \n
    \n

    运行和管理容器

    管理容器

    通过 docker ps 命令,可以罗列出 Docker 中的容器。

    \n
      \n
    • 默认情况下,docker ps 列出的容器是处于运行中的容器,如果要列出所有状态的容器,需要增加 -a--all 选项。
    • \n
    • CONTAINER IDIMAGECREATED、NAMES 分别表示容器 ID,容器所基于的镜像,容器的创建时间和容器的名称。
    • \n
    • COMMAND 表示的是容器中主程序 ( 也就是与容器生命周期所绑定进程所关联的程序 ) 的启动命令,这条命令是在镜像内定义的,而容器的启动其实质就是启动这条命令。
    • \n
    • STATUS 表示容器所处的状态,常见的状态表示有三种:
        \n
      • Created 此时容器已创建,但还没有被启动过。
      • \n
      • Up [ Time ] 这时候容器处于正在运行状态,而这里的 Time 表示容器从开始运行到查看时的时间。
      • \n
      • Exited ([ Code ]) [ Time ] 容器已经结束运行,这里的 Code 表示容器结束运行时,主程序返回的程序退出码,而 Time 则表示容器结束到查看时的时间。
      • \n
      \n
    • \n
    \n

    进入容器

    在开发过程中,我们更常使用它来作为我们进入容器的桥梁。

    \n
      \n
    • 这里说的进入容器,就是通过 docker exec 命令来启动 sh 或 bash,并通过它们实现对容器内的虚拟环境的控制。
    • \n
    • 由于 bash 的功能要比 sh 丰富,所以在能够使用 bash 的容器里,我们优先选择它作为控制台程序。
    • \n
    • docker exec -it nginx bash
        \n
      • -i ( –interactive ) 表示保持我们的输入流
          \n
        • 只有使用它才能保证控制台程序能够正确识别我们的命令
        • \n
        \n
      • \n
      • -t ( –tty ) 表示启用一个伪终端,形成我们与 bash 的交互
          \n
        • 如果没有它,我们无法看到 bash 内部的执行结果
        • \n
        \n
      • \n
      \n
    • \n
    \n

    衔接到容器

    Docker 为我们提供了一个 docker attach 命令,用于将当前的输入输出流连接到指定的容器上。

    \n
      \n
    • docker attach nginx
    • \n
    • 可以理解为我们将容器中的主程序转为了“前台”运行 ( 与 docker run 中的 -d 选项有相反的意思 )
    • \n
    • 在实际开发中,由于 docker attach 限制较多,功能也不够强大,所以并没有太多用武之地。
    • \n
    \n

    为容器配置网络

    在 Docker 网络中,有三个比较核心的概念:沙盒 ( Sandbox )、网络 ( Network )、端点 ( Endpoint )

    \n
      \n
    • 沙盒提供了容器的虚拟网络栈
        \n
      • 也就是之前所提到的端口套接字、IP 路由表、防火墙等的内容。
      • \n
      • 隔离了容器网络与宿主机网络,形成了完全独立的容器网络环境。
      • \n
      \n
    • \n
    • 网络可以理解为 Docker 内部的虚拟子网
        \n
      • 网络内的参与者相互可见并能够进行通讯。
      • \n
      • Docker 的这种虚拟网络也是与宿主机网络存在隔离关系的,其目的主要是形成容器间的安全通讯环境。
      • \n
      \n
    • \n
    • 端点是位于容器或网络隔离墙之上的洞
        \n
      • 其主要目的是形成一个可以控制的突破封闭的网络环境的出入口。
      • \n
      • 当容器的端点与网络的端点形成配对后,就如同在这两者之间搭建了桥梁,便能够进行数据传输了。
      • \n
      \n
    • \n
    \n

    这三者形成了 Docker 网络的核心模型,也就是容器网络模型 ( Container Network Model )。

    \n

    网络驱动的种类

    目前 Docker 官方为我们提供了五种 Docker 网络驱动,分别是:Bridge Driver、Host Driver、Overlay Driver、MacLan Driver、None Driver

    \n
      \n
    • Bridge 网络是 Docker 容器的默认网络驱动
        \n
      • 简而言之其就是通过网桥来实现网络通讯
      • \n
      \n
    • \n
    • Overlay 网络是借助 Docker 集群模块 Docker Swarm 来搭建的跨 Docker Daemon 网络
        \n
      • 我们可以通过它搭建跨物理主机的虚拟网络,进而让不同物理机中运行的容器感知不到多个物理机的存在。
      • \n
      \n
    • \n
    \n

    暴露端口

    Docker 为容器网络增加了一套安全机制,只有容器自身允许的端口,才能被其他容器所访问。

    \n
      \n
    • 这个容器自我标记端口可被访问的过程,我们通常称为暴露端口
    • \n
    \n

    端口的暴露可以通过 Docker 镜像进行定义,也可以在容器创建时进行定义。

    \n
      \n
    • 在容器创建时进行定义的方法是借助 –expose 这个选项。
    • \n
    • docker run -d --name mysql -e MYSQL_RANDOM_ROOT_PASSWORD=yes --expose 13306 --expose 23306 mysql:5.7
    • \n
    \n

    这里我们为 MySQL 暴露了 13306 和 23306 这两个端口,暴露后我们可以在 docker ps 中看到这两个端口已经成功的打开。

    \n
    … PORTS                                       NAMES
    … 3306/tcp, 13306/tcp, 23306/tcp, 33060/tcp mysql
    \n

    创建网络

    在 Docker 里,我们也能够创建网络,形成自己定义虚拟子网的目的。

    \n

    docker network create -d bridge individual

    \n
      \n
    • 通过 -d 选项我们可以为新的网络指定驱动的类型
        \n
      • 其值可以是刚才我们所提及的 bridge、host、overlay、maclan、none,也可以是其他网络驱动插件所定义的类型
      • \n
      • 这里我们使用的是 Bridge Driver ( 当我们不指定网络驱动时,Docker 也会默认采用 Bridge Driver 作为网络驱动 )。
      • \n
      \n
    • \n
    \n

    通过 docker network ls 或是 docker network list 可以查看 Docker 中已经存在的网络。

    \n

    我们创建容器时,可以通过 --network 来指定容器所加入的网络

    \n
      \n
    • 一旦这个参数被指定,容器便不会默认加入到 bridge 这个网络中了 ( 但是仍然可以通过 --network bridge 让其加入 )。
    • \n
    • docker run -d --name mysql -e MYSQL_RANDOM_ROOT_PASSWORD=yes --network individual mysql:5.7
    • \n
    • 两个容器处于不同的网络,之间是不能相互连接引用的。以下启动命令会报错:
        \n
      • docker run -d --name webapp --link mysql --network bridge webapp:latest
      • \n
      \n
    • \n
    \n

    端口映射

    在实际使用中,还有一个非常常见的需求,就是我们需要在容器外通过网络访问容器中的应用。

    \n

    \"\"

    \n

    通过 Docker 端口映射功能,我们可以把容器的端口映射到宿主操作系统的端口上,当我们从外部访问宿主操作系统的端口时,数据请求就会自动发送给与之关联的容器端口。

    \n
      \n
    • 要映射端口,我们可以在创建容器时使用 -p 或者是 –publish 选项。
    • \n
    • docker run -d --name nginx -p 80:80 -p 443:443 nginx:1.12
    • \n
    \n

    使用端口映射选项的格式是 -p <ip>:<host-port>:<container-port>,其中 ip 是宿主操作系统的监听 ip,可以用来控制监听的网卡,默认为 0.0.0.0,也就是监听所有网卡

    \n

    管理和存储数据

    挂载方式

    基于底层存储实现,Docker 提供了三种适用于不同场景的文件系统挂载方式:Bind Mount、Volume 和 Tmpfs Mount

    \n
      \n
    • Bind Mount 能够直接将宿主操作系统中的目录和文件挂载到容器内的文件系统中,通过指定容器外的路径和容器内的路径,就可以形成挂载映射关系,在容器内外对文件的读写,都是相互可见的。
    • \n
    • Volume 也是从宿主操作系统中挂载目录到容器内,只不过这个挂载的目录由 Docker 进行管理,我们只需要指定容器内的目录,不需要关心具体挂载到了宿主操作系统中的哪里。
    • \n
    • Tmpfs Mount 支持挂载系统内存中的一部分到容器的文件系统里,不过由于内存和容器的特征,它的存储并不是持久的,其中的内容会随着容器的停止而消失。
    • \n
    \n

    \"\"

    \n

    挂载文件到容器

    使用 -v--volume 来挂载宿主操作系统目录的形式是 -v <host-path>:<container-path>--volume <host-path>:<container-path>,其中 host-pathcontainer-path 分别代表宿主操作系统中的目录和容器中的目录。

    \n
      \n
    • 需要注意的是,为了避免混淆,Docker 这里强制定义目录时必须使用绝对路径,不能使用相对路径。
    • \n
    • 能够指定目录进行挂载,也能够指定具体的文件来挂载
    • \n
    \n

    Docker 还支持以只读的方式挂载,通过只读方式挂载的目录和文件,只能被容器中的程序读取,但不接受容器中程序修改它们的请求。在挂载选项 -v 后再接上 :ro 就可以只读挂载了。

    \n
      \n
    • docker run -d --name nginx -v /webapp/html:/usr/share/nginx/html:ro nginx:1.12
    • \n
    \n

    Bind Mount 常见场景:

    \n
      \n
    • 当我们需要从宿主操作系统共享配置的时候。
        \n
      • 对于一些配置项,我们可以直接从容器外部挂载到容器中,这利于保证容器中的配置为我们所确认的值,也方便我们对配置进行监控。
          \n
        • 例如,遇到容器中时区不正确的时候,我们可以直接将操作系统的时区配置,也就是 /etc/timezone 这个文件挂载并覆盖容器中的时区配置。
        • \n
        \n
      • \n
      \n
    • \n
    • 当我们需要借助 Docker 进行开发的时候。
        \n
      • 虽然在 Docker 中,推崇直接将代码和配置打包进镜像,以便快速部署和快速重建。但这在开发过程中显然非常不方便,因为每次构建镜像需要耗费一定的时间,这些时间积少成多,就是对开发工作效率的严重浪费了。如果我们直接把代码挂载进入容器,那么我们每次对代码的修改都可以直接在容器外部进行。
      • \n
      \n
    • \n
    \n

    挂载临时文件目录

    Tmpfs Mount 是一种特殊的挂载方式,它主要利用内存来存储数据。由于内存不是持久性存储设备,所以其带给 Tmpfs Mount 的特征就是临时性挂载。

    \n

    挂载临时文件目录要通过 --tmpfs 这个选项来完成。

    \n
      \n
    • 由于内存的具体位置不需要我们来指定,这个选项里我们只需要传递挂载到容器内的目录即可。
    • \n
    • docker run -d --name webapp --tmpfs /webapp/cache webapp:latest
    • \n
    \n

    Tmpfs Mount 常见场景:

    \n
      \n
    • 应用中使用到,但不需要进行持久保存的敏感数据,可以借助内存的非持久性和程序隔离性进行一定的安全保障。
    • \n
    • 读写速度要求较高,数据变化量大,但不需要持久保存的数据,可以借助内存的高读写速度减少操作的时间。
    • \n
    \n

    使用数据卷

    数据卷的本质其实依然是宿主操作系统上的一个目录,只不过这个目录存放在 Docker 内部,接受 Docker 的管理。

    \n
      \n
    • 在使用数据卷进行挂载时,我们不需要知道数据具体存储在了宿主操作系统的何处,只需要给定容器中的哪个目录会被挂载即可。
    • \n
    • docker run -d --name webapp -v /webapp/storage webapp:latest
    • \n
    \n

    为了方便识别数据卷,我们可以像命名容器一样为数据卷命名。在我们未给出数据卷命名的时候,Docker 会采用数据卷的 ID 命名数据卷。我们也可以通过 -v <name>:<container-path> 这种形式来命名数据卷。

    \n
      \n
    • $ docker run -d --name webapp -v appdata:/webapp/storage webapp:latest
    • \n
    • 前面提到了,-v 在定义绑定挂载时必须使用绝对路径,其目的主要是为了避免与数据卷挂载中命名这种形式的冲突。
    • \n
    \n

    数据卷常见场景:

    \n
      \n
    • 当希望将数据在多个容器间共享时,利用数据卷可以在保证数据持久性和完整性的前提下,完成更多自动化操作。
    • \n
    • 当我们希望对容器中挂载的内容进行管理时,可以直接利用数据卷自身的管理方法实现。
    • \n
    • 使用远程服务器或云服务作为存储介质的时候,数据卷能够隐藏更多的细节,让整个过程变得更加简单。
    • \n
    \n

    共用数据卷

    数据卷的另一大作用是实现容器间的目录共享,也就是通过挂载相同的数据卷,让容器之间能够同时看到并操作数据卷中的内容。

    \n
      \n
    • docker run -d --name webapp -v html:/webapp/html webapp:latest
    • \n
    • docker run -d --name nginx -v html:/usr/share/nginx/html:ro nginx:1.12
    • \n
    • 使用 -v 选项挂载数据卷时,如果数据卷不存在,Docker 会为我们自动创建和分配宿主操作系统的目录,而如果同名数据卷已经存在,则会直接引用。
    • \n
    \n

    删除数据卷

    通过 docker volume rm 来删除指定的数据卷

    \n
      \n
    • docker volume rm appdata
    • \n
    • 在删除数据卷之前,我们必须保证数据卷没有被任何容器所使用 ( 也就是之前引用过这个数据卷的容器都已经删除 ),否则 Docker 不会允许我们删除这个数据卷。
    • \n
    \n

    docker rm 删除容器的命令中,我们可以通过增加 -v 选项来删除容器关联的数据卷。

    \n
      \n
    • docker rm -v webapp
    • \n
    \n

    Docker 向我们提供了 docker volume prune 命令,可以删除那些没有被容器引用的数据卷。

    \n

    数据卷容器

    数据卷容器,就是一个没有具体指定的应用,甚至不需要运行的容器,我们使用它的目的,是为了定义一个或多个数据卷并持有它们的引用。

    \n

    由于不需要容器本身运行,因而找个简单的系统镜像都可以完成创建。

    \n
      \n
    • docker create --name appdata -v /webapp/storage ubuntu
    • \n
    • 在使用数据卷容器时,我们不建议再定义数据卷的名称,因为我们可以通过对数据卷容器的引用来完成数据卷的引用。
    • \n
    \n

    Docker 的 Network 是容器间的网络桥梁,如果做类比,数据卷容器就可以算是容器间的文件系统桥梁

    \n
      \n
    • 我们可以像加入网络一样引用数据卷容器,只需要在创建新容器时使用专门的 --volumes-from 选项即可。
    • \n
    • docker run -d --name webapp --volumes-from appdata webapp:latest
    • \n
    • 引用数据卷容器时,不需要再定义数据卷挂载到容器中的位置,Docker 会以数据卷容器中的挂载定义将数据卷挂载到引用的容器中
    • \n
    \n

    备份和迁移数据卷

    利用数据卷容器,我们能够更方便的对数据卷中的数据进行迁移。

    \n

    数据备份、迁移、恢复的过程可以理解为对数据进行打包,移动到其他位置,在需要的地方解压的过程。

    \n
      \n
    • 要备份数据,我们先建立一个临时的容器,将用于备份的目录和要备份的数据卷都挂载到这个容器上。
        \n
      • docker run --rm --volumes-from appdata -v /backup:/backup ubuntu tar cvf /backup/backup.tar /webapp/storage
          \n
        • 通过 --rm 选项,我们可以让容器在停止后自动删除,而不需要我们再使用容器删除命令来删除它,这对于我们使用一些临时容器很有帮助。
        • \n
        • 我们在镜像定义之后接上命令,可以直接替换掉镜像所定义的主程序启动命令,而去执行这一条命令。
        • \n
        • 在备份后,我们就可以在 /backup 下找到数据卷的备份文件,也就是 backup.tar 了。
        • \n
        \n
      • \n
      \n
    • \n
    • 如果要恢复数据卷中的数据,我们也可以借助临时容器完成。
        \n
      • docker run --rm --volumes-from appdata -v /backup:/backup ubuntu tar xvf /backup/backup.tar -C /webapp/storage --strip
      • \n
      \n
    • \n
    \n

    另一个挂载选项

    Docker 里为我们提供了一个相对支持丰富的挂载方式,也就是通过 --mount 这个选项配置挂载。

    \n
      \n
    • 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
        \n
      • --mount 中,我们可以通过逗号分隔这种 CSV 格式来定义多个参数。
          \n
        • 通过 type 我们可以定义挂载类型,其值可以是:bind,volume 或 tmpfs
        • \n
        \n
      • \n
      • --mount 选项能够帮助我们实现集群挂载的定义,例如在这个例子中,我们挂载的来源是一个 NFS 目录。
      • \n
      \n
    • \n
    \n

    挂载主要有三种目的:

      \n
    1. 将程序的配置通过挂载的方式覆盖容器中对应的文件
        \n
      • 这让我们可以直接在容器外修改程序的配置,并通过直接重启容器就能应用这些配置;
      • \n
      \n
    2. \n
    3. 把目录挂载到容器中应用数据的输出目录
    4. \n
    \n
      \n
    • 让容器中的程序直接将数据输出到容器外,对于 MySQL、Redis 中的数据,程序的日志等内容,我们可以使用这种方法来持久保存它们;
    • \n
    \n
      \n
    1. 把代码或者编译后的程序挂载到容器中
        \n
      • 让它们在容器中可以直接运行,这就避免了我们在开发中反复构建镜像带来的麻烦,节省出大量宝贵的开发时间。
      • \n
      \n
    2. \n
    \n

    保存和共享镜像

    提交容器更改

    Docker 镜像的本质是多个基于 UnionFS 的镜像层依次挂载的结果,而容器的文件系统则是在以只读方式挂载镜像后增加的一个可读可写的沙盒环境。Docker 中为我们提供了将容器中的这个可读可写的沙盒环境持久化为一个镜像层的方法。

    \n
      \n
    • 我们能够在 Docker 里将容器内的修改记录下来,保存为一个新的镜像。
    • \n
    \n

    将容器修改的内容保存为镜像的命令是 docker commit

    \n
      \n
    • 由于镜像的结构很像代码仓库里的修改记录,而记录容器修改的过程又像是在提交代码,所以这里我们更形象的称之为提交容器的更改。
    • \n
    • docker commit webapp
    • \n
    • 像通过 Git 等代码仓库软件提交代码一样,我们还能在提交容器更改的时候给出一个提交信息,方便以后查询。
        \n
      • docker commit -m "Configured" webapp
      • \n
      \n
    • \n
    • Docker 执行将容器内沙盒文件系统记录成镜像层的时候,会先暂停容器的运行,以保证容器内的文件系统处于一个相对稳定的状态,确保数据的一致性。
    • \n
    \n

    为镜像命名

    docker tag 0bc42f7ff218 webapp:1.0

    \n

    使用 docker tag 能够为未命名的镜像指定镜像名,也能够对已有的镜像创建一个新的命名。

    \n
      \n
    • 当我们对未命名的镜像进行命名后,Docker 就不会在镜像列表里继续显示这个镜像,取而代之的是我们新的命名。
    • \n
    • 而如果我们对以后镜像使用 docker tag,旧的镜像依然会存在于镜像列表中。
        \n
      • docker tag webapp:1.0 webapp:latest
      • \n
      • 实质是它们其实引用着相同的镜像层,这个我们能够从镜像 ID 中看得出来 ( 因为镜像 ID 就是最上层镜像层的 ID )。
      • \n
      \n
    • \n
    \n

    还可以直接在 docker commit 命令里指定新的镜像名,这种方式在使用容器提交时会更加方便。

    \n
      \n
    • docker commit -m "Upgrade" webapp webapp:2.0
    • \n
    \n

    导出镜像

    docker save 命令可以将镜像输出,提供了一种让我们保存镜像到 Docker 外部的方式。

    \n
      \n
    • 在默认定义下,docker save 命令会将镜像内容放入输出流中,这就需要我们使用管道进行接收
        \n
      • docker save webapp:1.0 > webapp-1.0.tar
      • \n
      \n
    • \n
    • docker save 命令还为我们提供了 -o 选项,用来指定输出文件,使用这个选项可以让命令更具有统一性。
        \n
      • docker save -o ./webapp-1.0.tar webapp:1.0
      • \n
      \n
    • \n
    \n

    导入镜像

    导入镜像的方式也很简单,使用与 docker save 相对的 docker load 命令即可。

    \n
      \n
    • docker load 命令是从输入流中读取镜像的数据,所以我们这里也要使用管道来传输内容。当然
        \n
      • docker load < webapp-1.0.tar
      • \n
      \n
    • \n
    • 也能够使用 -i 选项指定输入文件。
        \n
      • docker load -i webapp-1.0.tar
      • \n
      \n
    • \n
    • 镜像导入后,我们就可以通过 docker images 看到它了,导入的镜像会延用原有的镜像名称
    • \n
    \n

    批量迁移

    通过 docker savedocker load 命令我们还能够批量迁移镜像,只要我们在 docker save 中传入多个镜像名作为参数,它就能够将这些镜像都打成一个包,便于我们一次性迁移多个镜像。

    \n
      \n
    • docker save -o ./images.tar webapp:1.0 nginx:1.12 mysql:5.7
    • \n
    \n

    导出和导入容器

    使用 docker export 命令我们可以直接导出容器

    \n
      \n
    • 可以把它简单的理解为 docker commitdocker save 的结合体。
    • \n
    • docker export -o ./webapp.tar webapp
    • \n
    \n

    使用 docker export 导出的容器包,使用 docker import 导入。

    \n
      \n
    • 需要注意的是,使用 docker import 并非直接将容器导入,而是将容器运行时的内容以镜像的形式导入。
    • \n
    • docker import 的参数里,我们可以给这个镜像命名。
        \n
      • docker import ./webapp.tar webapp:1.0
      • \n
      \n
    • \n
    \n

    docker export 的应用场景主要用来制作基础镜像,比如你从一个ubuntu镜像启动一个容器,然后安装一些软件和进行一些设置后,使用docker export保存为一个基础镜像。然后,把这个镜像分发给其他人使用,比如作为基础的开发环境。

    \n

    docker savedocker export 的区别:

    \n
      \n
    • docker save 保存的是镜像(Image),docker export 保存的是容器(Container);
    • \n
    • docker load 用来载入镜像包,docker import 用来载入容器包,但两者都会恢复为镜像;
    • \n
    • docker load 不能对载入的镜像重命名,而 docker import 可以为镜像指定新名称。
    • \n
    \n

    通过 Dockerfile 创建镜像

    常见 Dockerfile 指令

    FROM

    通常来说,我们不会从零开始搭建一个镜像,而是会选择一个已经存在的镜像作为我们新镜像的基础,这种方式能够大幅减少我们的时间。

    \n

    通过 FROM 指令指定一个基础镜像,接下来所有的指令都是基于这个镜像所展开的。

    \n

    FROM 指令支持三种形式:

    \n
      \n
    • FROM <image> [AS <name>]
    • \n
    • FROM <image>[:<tag>] [AS <name>]
    • \n
    • FROM <image>[@<digest>] [AS <name>]
    • \n
    \n

    Dockerfile 中的第一条指令必须是 FROM 指令,因为没有了基础镜像,一切构建过程都无法开展。

    \n
      \n
    • 当 FROM 第二次或者之后出现时,表示在此刻构建时,要将当前指出镜像的内容合并到此刻构建镜像的内容里。
    • \n
    \n

    RUN

    在 RUN 指令之后,我们直接拼接上需要执行的命令,在构建时,Docker 就会执行这些命令,并将它们对文件系统的修改记录下来,形成镜像的变化。

    \n
      \n
    • RUN <command>
    • \n
    • RUN ["executable", "param1", "param2"]
    • \n
    • RUN 指令是支持 \\ 换行的,如果单行的长度过长,建议对内容进行切割,方便阅读。
    • \n
    \n

    ENTRYPOINT 和 CMD

    基于镜像启动的容器,在容器启动时会根据镜像所定义的一条命令来启动容器中进程号为 1 的进程。而这个命令的定义,就是通过 Dockerfile 中的 ENTRYPOINTCMD 实现的。

    \n
      \n
    • ENTRYPOINT ["executable", "param1", "param2"]
    • \n
    • ENTRYPOINT command param1 param2
    • \n
    • CMD ["executable","param1","param2"]
    • \n
    • CMD ["param1","param2"]
    • \n
    • CMD command param1 param2
    • \n
    \n

    当 ENTRYPOINT 与 CMD 同时给出时,CMD 中的内容会作为 ENTRYPOINT 定义命令的参数,最终执行容器启动的还是 ENTRYPOINT 中给出的命令。

    \n

    EXPOSE

    通过 EXPOSE 指令可以为镜像指定要暴露的端口。

    \n
      \n
    • EXPOSE <port> [<port>/<protocol>...]
    • \n
    \n

    当我们通过 EXPOSE 指令配置了镜像的端口暴露定义,那么基于这个镜像所创建的容器,在被其他容器通过 --link 选项连接时,就能够直接允许来自其他容器对这些端口的访问了。

    \n

    VOLUME

    在 Dockerfile 里,提供了 VOLUME 指令来定义基于此镜像的容器所自动建立的数据卷。

    \n
      \n
    • VOLUME ["/data"]
    • \n
    • 在 VOLUME 指令中定义的目录,在基于新镜像创建容器时,会自动建立为数据卷,不需要我们再单独使用 -v 选项来配置了。
    • \n
    \n

    COPY 和 ADD

      \n
    • COPY [--chown=<user>:<group>] <src>... <dest>
    • \n
    • ADD [--chown=<user>:<group>] <src>... <dest>
    • \n
    • COPY [--chown=<user>:<group>] ["<src>",... "<dest>"]
    • \n
    • ADD [--chown=<user>:<group>] ["<src>",... "<dest>"]
    • \n
    \n

    COPYADD 指令的定义方式完全一样,需要注意的仅是当我们的目录中存在空格时,可以使用后两种格式避免空格产生歧义

    \n

    ADD 能够支持使用网络端的 URL 地址作为 src 源,并且在源文件被识别为压缩包时,自动进行解压。

    \n

    构建镜像

    构建镜像的命令为 docker build

    \n
      \n
    • docker build ./webapp
    • \n
    • docker build 可以接收一个参数,需要特别注意的是,这个参数为一个目录路径
    • \n
    \n

    在默认情况下,docker build 也会从这个目录下寻找名为 Dockerfile 的文件,将它作为 Dockerfile 内容的来源。如果我们的 Dockerfile 文件路径不在这个目录下,或者有另外的文件名,我们可以通过 -f 选项单独给出 Dockerfile 文件的路径。

    \n
      \n
    • docker build -t webapp:latest -f ./webapp/a.Dockerfile ./webapp
    • \n
    • 在构建时我们最好总是携带上 -t 选项,用它来指定新生成镜像的名称。
    • \n
    \n

    Dockerfile 使用技巧

    构建中使用变量
    在 Dockerfile 里,我们可以用 ARG 指令来建立一个参数变量,我们可以在构建时通过构建指令传入这个参数变量,并且在 Dockerfile 里使用它。

    \n
    FROM debian:stretch-slim

    ## ......

    ARG TOMCAT_MAJOR
    ARG TOMCAT_VERSION

    ## ......

    RUN wget -O tomcat.tar.gz "https://www.apache.org/dyn/closer.cgi?action=download&filename=tomcat/tomcat-$TOMCAT_MAJOR/v$TOMCAT_VERSION/bin/apache-tomcat-$TOMCAT_VERSION.tar.gz"

    ## ......
    \n

    我们可以在构建时通过 docker build 的 –build-arg 选项来设置参数变量

    \n
      \n
    • docker build --build-arg TOMCAT_MAJOR=8 --build-arg TOMCAT_VERSION=8.0.53 -t tomcat:8.0 ./tomcat
    • \n
    \n

    环境变量

    环境变量也是用来定义参数的东西,与 ARG 指令相类似,环境变量的定义是通过 ENV 这个指令来完成的。

    \n
    FROM debian:stretch-slim

    ## ......

    ENV TOMCAT_MAJOR 8
    ENV TOMCAT_VERSION 8.0.53

    ## ......

    RUN wget -O tomcat.tar.gz "https://www.apache.org/dyn/closer.cgi?action=download&filename=tomcat/tomcat-$TOMCAT_MAJOR/v$TOMCAT_VERSION/bin/apache-tomcat-$TOMCAT_VERSION.tar.gz"
    \n

    环境变量与参数变量的区别:

    \n
      \n
    • 环境变量不仅能够影响构建,还能够影响基于此镜像创建的容器。
        \n
      • 环境变量设置的实质,其实就是定义操作系统环境变量,所以在运行的容器里,一样拥有这些变量,而容器中运行的程序也能够得到这些变量的值。
      • \n
      \n
    • \n
    • 环境变量的值不是在构建指令中传入的,而是在 Dockerfile 中编写的,所以如果我们要修改环境变量的值,我们需要到 Dockerfile 修改
    • \n
    • 在创建容器时使用 -e 或是 --env 选项,可以对环境变量的值进行修改或定义新的环境变量。
        \n
      • docker run -e MYSQL_ROOT_PASSWORD=my-secret-pw -d mysql:5.7
      • \n
      \n
    • \n
    • Dockerfile 中的 ENV 指令所定义的变量,永远会覆盖 ARG 所定义的变量
    • \n
    \n

    合并命令

    看似连续的镜像构建过程,其实是由多个小段组成。

    \n
      \n
    • 每当一条能够形成对文件系统改动的指令在被执行前,Docker 先会基于上条命令的结果启动一个容器,在容器中运行这条指令的内容
    • \n
    • 之后将结果打包成一个镜像层,如此反复,最终形成镜像。
    • \n
    \n

    \"\"

    \n
      \n
    • 镜像是由多个镜像层叠加而得,而这些镜像层其实就是在我们 Dockerfile 中每条指令所生成的。
    • \n
    • 将命令合并到一条指令中不但减少了镜像层的数量,也减少了镜像构建过程中反复创建容器的次数,提高了镜像构建的速度。
    • \n
    \n

    构建缓存

    Docker 判断镜像层与之前的镜像间不存在变化的两个维度:

    \n
      \n
    1. 所基于的镜像层是否一样
    2. \n
    3. 用于生成镜像层的指令的内容是否一样
    4. \n
    \n

    我们在条件允许的前提下,更建议将不容易发生变化的搭建过程放到 Dockerfile 的前部,充分利用构建缓存提高镜像构建的速度。

    \n

    另外一些时候,我们可能不希望 Docker 在构建镜像时使用构建缓存,这时我们可以通过 –no-cache 选项来禁用它。

    \n
      \n
    • docker build --no-cache ./webapp
    • \n
    \n

    搭配 ENTRYPOINT 和 CMD

    两个指令的区别在于,ENTRYPOINT 指令的优先级高于 CMD 指令。

    \n
      \n
    • 当 ENTRYPOINT 和 CMD 同时在镜像中被指定时,CMD 里的内容会作为 ENTRYPOINT 的参数,两者拼接之后,才是最终执行的命令。
    • \n
    \n

    ENTRYPOINT 和 CMD 设计的目的不同:

    \n
      \n
    • ENTRYPOINT 指令主要用于对容器进行一些初始化
    • \n
    • CMD 指令则用于真正定义容器中主程序的启动命令
    • \n
    \n

    容器启动时覆盖启动命令也只是覆盖 CMD 中定义的内容,不会影响 ENTRYPOINT 中的内容。

    \n

    使用脚本文件来作为 ENTRYPOINT 的内容是常见的做法,因为对容器运行初始化的命令相对较多,全部直接放置在 ENTRYPOINT 后会特别复杂:

    \n
    ## ......

    COPY docker-entrypoint.sh /usr/local/bin/

    ENTRYPOINT ["docker-entrypoint.sh"]

    ## ......

    CMD ["redis-server"]
    \n

    Redis 中的 ENTRYPOINT 脚本:

    \n
    #!/bin/sh
    set -e

    # first arg is `-f` or `--some-option`
    # or first arg is `something.conf`
    if [ "${1#-}" != "$1" ] || [ "${1%.conf}" != "$1" ]; then
    \tset -- redis-server "$@"
    fi

    # allow the container to be started with `--user`
    if [ "$1" = 'redis-server' -a "$(id -u)" = '0' ]; then
    \tfind . \\! -user redis -exec chown redis '{}' +
    \texec gosu redis "$0" "$@"
    fi

    exec "$@"
    \n

    在很多镜像的 ENTRYPOINT 脚本里,我们都会看到 exec "$@" 命令,其作用其实很简单,就是运行一个程序,而运行命令就是 ENTRYPOINT 脚本的参数

    \n
      \n
    • 由于 ENTRYPOINT 脚本的参数就是 CMD 指令中的内容,所以实际执行的就是 CMD 里的命令
    • \n
    • 所以说,虽然 Docker 对容器启动命令的结合机制为 CMD 作为 ENTRYPOINT 的参数,合并后执行 ENTRYPOINT 中的定义,但实际在我们使用中,我们还会在 ENTRYPOINT 的脚本里代理到 CMD 命令上
    • \n
    \n

    另外一篇 Dockerfile 最佳实践的文章:https://www.practicemp.com/2018/10/docker-best-practices-for-writing-dockerfiles.html

    \n

    使用 Docker Hub 中的镜像

    选择镜像与程序版本

    对于一些复杂的应用,除了版本外,还存在很多的变量,镜像的维护者们也喜欢将这些变量一同组合到镜像的 Tag 里,所以我们在使用镜像前,一定要先了解不同 Tag 对应的不同内容。

    \n

    \"\"

    \n
      \n
    • 通常来说,镜像的维护者会在镜像介绍中展示出镜像所有的 Tag,如果没有,我们也能够从页面上的 Tags 导航里进入到镜像标签列表页面。
    • \n
    • 在 OpenJDK 镜像的 Tag 列表里,我们可以看到同样版本号的镜像就存在多种标签。在这些不同的标签上,除了定义 OpenJDK 的版本,还有操作系统,软件提供者等信息。
    • \n
    • 镜像维护者为我们提供这么多的标签进行选择,其实方便了我们在不同场景下选择不同环境实现细节时,都能直接用到这个镜像,而不需要再单独编写 Dockerfile 并构建。
    • \n
    \n

    Alpine 镜像

    镜像标签中的 Alpine 指的是这个镜像内的文件系统内容,是基于 Alpine Linux 这个操作系统的。

    \n
      \n
    • Alpine Linux 是一个相当精简的操作系统,而基于它的 Docker 镜像可以仅有数 MB 的尺寸。
    • \n
    \n

    Alpine 镜像的缺点就在于它实在过于精简

    \n
      \n
    • 在 Alpine 中缺少很多常见的工具和类库
        \n
      • 以至于如果我们想基于软件 Alpine 标签的镜像进行二次构建,那搭建的过程会相当烦琐。
      • \n
      \n
    • \n
    • 所以想要对软件镜像进行改造,并基于其构建新的镜像,那么 Alpine 镜像不是一个很好的选择
        \n
      • 提倡基于 Ubuntu、Debian、CentOS 这类相对完整的系统镜像来构建
      • \n
      \n
    • \n
    \n

    使用 Docker Compose 管理容器

    Docker Compose

    \"\"

    \n

    如果说 Dockerfile 是将容器内运行环境的搭建固化下来,那么 Docker Compose 我们就可以理解为将多个容器运行的方式和配置固化下来。

    \n

    启动和停止

    最常使用的 Docker Compose 命令就是 docker-compose updocker-compose down 了。

    \n

    docker-compose up 命令类似于 Docker Engine 中的 docker run,它会根据 docker-compose.yml 中配置的内容,创建所有的容器、网络、数据卷等等内容,并将它们启动。

    \n
      \n
    • docker-compose up -d
    • \n
    • docker run 一样,默认情况下 docker-compose up 会在“前台”运行,我们可以用 -d 选项使其“后台”运行。
    • \n
    • docker-compose 命令默认会识别当前控制台所在目录内的 docker-compose.yml 文件,而会以这个目录的名字作为组装的应用项目的名称。
        \n
      • 可以通过选项 -f 来修改识别的 Docker Compose 配置文件,通过 -p 选项来定义项目名。
      • \n
      • docker-compose -f ./compose/docker-compose.yml -p myapp up -d
      • \n
      \n
    • \n
    \n

    docker-compose up 相反,docker-compose down 命令用于停止所有的容器,并将它们删除,同时消除网络等配置内容

    \n
      \n
    • docker-compose down
    • \n
    • 也就是几乎将这个 Docker Compose 项目的所有影响从 Docker 中清除
    • \n
    \n

    指定镜像

    在 Docker Compose 里,可以通过两种方式为服务指定所采用的镜像。

    \n
      \n
    1. 通过 image 这个配置
        \n
      • 给出能在镜像仓库中找到镜像的名称即可
      • \n
      \n
    2. \n
    3. 直接采用 Dockerfile 来构建镜像
        \n
      • 通过 build 这个配置我们能够定义构建的环境目录
      • \n
      • 如果通过这种方式指定镜像,那么 Docker Compose 先会帮助我们执行镜像的构建,之后再通过这个镜像启动容器。
      • \n
      \n
    4. \n
    \n

    在配置文件里,我们还能用 Map 的形式来定义 build,在这种格式下,我们能够指定更多的镜像构建参数,例如 Dockerfile 的文件名,构建参数等等:

    \n
    ## ......
    webapp:
    build:
    context: ./webapp
    dockerfile: webapp-dockerfile
    args:
    - JAVA_VERSION=1.6
    ## ......
    \n

    依赖声明

    如果我们的服务间有非常强的依赖关系,就必须告知 Docker Compose 容器的先后启动顺序。

    \n
      \n
    • 只有当被依赖的容器完全启动后,Docker Compose 才会创建和启动这个容器。
    • \n
    • 定义依赖的方式很简单,只需要通过 depends_on 列出这个服务所有依赖的其他服务即可
    • \n
    • Docker Compose 为我们启动项目的时候,会检查所有依赖,形成正确的启动顺序并按这个顺序来依次启动容器。
    • \n
    \n

    文件挂载

    使用 volumes 配置可以像 docker CLI 里的 -v 选项一样来指定外部挂载和数据卷挂载。

    \n

    在使用外部文件挂载的时候,我们可以直接指定相对目录进行挂载,这里的相对目录是指相对于 docker-compose.yml 文件的目录。

    \n
      \n
    • 由于有相对目录这样的机制,可以将 docker-compose.yml 和所有相关的挂载文件放置到同一个文件夹下,形成一个完整的项目文件夹。
        \n
      • 这样既可以很好的整理项目文件,也利于完整的进行项目迁移。
      • \n
      \n
    • \n
    • 在开发时,推荐直接将代码挂载到容器里,而不是通过镜像构建的方式打包成镜像。
    • \n
    • 在开发过程中,对于程序的配置等内容,也建议直接使用文件挂载的形式挂载到容器里,避免经常修改所带来的麻烦。
    • \n
    \n

    使用数据卷

    如果我们要在项目中使用数据卷来存放特殊的数据,我们也可以让 Docker Compose 自动完成对数据卷的创建,而不需要我们单独进行操作。

    \n

    在上面的例子里,独立于 servicesvolumes 配置就是用来声明数据卷的。定义数据卷最简单的方式仅需要提供数据卷的名称。

    \n

    如果我们想把属于 Docker Compose 项目以外的数据卷引入进来直接使用,我们可以将数据卷定义为外部引入,通过 external 这个配置就能完成这个定义。

    \n
    ## ......
    volumes:
    mysql-data:
    external: true
    ## ......
    \n

    在加入 external 定义后,Docker Compose 在创建项目时不会直接创建数据卷,而是优先从 Docker Engine 中已有的数据卷里寻找并直接采用。

    \n

    配置网络

    在 Docker Compose 里,我们可以为整个应用系统设置一个或多个网络。

    \n

    声明网络的配置同样独立于 services 存在,是位于根配置下的 networks 配置。

    \n

    除了简单的声明网络名称,让 Docker Compose 自动按默认形式完成网络配置外,我们还可以显式的指定网络的参数。

    \n
    networks:
    frontend:
    driver: bridge
    ipam:
    driver: default
    config:
    - subnet: 10.10.1.0/24
    ## ......
    \n

    在这里,我们为网络定义了网络驱动的类型,并指定了子网的网段。

    \n

    使用网络别名

    网络别名的定义方式很简单,这里需要将之前简单的网络 List 定义结构修改成 Map 结构,以便在网络中加入更多的定义。

    \n
    ## ......
    database:
    networks:
    backend:
    aliases:
    - backend.database
    ## ......
    webapp:
    networks:
    backend:
    aliases:
    - backend.webapp
    frontend:
    aliases:
    - frontend.webapp
    ## ......
    \n

    在进行这样的配置后,便可以使用这里所设置的网络别名对其他容器进行访问了。

    \n

    端口映射

    ports 配置项,是用来定义端口映射的。可以利用它进行宿主机与容器端口的映射,这个配置与 docker CLI 中 -p 选项的使用方法是近似的。

    \n

    需要注意的是,由于 YAML 格式对 xx:yy 这种格式的解析有特殊性,在设置小于 60 的值时,会被当成时间而不是字符串来处理,所以我们最好使用引号将端口映射的定义包裹起来,避免歧义。

    \n
      \n
    • "8080:8080"
    • \n
    \n

    重启机制

    restart 配置主要是用来控制容器的重启策略的。

    \n

    restart 选项:

    \n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n
    配置值说明
    no不设重启机制
    always总是重启
    on-failure在异常退出时重启
    unless-stopped除非由停止命令结束,其他情况都重启
    \n

    应用于服务化开发

    \"\"

    \n

    Overlay Network 能够跨越物理主机的限制,让多个处于不同 Docker daemon 实例中的容器连接到同一个网络,并且让这些容器感觉这个网络与其他类型的网络没有区别。

    \n
      \n
    • 要搭建 Overlay Network 网络,我们就要用到 Docker Swarm 这个工具了。
    • \n
    \n

    Docker Swarm

    Docker Swarm 是 Docker 内置的集群工具,它能够帮助我们更轻松地将服务部署到 Docker daemon 的集群之中。

    \n

    \"\"

    \n

    在真实的服务部署里,我们通常是使用 Docker Compose 来定义集群,而通过 Docker Swarm 来部署集群。

    \n
      \n
    • 对于 Docker Swarm 来说,每一个 Docker daemon 的实例都可以成为集群中的一个节点
    • \n
    • 在 Docker daemon 加入到集群成为其中的一员后,集群的管理节点就能对它进行控制。
    • \n
    • 我们要搭建的 Overlay 网络正是基于这样的集群实现的。
    • \n
    \n

    我们在任意一个 Docker 实例上都可以通过 docker swarm init 来初始化集群。

    \n
    $ docker swarm init

    Swarm initialized: current node (t4ydh2o5mwp5io2netepcauyl) is now a manager.

    To add a worker to this swarm, run the following command:

    docker swarm join --token SWMTKN-1-4dvxvx4n7magy5zh0g0de0xoues9azekw308jlv6hlvqwpriwy-cb43z26n5jbadk024tx0cqz5r 192.168.1.5:2377
    \n

    在集群初始化后,这个 Docker 实例就自动成为了集群的管理节点,而其他 Docker 实例可以通过运行这里所打印的 docker swarm join 命令来加入集群。

    \n

    加入到集群的节点默认为普通节点,如果要以管理节点的身份加入到集群中

    \n
      \n
    • 可以通过 docker swarm join-token 命令来获得管理节点的加入命令。
    • \n
    \n
    $ docker swarm join-token manager
    To add a manager to this swarm, run the following command:

    docker swarm join --token SWMTKN-1-60am9y6axwot0angn1e5inxrpzrj5d6aa91gx72f8et94wztm1-7lz0dth35wywekjd1qn30jtes 192.168.1.5:2377
    \n

    建立跨主机网络

    通过 docker network create 命令来建立 Overlay 网络。

    \n

    docker network create --driver overlay --attachable mesh

    \n
      \n
    • 在创建 Overlay 网络时,我们要加入 --attachable 选项以便不同机器上的 Docker 容器能够正常使用到它。
    • \n
    • 在创建了这个网络之后,我们可以在任何一个加入到集群的 Docker 实例上使用 docker network ls 查看一下其下的网络列表。
        \n
      • 会发现这个网络定义已经同步到了所有集群中的节点上。
      • \n
      \n
    • \n
    \n

    将网络的 external 属性设置为 true,就可以让 Docker Compose 将其建立的容器都连接到这个不属于 Docker Compose 的项目上了。

    \n
    networks:
    mesh:
    external: true
    \n

    准备程序配置

    我们常用下列几种方式来获得程序的配置文件:

    \n
      \n
    • 借助配置文档直接编写
    • \n
    • 下载程序源代码中的配置样例
    • \n
    • 通过容器中的默认配置获得
    • \n
    \n

    借助配置文档直接编写

    MySQL 文档中关于配置文件的参考:
    https://dev.mysql.com/doc/refman/5.7/en/server-options.html

    \n
      \n
    • 使用软件的文档来编写配置文件,其优势在于在编写的过程实际上也是我们熟悉软件的过程,通过配置加文档形式的阅读,你一定会从中收获很多。
    • \n
    • 这种方法也有很大的劣势,即需要仔细阅读文档,劳神劳力,对于常规开发中的使用来说,成效比很低。
    • \n
    \n

    下载程序源代码中的配置样例

    大部分软件,特别是开源软件都会直接给出一份示例配置文件作为参考。 我们可以直接拿到这份配置,达到我们的目的。

    \n
      \n
    • 在 Redis 源代码中,就包含了一份默认的配置文件,我们可以直接拿来使用:https://github.com/antirez/redis/blob/3.2/redis.conf
    • \n
    • 相对于通过配置文档获得配置,从配置示例里获得配置要来得更为简单容易。
    • \n
    \n

    通过容器中的默认配置获得

    大多数 Docker 镜像为了实现自身能够直接启动为容器并马上提供服务,会把默认配置直接打包到镜像中,以便让程序能够直接读取。

    \n
      \n
    • 所以说,我们可以直接从镜像里拿到这份配置,拷贝到宿主机里备用。
    • \n
    \n

    以 Tomcat 为例,说说如何从 Tomcat 镜像里拿到配置文件:

    \n
      \n
    1. 要拿到 Tomcat 中的配置文件,我们需要先创建一个临时的 Tomcat 容器。
        \n
      • docker run --rm -d --name temp-tomcat tomcat:8.5
      • \n
      \n
    2. \n
    3. 对于 Tomcat 来说,在开发过程中我们可能会经常改动的配置主要是 server.xmlweb.xml 这两个文件,所以接下来我们就把这两个文件从容器中复制到宿主机里。
        \n
      • docker cp temp-tomcat:/usr/local/tomcat/conf/server.xml ./server.xml
      • \n
      • docker cp temp-tomcat:/usr/local/tomcat/conf/web.xml ./web.xml
      • \n
      \n
    4. \n
    5. 完成上面的操作后清理我们创建的临时容器
        \n
      • docker stop temp-tomcat
      • \n
      • 由于我们在创建临时容器的时候增加了 --rm 选项,所以我们在这里只需要使用 docker stop 停止容器,就可以在停止容器的同时直接删除容器,实现直接清理的目的。
      • \n
      \n
    6. \n
    \n

    docker 和 docker-compose 命令手册:

    \n"},{"title":"docker 使用笔记","url":"/2018/docker-%E4%BD%BF%E7%94%A8%E7%AC%94%E8%AE%B0/","content":"

    常用启动命令

    docker run -d -p 5001:5000 --rm --name xxx jiapan/some-image:label
    \n

    编译镜像

    docker build -t jiapan/some-image:label .
    \n

    推送镜像到仓库

    docker push jiapan/some-image:label
    \n

    进入正在运行的docker

    docker exec -it <container_name> /bin/bash
    \n

    更改镜像时区(适用于 Ubuntu)

    Dockerfile 内添加:

    \n
    RUN echo "Asia/Shanghai" > /etc/timezone
    RUN dpkg-reconfigure -f noninteractive tzdata
    \n","tags":["笔记"]},{"title":"Dockerfile 下 ADD 与 COPY 的区别","url":"/2019/dockerfile-add-vs-copy/","content":"

    \"\"

    \n

    COPYADD 都是 Dockerfile 中的指令,有着类似的作用。它们允许我们将文件从特定位置复制到 Docker 镜像中。

    \n

    COPY

    COPY 指令从 <src> 复制新的文件或目录,并将它们添加到 Docker 容器文件系统的 <dest> 的路径下。

    \n

    COPY 有两种格式:

    \n
      \n
    • COPY [--chown=<user>:<group>] <src>... <dest>
    • \n
    • COPY [--chown=<user>:<group>] ["<src>",... "<dest>"](包含空格的路径使用这种格式)
    • \n
    \n

    ADD

    ADD 有两种格式:

    \n
      \n
    • ADD [--chown=<user>:<group>] <src>... <dest>
    • \n
    • ADD [--chown=<user>:<group>] ["<src>",... "<dest>"](包含空格的路径使用这种格式)
    • \n
    \n

    从 URL 复制的 Dockerfile 最佳实践

    通过 URL 进行复制的效率通常很低,最佳实践是使用其他策略来包含所需的远程文件。

    \n

    COPY 只支持基础的复制:将本地文件复制到容器中。

    \n

    而 ADD 有一些额外的功能 :

    \n
      \n
    • ADD 指令可以让你使用 URL 作为 <src> 参数。当遇到 URL 时候,可以通过 URL 下载文件并且复制到 <dest>
    • \n
    • ADD 的另一个特性是自动解压文件的能力。如果 <src> 参数是一个可识别压缩格式(tar, gzip, bzip2, …)的本地文件注:无法实现同时下载并解压),就会被解压到指定容器文件系统的路径 <dest> 下。
    • \n
    \n

    因此,ADD 的最佳用途是将本地压缩包文件自动提取到镜像中:

    \n
    ADD code.tar.gz /app/
    \n

    由于镜像的体积很重要,所以强烈建议不要使用 ADD 从远程 URL 获取文件,我们应该使用 curlwget 来代替。这样我们可以在解压后删除这些不再需要的文件,同时还也可以避免在镜像中生成额外的层。

    \n

    我们应该避免以下操作:

    \n
    ADD http://example.com/big.tar.xz /usr/src/things/
    RUN tar -xJf /usr/src/things/big.tar.xz -C /usr/src/things \\
    && make -C /usr/src/things all \\
    && rm -f /usr/src/things/big.tar.xz
    \n

    这个压缩包解压后,rm 命令处于独立的镜像层。

    \n

    我们可以这样做:

    \n
    RUN mkdir -p /usr/src/things \\
    && curl -SL http://example.com/big.tar.xz \\
    | tar -xJC /usr/src/things \\
    && make -C /usr/src/things all
    \n

    curl 会下载这个压缩包并通过管道传给 tar 命令进行解压,这样也就不会在文件系统中留下这个压缩文件了。

    \n

    对于不需要自动解压的文件或目录,应该始终使用 COPY

    \n

    最后,认准一个原则:总是使用 COPY(除非我们明确需要 ADD)。

    \n"},{"title":"医生不一致的口径","url":"/2022/doctor-Inconsistent-caliber/","content":"

    今年年初我确诊了桥本甲状腺炎,其实好几年前在体检时就发现了甲状腺有问题,但一直都没有去医院复查过,也错过了最佳干预时间。

    \n

    我是去的家附近的一家三甲医院,前几次都是找的同一个医生,刚开始她一直给我开的甲功七项检查单,大概检查几次抽了几次血后,她确诊我是甲状腺功能减退症,开始让我服用优甲乐,从半片开始吃。一周后复查,还是查的甲功七项,优甲乐计量改为一片,之后又找这个医生查了一次,依旧开甲功七项,药的计量加到了一片半,并且我基本每次找这个医生的时候都会问,有没有什么注意事项,这个医生说没有,每次开的单子上,描述处也都是写的甲状腺功能减退,从来没用过桥本甲状腺炎这个词。中间我有一次好奇,就问医生桥本甲状腺炎和我的病是什么关系,医生说是一个意思。

    \n

    后来有一次我去医院,刚好之前一直看的医生不在,我找了另一个医生,她给我开的化验单是甲功三项,而且跟我说了很多注意事项,比如不能吃海带、饮食清淡。临走时我问她坐诊时间是哪几天,她说不用必须找她,其他医生也可以,但之前那个医生跟我说了她哪天当班,让我固定找她治疗。

    \n

    再往后我就什么时候有时间什么时候挂号复查,每次遇到的医生也不一样,我发现医生的口径各有区别,比如:

    \n
      \n
    • 这个医生说 2 周后来抽血复查、那个医生说 3-4 周来一次就行;
    • \n
    • 这个医生开甲功三项的检查单、那个医生开甲功五项、另一个医生开甲功七项;
    • \n
    • 这个医生说没有什么要注意的、另一个医生让我注意这个注意那个;
    • \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\n

    4k 左右的下载速度。

    \n

    可以通过阿里提供的代理来解决,只需把 rep 地址中的 github.com 替换为 github.com.cnpmjs.org/ 就可以了。

    \n

    比如,之前的地址是:https://github.com/gin-gonic/gin,替换后为:https://github.com.cnpmjs.org/gin-gonic/gin

    \n

    效果如下:

    \n
    \"\"
    \n\n

    虽然没有快的飞起,但是已经相当不错了。

    \n
    \n

    P.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 作为演示镜像:

    \n
    ➜ docker run -it openjdk:11 bash
    root@d0fb3a0b527c:/# vi Lol.java
    bash: vi: command not found
    root@d0fb3a0b527c:/#
    \n

    下面介绍五种常用方法:

    方法1:使用挂载

    准备 Dockerfile:

    \n
    FROM openjdk:11
    WORKDIR "/app"
    \n

    编译镜像:

    \n
    docker build -t lol .
    \n

    最后,运行带有挂载的容器:

    \n
    docker run --rm -it --name=lol -v $PWD/app-vol:/app lol bash
    \n

    如果本地 $PWD/app-vol 目录不存在,会被自动创建。此后在 $PWD/app-vol 下的文件操作会映射在容器的 /app 目录下。

    \n

    方法2:安装编辑器

    docker run --rm -it --name=lol lol bash

    root@4b72fbabb0af:/app# apt-get update
    root@4b72fbabb0af:/app# apt-get -y install vim
    \n

    如果需要重复使用,更好的做法是写在 Dockerfile 中:

    \n
    FROM openjdk:11
    RUN ["apt-get", "update"]
    RUN ["apt-get", "-y", "install", "vim"]
    WORKDIR "/app"
    \n

    方法3:将文件拷贝到正在运行的容器中

    docker cp Lol.java lol:/app
    \n

    另一个与之类似的方法是将 docker exec 和 cat 结合使用,下边的命令同样把 Lol.java 文件复制到了正在运行的容器中:

    \n
    docker exec -i lol sh -c 'cat > /app/Lol.java' < Lol.java
    \n

    方法4:使用 Linux 工具

    虽然容器中通常没有安装编辑工具,但是其他 Linux 工具,如:sed, awk, echo, cat, cut 等是具备的,可以派上用场。比如 sed 和 awk 可以编辑文件的适当位置,还可以将 echo, cat, cut 联合起来并借助强大的重定向流创建和编辑文件。正如前文所示,这些工具可以与 docker exec 命令结合使用,从而发挥更强大的威力。

    \n

    方法5:使用远程 vim(或其他编辑器)

    这种方法只是为了开拓思路,并不会在实际中使用。

    \n

    修改 Dockerfile:

    \n
    FROM openjdk:11
    RUN ["apt-get", "update"]
    RUN ["apt-get", "install", "-y", "openssh-server"]
    RUN mkdir /var/run/sshd
    RUN echo 'root:lollol0' | chpasswd
    RUN sed -i 's/#PermitRootLogin prohibit-password/PermitRootLogin yes/' /etc/ssh/sshd_config
    RUN ["/etc/init.d/ssh", "start"]
    EXPOSE 22
    WORKDIR "/app"
    CMD ["/usr/sbin/sshd", "-D"]
    \n

    因为我们要借助 scp 来远程进行文件编辑,所以需要安装 openssh-server 并开放其端口。

    \n

    编译并运行:

    \n
    docker build -t lol .
    docker run --rm -p 2222:22 -d --name=lol lol
    \n

    现在我们可以使用以下命令来编辑 Lol.java 文件了:

    \n
    vim scp://root@localhost:2222//app/Lol.java
    \n

    注:在 vi 中需要先执行 :set bt=acwrite 命令再去编辑文件,相关讨论见:https://github.com/vim/vim/issues/2329

    \n

    编辑完成保存并退出后,可以使用下边的命令来验证文件确实被创建和保存了:

    \n
    docker exec -it lol cat /app/Lol.java
    \n"},{"title":"Effective Go 查漏补缺","url":"/2021/effective-go-read/","content":"
    \n

    前几天把 Effective Go 这本小书读了一下,里边有些比较生疏或者实用的知识点,在此记录。

    \n
    \n

    命名

    包应当以小写的单个单词来命名,且不应使用下划线或驼峰记法。

    \n

    另一个约定就是包名应为其源码目录的基本名称。在 src/pkg/encoding/base64 中的包应作为 “encoding/base64” 导入,其包名应为 base64, 而非 encoding_base64encodingBase64

    \n
    \n

    我们的代码中给包起别名时也应该遵循这个规则,即:livedomain “gitlab.xxx.com/backend/xxx-live/proto”,不应该是 live_domain

    \n
    \n

    长命名并不会使其更具可读性。一份有用的说明文档通常比额外的长名更有价值。

    \n
    \n

    避免 Java 那样的长命名

    \n
    \n

    若你有个名为 owner (小写,未导出)的字段,其获取器应当名为 Owner(大写,可导出)而非 GetOwner。大写字母即为可导出的这种规定为区分方法和字段提供了便利。 若要提供设置器方法,SetOwner 是个不错的选择。两个命名看起来都很合理:

    \n
    owner := obj.Owner()
    if owner != user {
    obj.SetOwner(user)
    }
    \n
    \n

    Go 中的 Set 方法无需以 Set 开头,只需实现一个大写开头的方法就可以了。(不过大部分常见下,变量可以直接用导出的)

    \n
    \n

    按照约定,只包含一个方法的接口应当以该方法的名称加上 er 后缀来命名,如 Reader、Writer、 Formatter、CloseNotifier 等。

    \n
    \n

    自己实现接口时也尽量遵循这个规范。

    \n
    \n

    Go 中约定使用驼峰记法 MixedCaps 或 mixedCaps。

    \n
    \n

    即便是常量也不例外,即:不应该写为 LIVE_USER_TABLE 而应该是 LiveUserTable。

    \n
    \n

    分号

    若在新行前的最后一个标记为标识符(包括 int 和 float64 这类的单词)、数值或字符串常量之类的基本字面或以下标记之一,词法分析器会使用一条简单的规则来自动插入分号,因此因此源码中基本就不用分号了。

    \n
    break continue fallthrough return ++ -- ) }
    \n
    \n

    所以

    \n
    \n
    if a == 1 && 
    b == 2
    \n
    \n

    可以编译通过

    \n
    \n
    if a == 1 
    && b == 2
    \n
    \n

    不能编译通过,因为词法分析器会自动在 if a == 1 后边插入分号。

    \n
    \n

    通常Go程序只在诸如 for 循环子句这样的地方使用分号,以此来将初始化器、条件及增量元素分开。如果你在一行中写多个语句,也需要用分号隔开。

    \n
    \n

    for i := 0; i <= 10; i++

    \n
    \n

    无论如何,你都不应将一个控制结构(if、for、switch 或 select)的左大括号放在下一行。如果这样做,就会在大括号前面插入一个分号,这可能引起不需要的效果。 你应该这样写

    \n
    if i < f() {
    g()
    }
    \n

    控制结构

    Go 不再使用 do 或 while 循环,只有一个更通用的 for;switch 要更灵活一点;if 和 switch 像 for 一样可接受可选的初始化语句; 此外,还有一个包含类型选择和多路通信复用器的新控制结构:select。

    \n

    Go 的 for 循环类似于 C,但却不尽相同。它统一了 for 和 while,不再有 do-while 了。它有三种形式,但只有一种需要分号。

    \n
    // Like a C for
    for init; condition; post { }

    // Like a C while
    for condition { }

    // Like a C for(;;)
    for { }
    \n
    \n

    体现出 go 的简洁,不用费心的去考虑应该用 for 还是while 或者 do while。

    \n
    \n

    由于 if 和 switch 可接受初始化语句, 因此用它们来设置局部变量十分常见。

    \n
    if err := file.Chmod(0664); err != nil {
    log.Print(err)
    return err
    }
    \n

    switch 并不会自动下溯,但 case 可通过逗号分隔来列举相同的处理条件。

    \n
    func shouldEscape(c byte) bool {
    switch c {
    case ' ', '?', '&', '=', '#', '+', '%':
    return true
    }
    return false
    }
    \n
    \n

    不用担心因为漏写 break 而导致的bug,case 中支持多个判断条件也很实用。

    \n
    \n

    尽管它们在 Go 中的用法和其它类 C 语言差不多,但 break 语句可以使 switch 提前终止。不仅是 switch, 有时候也必须打破层层的循环。在 Go 中,我们只需将标签放置到循环外,然后 “蹦” 到那里即可

    \n
    func main() {
    \tm, n := 2,2
    loop:\t
    \tfor i := 0; i < n; i++ {
    \t\tfor j:=0; j< m; j++ {
    \t\t\tif j ==1 {
    \t\t\t\tbreak loop
    \t\t\t}
    \t\t\tfmt.Println(i,j)
    \t\t}
    \t}
    \tfmt.Println(\"done\")
    }
    // output:
    // 0 0
    // done
    \n
    \n

    这种用法很少使用,我之前甚至不知道有这种 label break 的用法,类似于其他语言中的 goto。

    \n
    \n

    switch 也可用于判断接口变量的动态类型。

    \n
    var t interface{}
    t = functionOfSomeType()
    switch t := t.(type) {
    default:
    fmt.Printf(\"unexpected type %T\", t) // %T 输出 t 是什么类型
    case bool:
    fmt.Printf(\"boolean %t\\n\", t) // t 是 bool 类型
    case int:
    fmt.Printf(\"integer %d\\n\", t) // t 是 int 类型
    case *bool:
    fmt.Printf(\"pointer to boolean %t\\n\", *t) // t 是 *bool 类型
    case *int:
    fmt.Printf(\"pointer to integer %d\\n\", *t) // t 是 *int 类型
    }
    \n
    \n

    我们的工具库中也有这样的用法,比如将一个 interface{} 类型转为 int64类型,代码如下:

    \n
    \n
    func Int64(num interface{}, defaultValue ...int64) int64 {

    \tvar rsp int64
    \tvar err error

    \tswitch t := num.(type) {
    \tcase string:
    \t\trsp, err = strconv.ParseInt(t, 10, 64)
    \tcase int:
    \t\trsp = int64(t)
    \tcase int8:
    \t\trsp = int64(t)
    \tcase int16:
    \t\trsp = int64(t)
    \tcase int32:
    \t\trsp = int64(t)
    \tcase int64:
    \t\trsp = t
    \tdefault:
    \t}
    \tif err != nil {
    \t\tif len(defaultValue) > 0 {
    \t\t\treturn defaultValue[0]
    \t\t}
    \t}
    \treturn rsp
    }
    \n

    函数

    Go 与众不同的特性之一就是函数和方法可返回多个值。这种形式可以改善 C 中一些笨拙的习惯: 将错误值返回(例如用 -1 表示 EOF)和修改通过地址传入的实参。

    \n
    \n

    Java 中由于也不支持多返回值,也经常将引用传入一个方法,方法执行完后根据传入引用中的数据进行后续处理,这种方法通常被称为有副作用的方法。

    \n
    \n

    Go 函数的返回值或结果 “形参” 可被命名,并作为常规变量使用,就像传入的形参一样。 命名后,一旦该函数开始执行,它们就会被初始化为与其类型相应的零值; 若该函数执行了一条不带实参的 return 语句,则结果形参的当前值将被返回。

    \n

    此名称不是强制性的,但它们能使代码更加简短清晰:它们就是文档。若我们命名了 nextInt 的结果,那么它返回的 int 就值如其意了。

    \n
    \n

    避免在函数签名上命名返回值变量,除非无法从上下中判断返回值的含义用作文档用途,或者希望在 defer 中改变变量值

    \n
    \n

    Go 的 defer 语句用于预设一个函数调用(即推迟执行函数),该函数会在执行 defer 的函数返回之前立即执行。它显得非比寻常, 但却是处理一些事情的有效方式,例如无论以何种路径返回,都必须释放资源的函数。 典型的例子就是解锁互斥和关闭文件。

    \n
    // Contents returns the file's contents as a string.
    func Contents(filename string) (string, error) {
    f, err := os.Open(filename)
    if err != nil {
    return \"\", err
    }
    defer f.Close() // f.Close will run when we're finished.

    var result []byte
    buf := make([]byte, 100)
    for {
    n, err := f.Read(buf[0:])
    result = append(result, buf[0:n]...) // append is discussed later.
    if err != nil {
    if err == io.EOF {
    break
    }
    return \"\", err // f will be closed if we return here.
    }
    }
    return string(result), nil // f will be closed if we return here.
    }
    \n
    \n

    类似于 Java 中的 finally。

    \n
    \n

    推迟诸如 Close 之类的函数调用有两点好处:

    \n
      \n
    • 第一, 它能确保你不会忘记关闭文件。如果你以后又为该函数添加了新的返回路径时, 这种情况往往就会发生。
    • \n
    • 第二,它意味着 “关闭” 离 “打开” 很近, 这总比将它放在函数结尾处要清晰明了。
    • \n
    \n

    被推迟的函数按照后进先出(LIFO)的顺序执行,我们可以充分利用这个特点,即被推迟函数的实参在 defer 执行时才会被求值。 跟踪例程可针对反跟踪例程设置实参。以下例子:

    \n
    func trace(s string) string {
    fmt.Println(\"entering:\", s)
    return s
    }

    func un(s string) {
    fmt.Println(\"leaving:\", s)
    }

    func a() {
    defer un(trace(\"a\"))
    fmt.Println(\"in a\")
    }

    func b() {
    defer un(trace(\"b\"))
    fmt.Println(\"in b\")
    a()
    }

    func main() {
    b()
    }
    \n

    输出:

    \n
    entering: b
    in b
    entering: a
    in a
    leaving: a
    leaving: b
    \n

    数据

    new 是个用来分配内存的内建函数, 但与其它语言中的同名函数不同,它不会初始化内存,只会将内存置零。 也就是说,new(T) 会为类型为 T 的新项分配已置零的内存空间, 并返回它的地址,也就是一个类型为 *T 的值。用 Go 的术语来说,它返回一个指针, 该指针指向新分配的,类型为 T 的零值。

    \n

    表达式 new(File)&File{} 是等价的。

    \n
    \n

    开发时更常用到的是 &File{} 这种形式,因为可以同时对成员进行初始化。

    \n
    \n

    复合字面的字段必须按顺序全部列出。但如果以 字段: 值 对的形式明确地标出元素,初始化字段时就可以按任何顺序出现,未给出的字段值将赋予零值。

    \n

    内建函数 make(T, args) 的目的不同于 new(T)。它只用于创建切片、映射和信道,并返回类型为 T(而非 *T)的一个已初始化 (而非置零)的值。 出现这种用差异的原因在于,这三种类型本质上为引用数据类型,它们在使用前必须初始化。

    \n

    make 只适用于映射、切片和信道且不返回指针。若要获得明确的指针, 请使用 new 分配内存。

    \n
    \n

    这就是 slice, map, channel 需要使用 make 进行初始化的原因。

    \n
    \n

    映射可使用一般的复合字面语法进行构建,其键-值对使用冒号分隔,因此可在初始化时很容易地构建它们。

    \n
    var timeZone = map[string]int{
    \"UTC\": 0*60*60,
    \"EST\": -5*60*60,
    \"CST\": -6*60*60,
    \"MST\": -7*60*60,
    \"PST\": -8*60*60,
    }
    \n
    \n

    map 可以在初始化时同时赋值,很方便。

    \n
    \n

    集合可实现成一个值类型为 bool 的映射。将该映射中的项置为 true 可将该值放入集合中,此后通过简单的索引操作即可判断是否存在。

    \n
    attended := map[string]bool{
    \"Ann\": true,
    \"Joe\": true,
    ...
    }

    if attended[person] { // will be false if person is not in the map
    fmt.Println(person, \"was at the meeting\")
    }
    \n
    \n

    Go 中没有 Set,可以用这种方法代替,有些人习惯将 map 的 value 值声明为 interface 类型,我个人不是很喜欢,bool 更方便使用一些。

    \n
    \n

    在使用 map 时,有时你需要区分某项是不存在还是其值为零值。如对于一个值本应为零的 “UTC” 条目,也可能是由于不存在该项而得到零值。你可以使用多重赋值的形式来分辨这种情况。

    \n
    var seconds int
    var ok bool
    seconds, ok = timeZone[tz]
    \n

    在下面的例子中,若 tz 存在, seconds 就会被赋予适当的值,且 ok 会被置为 true; 若不存在,seconds 则会被置为零,而 ok 会被置为 false。

    \n
    func offset(tz string) int {
    if seconds, ok := timeZone[tz]; ok {
    return seconds
    }
    log.Println(\"unknown time zone:\", tz)
    return 0
    }
    \n

    若仅需判断映射中是否存在某项而不关心实际的值,可使用 空白标识符 (_)来代替该值的一般变量。

    \n
    _, present := timeZone[tz]
    \n

    要删除映射中的某项,可使用内建函数 delete,它以映射及要被删除的键为实参。 即便对应的键不在该映射中,此操作也是安全的。

    \n
    delete(timeZone, \"PDT\")  // Now on Standard Time
    \n

    当打印结构体时,改进的格式 %+v 会为结构体的每个字段添上字段名,而另一种格式 %#v 将完全按照 Go 的语法打印值。

    \n

    初始化

    常量只能是数字、字符(符文)、字符串或布尔值。由于编译时的限制, 定义它们的表达式必须也是可被编译器求值的常量表达式。例如 1<<3 就是一个常量表达式,而 math.Sin(math.Pi/4) 则不是,因为对 math.Sin 的函数调用在运行时才会发生。

    \n

    在 Go 中,枚举常量使用枚举器 iota 创建。由于 iota 可为表达式的一部分,而表达式可以被隐式地重复,这样也就更容易构建复杂的值的集合了。

    \n
    type ByteSize float64

    const (
    // 通过赋予空白标识符来忽略第一个值
    _ = iota // ignore first value by assigning to blank identifier
    KB ByteSize = 1 << (10 * iota)
    MB
    GB
    TB
    PB
    EB
    ZB
    YB
    )
    \n

    方法

    以指针或值为接收者的区别在于:值方法可通过指针和值调用, 而指针方法只能通过指针来调用。

    \n

    之所以会有这条规则是因为指针方法可以修改接收者;通过值调用它们会导致方法接收到该值的副本, 因此任何修改都将被丢弃,因此该语言不允许这种错误。不过有个方便的例外:若该值是可寻址的, 那么该语言就会自动插入取址操作符来对付一般的通过值调用的指针方法。在我们的例子中,变量 b 是可寻址的,因此我们只需通过 b.Write 来调用它的 Write 方法,编译器会将它重写为 (&b).Write。

    \n
    \n

    通常我们会将方法写为指针接收者,这种情况下,即便是用值调用这个方法,编辑器会自动帮我们改为指针调用。

    \n
    \n

    并发

    并发是用可独立执行的组件构造程序的方法,而并行则是为了效率在多 CPU 上平行地进行计算。

    \n
    \n

    并发是两个队列交替使用一台咖啡机,并行是两个队列同时使用两台咖啡机

    \n
    \n

    错误

    若调用者关心错误的完整细节,可使用类型选择或者类型断言来查看特定错误,并抽取其细节。

    \n
    for try := 0; try < 2; try++ {
    file, err = os.Create(filename)
    if err == nil {
    return
    }
    if e, ok := err.(*os.PathError); ok && e.Err == syscall.ENOSPC {
    deleteTempFiles() // Recover some space.
    continue
    }
    return
    }
    \n

    panic 被调用后(包括不明确的运行时错误,例如切片检索越界或类型断言失败),程序将立刻终止当前函数的执行,并开始回溯 Go 程的栈,运行任何被推迟的函数。 若回溯到达 Go 程栈的顶端,程序就会终止。不过我们可以用内建的 recover 函数来重新或来取回 Go 程的控制权限并使其恢复正常执行。

    \n

    调用 recover 将停止回溯过程,并返回传入 panic 的实参。 由于在回溯时只有被推迟函数中的代码在运行,因此 recover 只能在被推迟的函数中才有效。

    \n

    recover 的一个应用就是在服务器中终止失败的 Go 程而无需杀死其它正在执行的 Go 程。

    \n
    func server(workChan <-chan *Work) {
    for work := range workChan {
    go safelyDo(work)
    }
    }

    func safelyDo(work *Work) {
    defer func() {
    if err := recover(); err != nil {
    log.Println(\"work failed:\", err)
    }
    }()
    do(work)
    }
    \n","tags":["go"]},{"title":"有效不一定复杂","url":"/2023/effectiveness-not-complexity/","content":"

    上个季度我在业务中做了个看似很简单的功能,却得到了非常好的收益。这个功能简单来说就是帮用户给对方打个招呼,提升两个人聊天的概率,进而提升日活等指标。

    \n

    我们是个社交平台,但是发现很多用户在形成匹配后并没有说过话,浪费了很多的机会。之前已经在产品形态上给了用户非常便捷的方式去选择一个文案发送,但渗透率还是很低,这次我们尝试直接帮用户去发开场打招呼这条消息。

    \n

    我们把一种特殊类型的系统消息实现成用户自己发送消息的样式,只在男性侧展示,男性会认为是女性主动给她了条消息。文案上我们只使用常见且无意义的打招呼文案,不容易被对方察觉,比如:hello、你好、hi、嗨,等等。

    \n

    我们通过一些特征匹配到受众的男性(并不是所有用户都适用),这些特征也是我们不断摸索出来的。再为这个男性找一个最适合他的女性,通过上边说的那条消息引导男性活跃、主动起来,我们帮用户迈出第一步,当第一层窗户纸捅破后用户大概率可以继续聊起来。

    \n

    当然这中间我们还打磨了大量细节,比如负反馈、冷却期等,这里不再详述。上线经过几轮实验迭代后得到了非常好的收益,日活、次留、七留等指标都有超大幅度提升。

    \n

    这个功能并不复杂,实现起来也比较简单,却取得了巨大的成功,主要还是巧妙利用了人性,找到了非常好的触发点。

    \n
      \n
    • 一个不怎么聊天的男性看到有女性给他打招呼,会很有诱惑力,毕竟有句老话叫:女追男隔层纱。
    • \n
    • 我们的使用的文案都是非常常见的消息,基本不会被察觉,男性就会顺着这条消息给女性也做个礼貌性回复,并且继续往下聊。
    • \n
    • 一开始是用女性拉动男性,男性再回消息拉动女性,这样转起来就会形成一个正向螺旋,整个大船就会启动起来。
    • \n
    \n
    \n

    这个功能最终被下线了,原因是其他业务线产品发现这个套路后,过度使用这种方法来拉动短期增长,以达成 Q2指标,求快过程中没有经过多次实验迭代直接全量,产生了大量客诉,给生态造成了影响,最后使用相关特性的功能全部停止。虽然最终下线了,但整个过程让我产生很多思考,而且我也很荣幸引领了一股产品决策的潮流。

    \n

    最有效的方法往往没那么复杂,生活也一样,可以很简单,你只需要不假思索地做正确的事:

    \n
      \n
    • 健康:节食,早睡
    • \n
    • 家庭:付出,陪伴
    • \n
    • 成长:阅读,写作
    • \n
    • 财富:定投,指数
    • \n
    \n"},{"title":"约法三章","url":"/2023/establish-a-set-of-rules/","content":"

    现在是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
    \n

    我无疑是云雀星,白天也没有午睡习惯。但我很羡慕周五晚上去嗨,然后利用周末补觉的那些人,我自己尝试了多次后发现真的不适合自己,毕竟这些东西都是基因里已经决定了的。

    \n

    我在昨天属实算「纵欲」了,因为最近一段时间睡眠质量不错,就放松了警惕,白天喝了两杯咖啡,上午一杯、下午一杯,晚上还喝了度数较高的啤酒,为了配酒还吃了高热量实物,深夜又刷了很久手机。

    \n

    有必要给自己约法三章了,虽然之前也已经约法过,但希望这次是最后一次…

    \n
      \n
    1. 每天最多喝一杯咖啡,如果前一晚没睡好可以考虑加浓。下午2点后禁止喝咖啡!
    2. \n
    3. 任何场合都要远离酒精制品,包括团建、家庭聚会。
    4. \n
    5. 晚上10点半后不再看手机(工作内容,比如业务报警除外)。
    6. \n
    \n

    再补充一个让我有点难过的事情,凌晨4点多我决定不再尝试入睡,起来写这篇文章前,看了眼手机才注意到微信里有一条昨天晚上9点40多的语音消息,念念和我说她要一个人睡了,还给我发了照片。我当时在喝酒,没有看到也没有回复她,她当时一定很期待我的回复吧。

    \n"},{"title":"阅读 Eureka 的 StringCache 源码 get 到的知识","url":"/2018/eureka-StringCache-get-new-knowledge/","content":"

    今天在读 Eureka 源码时,看到了它里边实现了一个工具类 StringCache 阅读后我产生了几个疑问,查阅资料后一一进行了解决,受益良多并以此文进行记录。

    \n

    StringCache 实现了一个字符串缓存,代码如下

    \n
    public class StringCache {

    public static final int LENGTH_LIMIT = 38;

    private static final StringCache INSTANCE = new StringCache();

    private final ReadWriteLock lock = new ReentrantReadWriteLock();
    private final Map<String, WeakReference<String>> cache = new WeakHashMap<String, WeakReference<String>>();
    private final int lengthLimit;

    public StringCache() {
    this(LENGTH_LIMIT);
    }

    public StringCache(int lengthLimit) {
    this.lengthLimit = lengthLimit;
    }

    public String cachedValueOf(final String str) {
    if (str != null && (lengthLimit < 0 || str.length() <= lengthLimit)) {
    // Return value from cache if available
    try {
    lock.readLock().lock();
    WeakReference<String> ref = cache.get(str);
    if (ref != null) {
    return ref.get();
    }
    } finally {
    lock.readLock().unlock();
    }

    // Update cache with new content
    try {
    lock.writeLock().lock();
    WeakReference<String> ref = cache.get(str);
    if (ref != null) {
    return ref.get();
    }
    cache.put(str, new WeakReference<>(str));
    } finally {
    lock.writeLock().unlock();
    }
    return str;
    }
    return str;
    }

    public int size() {
    try {
    lock.readLock().lock();
    return cache.size();
    } finally {
    lock.readLock().unlock();
    }
    }

    public static String intern(String original) {
    return INSTANCE.cachedValueOf(original);
    }
    }
    \n

    什么是字符串常量池?

    String s = "a" + "bc";
    String t = "ab" + "c";
    System.out.println(s == t);
    \n

    上边这段程序会打印 true(尽管我们没有使用正确比较字符串的 equals 方法)

    \n

    当编译器优化字符串的字面值时,它看到 st 有相同的值,因为字符串在 Java 中是不可变的,所以提供同一个字符串对象也是安全的,因此 st 指向了同一个对象并且节省了一丢丢的内存。

    \n

    「字符串常量池」的灵感来源于这样的想法:所有已定义的字符串都存储在一个「池子」中,在创建新的 String 对象前,编译器需要检查这个字符串是否已经被定义,若已经在「池子」中存在就直接拿出来用。

    \n

    也就是说 Java 编译器已经用字符串常量池实现了字符串缓存的特性,在我们直接使用双引号来声明 String 对象时会自动利用以上特性,如果不是用双引号声明的,可以用 String 提供的 intern() 方法。intern() 方法会从字符串常量池中查询当前字符串是否存在,若不存在就会将当前字符串放入常量池中。

    \n

    示例程序:

    \n
    String a1 = "aaa";
    String a2 = "aaa";
    String a3 = new String("aaa");
    System.out.println(a1 == a2); // true
    System.out.println(a1 == a3); // false
    System.out.println(a1 == a3.intern()); // true

    String b1 = new String("bbb").intern();
    String b2 = "bbb";
    System.out.println(b1 == b2); // true
    \n

    为什么 Eureka 要再造轮子?

    既然 Java 编译器已经对相同的字符串进行了优化,为什么 Eureka 还要再造一个轮子呢,因为字符串常量池在存储大量的字符串后,会出现严重的性能问题。

    \n

    以下解释来自美团点评技术团队编写的 深入解析String#intern 一文:

    \n
    \n

    Java 使用 JNI 调用 C++ 实现的 StringTable 的 intern 方法,StringTable 的 intern 方法跟 Java 中的 HashMap 的实现是差不多的,只是不能自动扩容。默认大小是1009。

    \n
    \n
    \n

    要注意的是,String 的 String Pool 是一个固定大小的 Hashtable,默认值大小长度是1009,如果放进 String Pool 的 String 非常多,就会造成 Hash 冲突严重,从而导致链表会很长,而链表长了后直接会造成的影响就是当调用 String.intern 时性能会大幅下降(因为要一个一个找)。

    \n
    \n

    好了原因解释清楚了,我们来看一下具体实现中有那哪些问题。

    \n

    WeakHashMap 和 HashMap 有什么区别?

    StringCache 的代码不难理解,大致就是声明一个锁和一个 Map,取值时先获取锁,如果存在就直接从 Mapget 出来然后返回,不存在就 put 进去作为缓存以便下次使用。

    \n

    这里声明的 Map 类型是 WeakHashMap,这种 Map 的特点是,当除了自身有对 key 的引用外,此 key 没有其他引用那么这个 map 会自动丢弃此值。

    \n

    示例程序:

    \n
    String a = new String("a");
    String b = new String("b");
    Map<String, String> weakmap = new WeakHashMap();
    Map<String, String> map = new HashMap();
    map.put(a, "aaa");
    map.put(b, "bbb");

    weakmap.put(a, "aaa");
    weakmap.put(b, "bbb");

    map.remove(a);
    a = null;
    b = null;

    System.gc();
    Iterator i = map.entrySet().iterator();
    while (i.hasNext()) {
    Map.Entry en = (Map.Entry)i.next();
    System.out.println("map: "+en.getKey()+":"+en.getValue());
    }

    Iterator j = weakmap.entrySet().iterator();
    while (j.hasNext()) {
    Map.Entry en = (Map.Entry)j.next();
    System.out.println("weakmap: "+en.getKey()+":"+en.getValue());
    }
    \n

    我们声明了两个 Map 对象,一个是 HashMap,一个是 WeakHashMap,同时向两个 map 中放入 ab 两个对象,从 HashMapremovea 并且将 ab 都指向 null 时,WeakHashMap 中的 a 将自动被回收掉。出现这个状况的原因是,对于 a 对象而言,当从 HashMapremovea 并且将 a 指向 null 后,除了 WeakHashMap 中还保存 a 外已经没有指向 a 的指针了,所以 WeakHashMap 会自动舍弃掉 a,而对于 b 对象虽然指向了null,但 HashMap 中还有指向 b 的指针,所以 WeakHashMap 将会保留 b

    \n

    以上程序得到的结果是:

    \n
    map: b:bbb
    weakmap: b:bbb
    \n

    WeakReference 和普通的引用有什么区别?

    可以看到我们的 StringCache 中的 Map 值类型用的是 WeakReference<String>,如果你希望能随时取得某对象的信息,但又不想影响此对象的垃圾收集,那么你应该用 Weak Reference 来记住此对象,而不是用一般的 reference

    \n

    如果不这样用,会导致我们 Map 的值也会引用我们想缓存的字符串,这就导致即使 key 已经没有任何地方引用了,这个 WeakHashMap 也不会丢弃此值。

    \n

    ReentrantReadWriteLock 有什么特性?

    ReentrantReadWriteLock 是一个读写锁:分为读锁和写锁,多个读锁不互斥,读锁与写锁互斥,这是由 jvm 自己控制的,你只要上好相应的锁即可。如果你的代码只读数据,可以很多人同时读,但不能同时写,那就上读锁;如果你的代码修改数据,只能有一个人在写,且不能同时读取,那就上写锁。总之,读的时候上读锁,写的时候上写锁。

    \n

    线程进入读锁的前提条件:

      \n
    • 没有其他线程的写锁,
    • \n
    • 没有写请求或者有写请求,但调用线程和持有锁的线程是同一个
    • \n
    \n

    线程进入写锁的前提条件:

      \n
    • 没有其他线程的读锁
    • \n
    • 没有其他线程的写锁
    • \n
    \n"},{"title":"Eureka Server 外置配置文件","url":"/2017/eureka-config-outside/","content":"

    在我们现在的架构中,一切都是由 Eureka 开始的,因为业务中的配置中心地址是使用 service-id 的形式,从注册中心来自动发现配置中心地址。

    \n

    这就出现了一个矛盾,如果我们想将 Eureka Server 的配置外置的话就不太可行了,因为 Eureka 启动前,配置中心还没有注册进来,所以它也无法发现配置中心。

    \n

    现在我们项目中的做法是:Eureka Server 的所有配置文件都是写在自己的 application.yml 中。

    \n

    今天我想到一个思路,并验证了其可行性:

    \n

    可以先将 Config Server 启动起来,因为 Config Server 需要注册到 Eureka Server 上,但是注册失败并不会导致服务的终止,只是在发心跳包时会有一些错误信息。也就是说,即便不注册上去,配置中心也是可以通过 IP+端口号 的形式来访问的。

    \n

    Config Server 的 application.yml 如下:

    \n
    management:
    security:
    enabled: false

    server:
    port: 7020

    spring:
    application:
    name: config-server
    cloud:
    config:
    server:
    git:
    username: xxxxx
    password: yyyyy
    uri: http://gitlab-server/config-repo/{application}.git
    force-pull: true
    label: master

    eureka:
    client:
    service-url:
    defaultZone: http://localhost:7011/eureka/
    instance:
    prefer-ip-address: true
    \n

    然后我们在 Git 中新建 eureka 仓库,并创建 eureka-dev.yml 文件,其内容如下:

    \n
    server:
    port: 7011

    eureka:
    instance:
    hostname: localhost
    client:
    service-url:
    defaultZone: http://localhost:7011/eureka/
    register-with-eureka: false
    fetch-registry: false
    \n

    然后在 Eureka Server 项目中新建 bootstrap.yml 文件,内容如下:

    \n
    spring:
    application:
    name: eureka
    jackson:
    default-property-inclusion: non_null
    cloud:
    config:
    label: master
    profile: dev
    uri: http://localhost:7020/
    \n

    这里我们使用 IP+端口号 的形式来访问配置中心,然后将之前的 application.yml 配置文件删除。

    \n

    现在我们来分别启动这两个项目,注意启动顺序:先启动 Config Server,再启动 Eureka Server,启动完 Config Server 后会看到一些错误,暂时不用理会,启动 Eureka Server 时我们可以在控制台中看到可以正常拉取配置,如图:

    \n

    \"\"

    \n

    待 Eureka Server 启动完后,再回来看 Config Server 的控制台,已经不报错了,到 http://localhost:7011/ 看到,配置中心也成功注册上来,这也说明我们的 Eureka Server 已经成功读到了 配置中心 提供的配置文件。

    \n

    \"\"

    \n

    以上验证了这种思路是可行的,回头可以将线上环境也修改为这种方式。

    \n"},{"title":"关于事件循环的 15 个问题","url":"/2021/event-loop-questions/","content":"

    如果你的程序中只有一个事件循环,没有其他代码,那么是否有可能在同一时间运行两行代码?

    不可能。

    \n

    一个事件循环中的所有代码都在一个操作系统线程中运行,所以任何时刻只能有一段代码在运行。

    \n

    在一个有事件循环的程序中,是否可以有其他线程?

    是的。

    \n

    例如,在 node.js 中,所有的 Javascript 代码都在一个线程中运行,但还有其他工作线程来处理网络请求和其他 I/O。

    \n

    在一个事件循环中,是否由操作系统来负责调度函数执行的顺序?

    不是。

    \n

    例如,用Python的 asyncio,做调度的代码是一个 Python 程序。

    \n

    事件循环真的是一个循环吗?(就像像 for 循环或 while 循环?)

    是的。

    \n

    通常事件循环都是以while循环的形式实现的,它看起来像这样。

    \n
    while True:
    self._run_once()
    if self._stopping:
    break
    \n

    (以上是 Python 的 asyncio 事件循环的实际代码)

    \n

    事件循环如何决定下一个函数的运行?

    通过队列。

    \n

    当函数准备好运行时,它们会被推到队列中,然后事件循环按顺序执行队列中的函数。

    \n

    如果一个网络请求返回,并且它有一个附加的回调,该回调是否会被推送到事件循环的队列中?

    是的。

    \n

    当网络请求或其他 I/O 完成后,或者用户点击了某些东西,或者因为该函数计划在那时运行等,函数可能会被推入事件循环的队列中。

    \n

    常规函数和异步函数一样吗?

    不一样。

    \n

    异步函数特殊之处在于,它们可以被“暂停”并在稍后的事件循环中重新启动。

    \n

    例如,在下边这段 Javascript 代码中

    \n
    async function panda() {
    let x = 3;
    await elephant();
    let y = 4;
    }
    \n

    事件循环调度 elephant(),暂停 panda,并在 elephant() 运行完毕后调度 panda() 重新启动。普通的非 async 函数不能像这样暂停和重启。这些可以暂停和重启的异步函数的另一个名字是协程。

    \n

    如果你要求事件循环在某个时间运行一个函数(比如 Javascript 中的setTimeout),它能保证在那个时间运行吗?

    不能。

    \n

    事件循环会尽力而为,但有时会延迟。

    \n

    在 Javascript 中,promises、setTimeout、async/await 和回调是否都使用相同的事件循环?

    是的。

    \n

    虽然它们的语法不同,但它们是安排代码稍后运行的不同方式。

    \n

    在下边这段代码中,是否可以让事件循环在x=3后中断,然后运行别的东西?

    x = 3;
    y = 4;
    \n

    不能。

    \n

    你需要显式让步给事件循环,让它运行一个不同的函数,例如使用 await

    \n

    如果你运行一些CPU密集型的代码,如下,事件循环最终会中断代码吗?

    while(true) { 
    i = i * 3
    }
    \n

    不会。

    \n

    通常,你可以通过运行一些 CPU 密集型操作来长时间阻塞事件循环。

    \n

    如果你的Web 服务器事件循环中 CPU 的使用率到达100%,当新的 HTTP 请求进来时,是否能够立即响应?

    不能。

    \n

    如果你的事件循环的 CPU 总是很忙,那么新进来的事件就不会得到及时处理。

    \n

    Javascript代码总是有一个事件循环吗?

    是的。

    \n

    至少在 node.js 和浏览器中是这样的,Javascript 代码总是有一个事件循环运行。

    \n

    是否存在一个所有的事件循环都在使用标准的事件循环库?

    没有。

    \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

    事件溯源 vs 事件驱动架构

    目的

      \n
    • 事件溯源是一种持久化策略的代替方案,目的是保留历史。
    • \n
    • 事件驱动架构是一种分布式异步架构模式,用于提升应用程序的扩展性。
    • \n
    \n

    范围

      \n
    • 事件溯源通常应用于单一的系统或应用
    • \n
    • 事件驱动架构在多个系统或应用中使用。作为一种可靠的集成模式,具有很高的灵活性,可迅速响应不断变化的环境。
    • \n
    \n

    数据存储

      \n
    • 事件溯源有一个中央事件仓库,通常有副本、分片等。它依赖于一个中央数据库。
    • \n
    • 事件驱动架构是分布式的,每个组件或处理器都是解耦的,可能各自有独立的仓库。
    • \n
    \n

    可测实性

      \n
    • 事件溯源更容易测试,因为它可以从头开始重放整个事件序列,直到达到某个状态或情况。
    • \n
    • 事件驱动架构很容易单独测试每个组件,但由于这种模式的异步性,对整体的测试就比较复杂了。
    • \n
    \n

    综合

    \n\n\n\n\n\n\n\n\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/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

    现在她的生活依然丰富多彩,全国各处旅游打卡吃美食,而且是个滑雪手、摩托车手。工作也是换了一份又一份,很早之前我问时是在做婚礼策划,过一段时间再问时准备开个精酿小酒馆,她就像一个神,让人捉摸不定,我是泯然众生中的一个守望者。

    \n

    2021年她在朋友圈晒了结婚证,巧的是那个男生也姓贾。去年他们举办了婚礼,她穿婚纱真好看。

    \n"},{"title":"exa 和 zsh-syntax-highlighting","url":"/2017/exa-and-zsh-syntax-highlighting/","content":"

    今天再来记录两个命令行神器。

    \n

    第一个是 exa: https://the.exa.website/

    \n

    官方的介绍为

    \n
    \n

    exa is a modern replacement for ls.

    \n
    \n

    顾名思义 exa 是一个用来替代 ls 的工具,官方介绍了很多关于 exa 的特性,对于我来说,使用它的原因是可以支持不同文件类型可以用不同颜色来展示这个特性。至于官方还提到,exals 要更快一些,这我倒是没有什么感觉。

    \n

    在 Mac 下直接用 Homebrew 安装就行了: brew install exa,为了方便使用,我直接修改 alias 为 ls,这样之后再使用 ls 命令时,系统就自动用 exa 来代替了,毫无学习成本。

    \n

    \"\"

    \n

    来看下效果:

    \n

    \"\"

    \n

    我这个目录下不同类型的文件不多,没有展示出特别好的效果

    \n

    第二个神器是 zsh-syntax-highlighting,看名字就知道,它是一个在 zsh 下使用的工具,官方介绍为:

    \n
    \n

    Fish shell-like like syntax highlighting for Zsh.

    \n
    \n

    zsh-syntax-highlighting 是用来在命令行中提供语法高亮的工具 (很抱歉我没有用过 Fish)。

    \n

    效果图:

    \n

    \"\"

    \n

    安装方法:

    \n

    brew install zsh-syntax-highlighting

    \n

    然后将

    \n

    source /usr/local/share/zsh-syntax-highlighting/zsh-syntax-highlighting.zsh

    \n

    加入到 .zshrc 文件中即可。

    \n

    最后再来说下我现在用的 iTerm2 配色,之前一直用的都是自己配的,会遇到文字和背景色不太搭的情况,比如 Date 和 Modified 那两列:

    \n

    \"\"

    \n

    所以最近换用了: https://github.com/dracula/iterm 这个配色方案。看起来挺舒服的,所以就不再自己折腾了。

    \n

    \"\"

    \n"},{"title":"Idea 启动 SpringBoot 加速","url":"/2019/fast-idea-in-mac/","content":"
    # 查看自己的 hostname
    ➜ hostname
    jiapandeMacBook-Pro.local

    # 将 hostname 加入 hosts
    127.0.0.1 localhost jiapandeMacBook-Pro.local
    ::1 localhost jiapandeMacBook-Pro.local
    \n

    参考:https://amsterdam.luminis.eu/2017/05/10/fixing-slow-startup-time-java-application-running-macos-sierra/

    \n"},{"title":"我对肥胖、营养与三餐的看法","url":"/2019/fat-nutrition-and-meal/","content":"

    \"\"

    \n
    \n

    我之前在一年多的时间里,在很少运动的情况下,减掉了 50 多斤的体重(从 90 多公斤到目前稳定在 65 公斤内),同时内脏脂肪也降了下来(我的体脂称告诉我的),今年体检后各项指标也都正常,所以对本文谈到的东西也算有话语权。

    \n
    \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。

    \n

    break 的意思是 打破、阻断、破坏。

    \n

    fast 大多数人只知道它有快的意思,但实际上它是一个医学专有名词,叫做「禁食」

    \n

    breakfast 这个单词已经告诉我们了,早餐是破坏禁食状态,不吃才是对的,常态应该是不吃。但是很多人就会跳起来反驳我,不吃我会饿啊。别急,听我继续说。

    \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 时出了问题,详细报错如下:

    \n
    ERROR: Command errored out with exit status 1:
    command: /usr/local/bin/python -u -c 'import sys, setuptools, tokenize; sys.argv[0] = '"'"'/tmp/pip-install-Od1Eam/MySQL-python/setup.py'"'"'; __file__='"'"'/tmp/pip-install-Od1Eam/MySQL-python/setup.py'"'"';f=getattr(tokenize, '"'"'open'"'"', open)(__file__);code=f.read().replace('"'"'\\r\\n'"'"', '"'"'\\n'"'"');f.close();exec(compile(code, __file__, '"'"'exec'"'"'))' bdist_wheel -d /tmp/pip-wheel-wCwIa3 --python-tag cp27
    cwd: /tmp/pip-install-Od1Eam/MySQL-python/
    Complete output (38 lines):
    running bdist_wheel
    running build
    running build_py
    creating build
    creating build/lib.linux-x86_64-2.7
    copying _mysql_exceptions.py -> build/lib.linux-x86_64-2.7
    creating build/lib.linux-x86_64-2.7/MySQLdb
    copying MySQLdb/__init__.py -> build/lib.linux-x86_64-2.7/MySQLdb
    copying MySQLdb/converters.py -> build/lib.linux-x86_64-2.7/MySQLdb
    copying MySQLdb/connections.py -> build/lib.linux-x86_64-2.7/MySQLdb
    copying MySQLdb/cursors.py -> build/lib.linux-x86_64-2.7/MySQLdb
    copying MySQLdb/release.py -> build/lib.linux-x86_64-2.7/MySQLdb
    copying MySQLdb/times.py -> build/lib.linux-x86_64-2.7/MySQLdb
    creating build/lib.linux-x86_64-2.7/MySQLdb/constants
    copying MySQLdb/constants/__init__.py -> build/lib.linux-x86_64-2.7/MySQLdb/constants
    copying MySQLdb/constants/CR.py -> build/lib.linux-x86_64-2.7/MySQLdb/constants
    copying MySQLdb/constants/FIELD_TYPE.py -> build/lib.linux-x86_64-2.7/MySQLdb/constants
    copying MySQLdb/constants/ER.py -> build/lib.linux-x86_64-2.7/MySQLdb/constants
    copying MySQLdb/constants/FLAG.py -> build/lib.linux-x86_64-2.7/MySQLdb/constants
    copying MySQLdb/constants/REFRESH.py -> build/lib.linux-x86_64-2.7/MySQLdb/constants
    copying MySQLdb/constants/CLIENT.py -> build/lib.linux-x86_64-2.7/MySQLdb/constants
    running build_ext
    building '_mysql' extension
    creating build/temp.linux-x86_64-2.7
    gcc -pthread -fno-strict-aliasing -g -O2 -DNDEBUG -g -fwrapv -O3 -Wall -Wstrict-prototypes -fPIC -Dversion_info=(1,2,5,'final',1) -D__version__=1.2.5 -I/usr/include/mariadb -I/usr/include/mariadb/mysql -I/usr/local/include/python2.7 -c _mysql.c -o build/temp.linux-x86_64-2.7/_mysql.o
    In file included from _mysql.c:44:
    /usr/include/mariadb/my_config.h:3:2: warning: #warning This file should not be included by clients, include only <mysql.h> [-Wcpp]
    #warning This file should not be included by clients, include only <mysql.h>
    ^~~~~~~
    In file included from _mysql.c:46:
    /usr/include/mariadb/mysql.h:440:3: warning: function declaration isn’t a prototype [-Wstrict-prototypes]
    MYSQL_CLIENT_PLUGIN_HEADER
    ^~~~~~~~~~~~~~~~~~~~~~~~~~
    _mysql.c: In function ‘_mysql_ConnectionObject_ping’:
    _mysql.c:2005:41: error: ‘MYSQL’ {aka ‘struct st_mysql’} has no member named ‘reconnect’
    if ( reconnect != -1 ) self->connection.reconnect = reconnect;
    ^
    error: command 'gcc' failed with exit status 1
    ----------------------------------------
    ERROR: Failed building wheel for MySQL-python
    \n

    错误原因是 MariaDB 10.2、10.3 的 MySQL 版本没有定义 reconnect,需要自己来声明,只需在 pip install 前插入如下命令即可:

    \n
    RUN sed '/st_mysql_options options;/a unsigned int reconnect;' /usr/include/mysql/mysql.h -i.bkp
    \n

    非 Docker 环境执行一下 RUN 之后的命令就可以了。

    \n

    参考:https://github.com/DefectDojo/django-DefectDojo/issues/407

    \n"},{"title":"2019 Flags","url":"/2019/flag-2019/","content":"
      \n
    • 至少阅读 3 本计算机相关的砖头书
        \n
      • [ ] 深入理解计算机系统
      • \n
      • [ ] 代码大全
      • \n
      • [ ] 程序员修炼之道 - 从小工到专家(二刷)
      • \n
      • [ ] UNINX 编程艺术
      • \n
      • [ ] 算法
      • \n
      • [ ] 计算机网络:自顶向下方法
      • \n
      \n\n
    • \n
    • 每月至少读一本非计算机类的书
    • \n
    • 熟练掌握 Go 语言,并用它完成一个大中型项目
    • \n
    • 阅读 & 解析 Spring、Eureka 源码
    • \n
    • 学习 Team Leader
    • \n
    \n","tags":["flag"]},{"title":"flask-bootstrap默认使用国外cdn解决方案","url":"/2015/flask-bootstrap%E9%BB%98%E8%AE%A4%E4%BD%BF%E7%94%A8%E5%9B%BD%E5%A4%96cdn%E8%A7%A3%E5%86%B3%E6%96%B9%E6%A1%88/","content":"

    flask-bootstrap 默认走的是国外CDN,所以在天朝使用起来速度奇慢。

    \n

    解决办法:

    \n
      \n
    • 找到在包管理目录中找到flask-bootstrap__init__.py文件,将里边控制CDN部分的代码修改为:
    • \n
    \n
    bootstrap = lwrap(
    WebCDN('//cdn.bootcss.com/bootstrap/%s/'
    % BOOTSTRAP_VERSION),
    local)

    jquery = lwrap(
    WebCDN('//cdn.bootcss.com/jquery/%s/'
    % JQUERY_VERSION),
    local)
    \n

    只修改这两段即可。

    \n
      \n
    • 直接用我fork的分支进行安装flask-bootstrap,源码我已修改为使用国内CDN:
    • \n
    \n
    pip install git+https://github.com/Panmax/flask-bootstrap.git
    \n

    \n
    git+https://github.com/Panmax/flask-bootstrap.git
    \n

    将此写在requirements.txt文件中

    \n","categories":["flask"],"tags":["flask","CDN","bootstrap"]},{"title":"foobar","url":"/2023/foobar/","content":"

    如果你是一位程序员,一定在编程教材或网上文档的示例代码中见到过使用 foo、bar 这两个词为变量命名。如:

    \n
    String foo = \"Hello, \";
    String bar = \"World\";
    System.out.println(foo + bar);
    \n

    或者:

    \n
    #include <stdio.h>

    int main()
    {
    char foo[] = \"Hello,\";
    char bar[] = \"World!\";
    printf(\"%s %s\\\\n\", foo, bar);

    return 0;
    }
    \n

    foobar 的由来

    foo、bar 的来源究竟是什么呢?我尝试查了一些资料来解答这个问题。

    \n

    对于 foobar 的来源,主要有两种解释:

    \n

    1. FUBAR 缩写词

    这一派认为,foo和bar源自美国陆军二战缩写 FUBAR,“Fouled Up Beyond All Recognition”(操蛋到无法修复)。

    \n

    \n

    2. 电子学术语

    foo 表示电子学中反转的信号,bar 表示一个低电平有效的数字信号。

    \n

    为什么使用 foo和 bar

    1. 约定成俗

    老一辈的程序员们很喜欢在示例代码中使用这两个词作为变量名,发展到后来甚至已经成为 C 和 UNIX 文化的一部分。

    \n

    在 linux/lib/test_debug_virtual.c 中,使用 foo 作为结构名称,使用 bar 作为内部字段名称。:

    \n
    struct foo {
    \tunsigned int bar;
    };

    static struct foo *foo;
    \n

    在 linux/tools/testing/selftests/bpf/test_cgroup_attach.c 中将临时文件夹命名为 foo 和 bar:

    \n
    #define FOO\t\t\"/foo\"
    #define BAR\t\t\"/foo/bar/\"
    #define PING_CMD\t\"ping -q -c1 -w1 127.0.0.1 > /dev/null\"

    char bpf_log_buf[BPF_LOG_BUF_SIZE];
    \n

    2. 易于查找

    这个解释虽然有些牵强,但也说的通。

    \n

    foo 和 bar 很容易在代码块中发现,这使得在用眼睛浏览和扫描代码时可以轻松找到和替换。

    \n

    结论

    foo 和bar 在代码中无任何实际含义,在教学或写文档过程中为了快速说明一个特性、操作符的使用方法,同时作者又不想大费周章的想一个恰当的变量名,就统统使用 foo、bar 来表示一些无意义的变量,久而久之这个习惯就流传了下来。

    \n

    这两个词在这种用法中没有任何意义,仅仅表示一个变量占位符,就像代数中使用的字母 x 和 y 一样。

    \n

    最后

    如果你在示例代码中看到 foo、bar,需要明白这个变量的名称是不重要且随意的,将重点放在后边的代码或者整体逻辑上即可。foo 和 bar 作为两个最常用的临时变量,它们实际上并没有任何词语含义,通常为了方便起见,用来代替更准确的名称。

    \n

    foo 和 bar 比其他临时变量更受欢迎,因为它们的受欢迎,而且它们不可理解的性质使它们很容易被精确定位。

    \n

    也因为 foobar 这个术语非常流行,后来有一个 Windows 上的音频播放器将自己命为 foobar2000。

    \n"},{"title":"为了碎银几两","url":"/2023/for-money/","content":"

    最近几天北京下暴雨,公司启动了远程办公模式,我之前好像在一篇文章中写到,相对来说我更喜欢到公司上班,因为去公司工作更有条理和规划,(基本上)能事先规划好每个时间段要做的事情,也能保持生物钟的稳定状态。

    \n

    周末的时候,我整理了新家的吧台,然后在吧台旁看了一会书。

    \n

    \n

    周一早上把电脑搬了过来,把吧台作为了一个可以观景的办公环境。

    \n

    \n

    在家办公效率总觉得很低,主要有以下几个原因:

    \n
      \n
    1. 没有双显示器的辅助。我之前本来有一个外界显示器,是在19年买的,分辨率不怎么高,前段时间出咸鱼了。
    2. \n
    3. 总是想摸摸这里看看那里分散注意力。
    4. \n
    5. 还要考虑吃些什么,必要时还要做饭。
    6. \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
    1. 鸡胸肉划上斜刀用盐和料酒腌十分钟
    2. \n
    3. 然后锅里倒一点点油(我用的橄榄油)油稍微冒烟后下鸡胸
    4. \n
    5. 煎至两面变色后加入一大勺蚝油
    6. \n
    7. 然后煎至两面金黄
    8. \n
    9. 撒上黑胡椒再煎两分钟即可出锅
    10. \n
    11. 装盘时可以用盐水煮芦笋和柠檬做装饰
    12. \n
    \n

    是不是非常简单?

    \n

    最后,上几张图:

    \n

    \"\"

    \n

    \"\"

    \n

    \"\"

    \n"},{"title":"functools.partial","url":"/2016/functools-partial/","content":"

    今天在看flask源码时看到了这样的写法:

    \n

    request = LocalProxy(partial(_lookup_req_object, 'request'))

    \n

    第一次见partial的使用,所以查了查资料学习了下。

    \n

    我们可以简单的理解为 partial(func, ‘request’) 就是使用 ‘request’ 作为func的第一个默认参数来产生另外一个function。

    \n

    所以, partial(_lookup_req_object, ‘request’) 我们可以理解为:

    \n

    生成一个callable的function,这个function主要是从 _request_ctx_stack 这个LocalStack对象获取堆栈顶部的第一个RequestContext对象,然后返回这个对象的request属性。

    \n
    \n

    functools.partial 通过包装手法,允许我们 “重新定义” 函数签名

    \n

    用一些默认参数包装一个可调用对象,返回结果是可调用对象,并且可以像原始对象一样对待

    \n

    冻结部分函数位置函数或关键字参数,简化函数,更少更灵活的函数参数调用

    \n
    #args/keywords 调用partial时参数
    def partial(func, *args, **keywords):
    def newfunc(*fargs, **fkeywords):
    newkeywords = keywords.copy()
    newkeywords.update(fkeywords)
    return func(*(args + fargs), **newkeywords) #合并,调用原始函数,此时用了partial的参数
    newfunc.func = func
    newfunc.args = args
    newfunc.keywords = keywords
    return newfunc
    \n

    声明:

    urlunquote = functools.partial(urlunquote, encoding='latin1')

    \n

    当调用 urlunquote(args, *kargs)

    \n

    相当于 urlunquote(args, *kargs, encoding='latin1')

    \n

    应用:

    典型的,函数在执行时,要带上所有必要的参数进行调用。

    \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

    当你使用一个装饰器,就是用另一个函数替换当前函数。换一种说法,如果你有一个装饰器:

    \n
    def logged(func):
    def with_logging(*args, **kwargs):
    print func.__name__ + " was called"
    return func(*args, **kwargs)
    return with_logging
    \n

    你这样使用它:

    \n
    @logged
    def f(x):
    """does some math"""
    return x + x * x
    \n

    实际上和这种用法相同:

    \n
    def f(x):
    """does some math"""
    return x + x * x
    f = logged(f)
    \n

    而且你的函数 fwith_loging 替换。不幸的是,这意味着当你使用:

    \n
    print f.__name___
    \n

    它会打印出 with_logging 因为这是你新函数的名字。事实上,如果你查看 f 的 文档字符串,它将是空的,因为 with_logging 没有文档字符串所以你写的文档字符串不会在这里出现。并且,如果你查看这个函数使用 pydoc 生成结果,他的参数列表不是一个参数 x,取而代之的是 *args**kwargs因为这是 with_logging 所持有的。

    \n

    如果使用装饰器总是意味着丢失这个函数的信息,这将是个严重的问题。这就是为什么我们有 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 是它自己的装饰器,下边的代码将会做正确的事情。

    \n
    from functools import wraps
    def logged(func):
    @wraps(func)
    def with_logging(*args, **kwargs):
    print func.__name__ + " was called"
    return func(*args, **kwargs)
    return with_logging

    @logged
    def f(x):
    """does some math"""
    return x + x * x

    print f.__name__ # prints 'f'
    print f.__doc__ # prints 'does some math'
    \n","categories":["Code"],"tags":["Python"]},{"title":"__getattr__() 和 __getattribute__() 方法的区别","url":"/2016/getattr-%E5%92%8C-getattribute-%E6%96%B9%E6%B3%95%E7%9A%84%E5%8C%BA%E5%88%AB/","content":"

    python 在访问属性的方法上定义了__getattr__()__getattribute__() 2种方法,其区别非常细微,但非常重要。

    \n
      \n
    • 如果某个类定义了 __getattribute__() 方法,在 每次引用属性或方法名称时 Python 都调用它(特殊方法名称除外,因为那样将会导致讨厌的无限循环)。
    • \n
    • 如果某个类定义了 __getattr__() 方法,Python 将只在正常的位置查询属性时才会调用它。如果实例 x 定义了属性 colorx.color 将 不会 调用x.__getattr__('color');而只会返回 x.color 已定义好的值。
    • \n
    \n\n

    下边举几个栗子:

    \n

    当一个类没有定义__getattr____getattribute__时,在访问类的实例一个不存在的属性时会报错

    class GetAttrClass(object):
    def __init__(self):
    pass


    if __name__ == '__main__':
    gac = GetAttrClass()
    print gac.x
    \n

    上边程序运行后得到一下错误:

    \n
    Traceback (most recent call last):
    File "/Users/pan/PycharmProjects/test/getattr.py", line 13, in <module>
    print gac.x
    AttributeError: 'GetAttrClass' object has no attribute 'x'
    \n

    当一个类定义__getattr____getattribute__时,在访问类的实例一个不存在的属性时会返回None

    class GetAttrClass(object):
    def __init__(self):
    pass

    def __getattr__(self, item):
    pass


    if __name__ == '__main__':
    gac = GetAttrClass()
    print gac.x
    \n

    程序运行结果为:None

    \n

    当存在已定义好的值后,不再调用__getattr__而是直接返回定义好的值

    class GetAttrClass(object):
    def __init__(self):
    pass

    def __getattr__(self, item):
    if item == 'color':
    return 'red'


    if __name__ == '__main__':
    gac = GetAttrClass()
    print gac.color
    gac.color = 'green'
    print gac.color
    \n

    程序运行结果为:

    \n
    red
    green
    \n
    class GetAttrClass(object):
    def __init__(self):
    self.color = 'black'

    def __getattr__(self, item):
    if item == 'color':
    return 'red'


    if __name__ == '__main__':
    gac = GetAttrClass()
    print gac.color
    gac.color = 'green'
    print gac.color
    \n

    程序运行结果为:

    \n
    black
    green
    \n

    当程序定义__getattribute__后,每次引用属性和方法都会调用它

    class GetAttrClass(object):
    def __init__(self):
    self.color = 'black'

    def __getattribute__(self, item):
    if item == 'color':
    return 'red'


    if __name__ == '__main__':
    gac = GetAttrClass()
    print gac.color
    gac.color = 'green'
    print gac.color
    \n

    程序运行结果为:

    \n
    red
    red
    \n

    即便已经显式地设置 gac.color,在获取 gac.color 的值时, 仍将调用 __getattribute__() 方法。如果存在 __getattribute__() 方法,将在每次查找属性和方法时 无条件地调用 它,哪怕在创建实例之后已经显式地设置了属性。

    \n
    \n

    如果定义了类的 __getattribute__() 方法,你可能还想定义一个 __setattr__() 方法,并在两者之间进行协同,以跟踪属性的值。否则,在创建实例之后所设置的值将会消失在黑洞中。

    \n
    \n

    必须特别小心 __getattribute__() 方法,因为 Python 在查找类的方法名称时也将对其进行调用。

    class GetAttrClass(object):

    def __getattribute__(self, item):
    raise AttributeError

    def hello(self):
    print 'hello world!'


    if __name__ == '__main__':
    gac = GetAttrClass()
    gac.hello()
    \n

    以上程序报错:

    \n
    Traceback (most recent call last):
    File "/Users/pan/PycharmProjects/test/getattr.py", line 17, in <module>
    gac.hello()
    File "/Users/pan/PycharmProjects/test/getattr.py", line 9, in __getattribute__
    raise AttributeError
    AttributeError
    \n
      \n
    • 该类定义了一个总是引发 AttributeError 异常的 __getattribute__() 方法。没有属性或方法的查询会成功。
    • \n
    • 调用 gac.hello() 时,Python 将在 GetAttrClass 类中查找 hello() 方法。该查找将执行整个 __getattribute__()方法,因为所有的属性和方法查找都通过__getattribute__() 方法。在此例中, __getattribute__() 方法引发 AttributeError 异常,因此该方法查找过程将会失败,而方法调用也将失败。
    • \n
    \n","categories":["Code"],"tags":["Python"]},{"title":"GitLab 瘦身方法","url":"/2020/git-lose-weight/","content":"

    \"\"

    \n
    \n

    由于项目代码中存放了一些大文件在 git 仓库中(比如训练后的模型数据),所以最近收到公司的通知,需要给 git 进行瘦身。

    \n

    本文内容是摘自公司的通知。

    \n
    \n

    提前告知

      \n
    • 瘦身将从此库中永久删除此文件,且无法恢复。包括所有“分支”中的引用,所有“Tag”中的引用,连同提交此文件的log记录也一并清除。

      \n
    • \n
    • 请在操作之前将要永久删除的文件备份,并记录目录位置。待瘦身结束后,将此大文件以「LFS」的形式 commit 到此库中。 详见《GitLab lfs 使用》。

      \n
    • \n
    \n

    基本原理

      \n
    1. 先在本地对 git 库瘦身,再镜像推送到 GitLab 新创建的库。

      \n
    2. \n
    3. 待新库测试稳定后,通知管理员将旧的 git 库归档,组内使用新库,新/旧库 rename 互换。

      \n
    4. \n
    \n

    操作步骤:(大库瘦身需要几个小时,请提前注销组员权限)

    1. 需要瘦身的库 git clone –bare 到本地

    git clone --bare https://git.server.com/group/name.git
    \n

    2. 查看 git 库空间大小

    du -sh ./name.git
    \n

    3. 查看历史上哪些文件庞大(检查所有分支)

    cd name.git
    git verify-pack -v ./objects/pack/*.idx | sort -k 3 -n | tail -10
    \n

    查询结果对应关系:<SHA-1> <类型> <size> <size-in-packfile> <offset-in-packfile>

    \n

    如:

    \n
    950dae43f100f6586884893eab3b258a09da1076 blob   173244608 172458659 28056
    d969843d33706a6d1f0d2ef9576ce8baa95d6786 blob 188144087 188196487 204238271
    dd99138acfdbfbe40ce8caed731fbe077f087a82 blob 225264868 208815706 184299
    006dcd011a0a1d31a3066634befcda6c8fd60d0d blob 255858144 236934769 453966523
    50703d1627abf7d3a1a4a6447ae5c001c2cdd263 blob 255858144 236936133 690901292
    92dbd0d8f1eaf303a2deac80cfeb9fa7f3f864f1 blob 255858144 236936851 1640249530
    9631c6903a73bda0d218e7cc19cfc513fbcf01a2 blob 255858144 236935865 1166376569
    b5fe73bc6dcc740e28a4ee414483b0d712dc05fa blob 255858144 236936876 1877186381
    b79e9f47640942b4e3fd035ced4140366b645376 blob 255858144 236937096 1403312434
    fd6ca8cebdb65c89f2d392f3143ba3cdadfdbddd blob 257557808 238539144 927837425
    \n

    4. 查看大文件名称,排名前 10,从小到大,检索 5G 库需要 1 分钟

    git rev-list --objects --all | grep "$(git verify-pack -v ./objects/pack/*.idx | sort -k 3 -n | tail -10 | awk '{print$1}')"
    \n

    5. 删除历史文件,删除5G库需要15分钟(此步永久删除,对所有分支 /tag/log 的删除操作)

    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
    \n

    filter-branch 是让 git 重写每一个分支

    \n
      \n
    • --force 假如遇到冲突也让 git 强制执行。
    • \n
    • --index-filter 重写索引的过滤器。
    • \n
    • --prune-empty 如果修改后的提交为空则扔掉不要。
    • \n
    • --tag-name-filter 表示对每一个 tag 如何重命名,重命名的命令紧跟在后面,当前的 tag
      名会从标注输入送给后面的命令,用 cat 就表示保持 tag 名不变。
      紧跟着的 -- 表示分割符,最后的 --all 表示对所有的分支和 tag 都考虑在内。
    • \n
    \n

    6. 删除GIT缓存记录里的内容

    rm -rf ./refs/original/
    \n

    7. 对 git log 处理,任何时间运行 git reflog 命令可以查看当前的状态

    git reflog expire --expire=now --all
    \n

    8. 在进行 repack 前需要将所有对这些 commits 的引用去除

    git repack -A -d
    \n

    9. 执行 gc 压缩

    git gc --aggressive --prune=now
    \n

    --aggressive 最大限度的压缩,会比较缓慢

    \n

    10. 检查完整性

    git fsck --full --unreachable
    \n

    11. 再次查看 .git 空间大小

    du -sh ../name.git
    \n

    联系 gitlab 管理员

      \n
    1. 联系管理员创建新的 git 库
    2. \n
    3. 将瘦身后的 git 库镜像推送到 gitlab
        \n
      • git push --mirror https://git.server.com/group/name_new.git
      • \n
      \n
    4. \n
    5. 测试使用新的库
    6. \n
    7. 将旧库 rename 并归档,新库 rename 成旧库名字
    8. \n
    9. 将大文件以 LFS 形式 commit 到新库中
    10. \n
    11. 恢复新库的人员权限,通知大家使用
    12. \n
    \n
    \n

    GitLab LFS 使用方法

    Linux 安装

    git lfs 要求 git >= 1.8.2

    \n
    yum install git-lfs -y
    \n

    MacOS 安装

    运行 brew install git-lfs 即可

    \n

    Windows 安装

    git 版本大于 2.12

    \n

    关闭 Windows 的 ssl 校验

    \n
    git config --global http.sslVerify false
    \n

    \"\"

    \n

    申请 git lfs 仓库

    走流程申请一个 aritfactory –git lfs 仓库

    \n

    使用方法

    告诉 lfs 需要管理的大文件

    比如 3.pdf,运行命令 git lfs track 3.pdf,会产生 git lfs 管理文件 .gitattributes

    \n

    支持通配符比如 git lfs track *.exe

    \n

    添加 .lfsconfig 文件,指定 git lfs 文件存放位置

    我申请的 git lfs 仓库叫做 git-lfs

    \n

    登陆 aritfactory 后,如下操作:

    \n

    \"\"

    \n
      \n
    1. 同时提交 .gitattributes.lfsconfig3.pdf,然后在 gitlab 中查看
    2. \n
    \n

    \"\"

    \n"},{"title":"git 子模块使用","url":"/2021/git-submodule/","content":"

    前几天调研了一下 hugo,准备后边把自己的博客迁移过去。hugo 教程中新增主题推荐通过 git 子模块的方式(前提是原始文件就已经在一个 git 项目下),比如我要新增一个名字叫 zen 的主题,可以通过以下命令进行安装:

    \n
    git submodule add https://github.com/frjo/hugo-theme-zen.git themes/zen
    \n

    这个命令会将主题仓库中的文件 clone 到 themes/zen 路径下,同时会在我的的仓库根路径下新建一个 .gitmodules 文件,之后 add 其他子模块时,会往这个文件中追加数据,格式如下:

    \n
    [submodule "themes/zen"]
    \tpath = themes/zen
    \turl = https://github.com/frjo/hugo-theme-zen.git themes/zen
    [submodule "themes/cleanwhite"]
    \tpath = themes/cleanwhite
    \turl = https://github.com/zhaohuabing/hugo-theme-cleanwhite.git
    [submodule "themes/anatole"]
    \tpath = themes/anatole
    \turl = https://github.com/lxndrblz/anatole
    [submodule "themes/jane"]
    \tpath = themes/jane
    \turl = https://github.com/xianmin/hugo-theme-jane.git
    \n

    我们需要把 .gitmodules 文件加入到 git 的版本控制中。

    \n

    子模块常用管理命令

    更新子模块:

    git submodule update --recursive --remote
    \n

    在新环境中拉取所有子模块代码

    首次在一个新的环境中 clone 我们的仓库后是不带子模块代码的,可以通过下边这个命令来把所有子模块代码拉下来:

    \n
    git submodule update --init --recursive
    \n","tags":["git"]},{"title":"Go 中 5 个常见错误","url":"/2021/go-5-normally-mistakes/","content":"

    1. 循环中

    使用循环时下边几个容易尝试混乱的编码方式我们要尽量避免。

    \n

    1.1 对循环的变量进行引用

    考虑到效率,在进行循环遍历过程中,迭代出的变量会赋值到同一个地址。这可能会导致无意识的错误。

    \n
    in := []int{1, 2, 3}

    var out []*int
    for _, v := range in {
    \tout = append(out, &v)
    }

    fmt.Println("Values:", *out[0], *out[1], *out[2])
    fmt.Println("Addresses:", out[0], out[1], out[2])
    \n

    以上代码得到的结果是:

    \n
    Values: 3 3 3
    Addresses: 0xc000014188 0xc000014188 0xc000014188
    \n

    原因很容易解释:每次迭代时我们将 v 的地址追加到 out 切片中,前边提到,v 在每次遍历时为同一个变量,在输出的第二行可以看到打印出了相同的地址。

    \n

    简单的修复方法是,将每一次的迭代出的变量复制给一个新的变量:

    \n
    in := []int{1, 2, 3}

    var out []*int
    for _, v := range in {
    \tv := v
    \tout = append(out, &v)
    }

    fmt.Println("Values:", *out[0], *out[1], *out[2])
    fmt.Println("Addresses:", out[0], out[1], out[2])
    \n

    输出:

    \n
    Values: 1 2 3
    Addresses: 0xc0000b6010 0xc0000b6018 0xc0000b6020
    \n

    同样的问题会出现在将迭代出的变量用在 Goroutine 中:

    \n
    list := []int{1, 2, 3}

    for _, v := range list {
    \tgo func() {
    \t\tfmt.Printf("%d ", v)
    \t}()
    }
    \n

    输出:

    \n
    3 3 3
    \n

    这个 bug 也可以使用上边提到的方法解决。(注:如果不在 Goroutine 中执行,上边的代码是没有问题的)

    \n

    1.2 在循环中调用 WaitGroup.Wait

    下边代码循环中的 group.Wait() 会被阻塞,导致无法执行后边的循环。

    \n
    var wg sync.WaitGroup
    wg.Add(len(tasks))
    for _, t := range tasks {
    \tgo func(t *task) {
    \t\tdefer group.Done()
    \t}(t)
    \tgroup.Wait()
    }
    \n

    正确的写法是把 Wait() 放在循环外:

    \n
    var wg sync.WaitGroup
    wg.Add(len(tasks))
    for _, t := range tasks {
    \tgo func(t *task) {
    \t\tdefer group.Done()
    \t}(t)
    }

    group.Wait()
    \n

    1.3 在循环中使用 defer

    只有当函数返回时,defer 才会被执行。除非你知道你在做什么,否则不应该将 defer 用在循环中。

    \n
    var mutex sync.Mutex
    type Person struct {
    \tAge int
    }
    persons := make([]Person, 10)
    for _, p := range persons {
    \tmutex.Lock()
    \tdefer mutex.Unlock()
    \tp.Age = 13
    }
    \n

    在上边的例子中,在完成第一次循环后,之后的循环无法获得互斥锁从而被阻塞。应该改成下边的显性释放锁的方式:

    \n
    var mutex sync.Mutex
    type Person struct {
    \tAge int
    }
    persons := make([]Person, 10)
    for _, p := range persons {
    \tmutex.Lock()

    \tp.Age = 13
    \tmutex.Unlock()
    }
    \n

    如果你确实需要在循环中使用 defer,可以考虑将工作委托给另一个函数:

    \n
    var mutex sync.Mutex
    type Person struct {
    \tAge int
    }
    persons := make([]Person, 10)
    for _, p := range persons {
    \tfunc() {
    \t\tmutex.Lock()
    \t\tdefer mutex.Unlock()
    \t\tp.Age = 13
    \t}()
    }
    \n

    2. 往 unbuffered channel 中发送数据

    func doReq(timeout time.Duration) obj {
    \tch :=make(chan obj)
    \tgo func() {
    \t\tobj := do()
    \t\tch <- obj
    \t} ()
    \tselect {
    \tcase result = <- ch :
    \t\treturn result
    \tcase<- time.After(timeout):
    \t\treturn nil
    \t}
    }
    \n

    上边的代码模拟这样一个行为:超时前获得到结果将结果返回,若超时则返回 nil。

    \n

    我们通过一个 Goroutine 异步获取结果,并通过一个 channel 配合 select 来阻塞代码往后执行。

    \n

    上边代码使用了 unbuffered channel,这会导致的问题是,如果代码因超时提前返回了,Goroutine 在获取到结果后,会阻塞在 ch <- obj 这一行(因为没有其他的 Goroutine 来读取这个 channle),从而这个 Goroutine 无法退出,进而会发生 Goroutine 泄露。

    \n

    解决方法是使用一个长度为 1 的 buffered channel

    \n
    func doReq(timeout time.Duration) obj {
    \tch := make(chan obj, 1)
    \tgo func() {
    \t\tobj := do()
    \t\tch <- result
    \t} ()
    \tselect {
    \tcase result = <- ch :
    \t\treturn result
    \tcase<- time.After(timeout):
    \t\treturn nil
    \t}
    }
    \n

    还有一种修复方式是在 Goroutine 中使用一个 select 配合一个空的 default

    \n
    ...
    select {
    case ch <- result:
    default:
    }
    ...
    \n

    当没有其他 Goroutine 来读取这个 channel 时,会走到 default 行为,这个 Goroutine 也就可以正常退出了。

    \n

    3. 不使用接口

    接口可以使代码更具灵活性,是在代码中引入多态的一种方法。接口允许我们关注一组行为而非特定类型。不使用接口不会有错误产生,但会让我们的代码看起来不那么优雅、不具有可扩展性。

    \n

    在众多接口中,io.Readerio.Writer 可能是最受欢迎的一对。

    \n
    type Reader interface {
    Read(p []byte) (n int, err error)
    }

    type Writer interface {
    Write(p []byte) (n int, err error)
    }
    \n

    这些接口非常强大, 假设我们需要将一个对象写入一个文件,可以这样定义一个 Save 方法:

    \n
    func (o *obj) Save(file os.File) error
    \n

    如果明天我们有需要将这个文件写入 http.ResponseWriter 呢?我们可不想重新定义一个新的方法,这时 io.Writer 就派上用场了:

    \n
    func (o *obj) Save(w io.Writer) error
    \n

    还需明白的一点是:我们应该只关心我们要使用的行为。在上边的例子中,使用 io.ReadWriteCloser 虽然也行得通,但如果我们只用到了 Write 方法,就不是特别好的实践了。接口面积越大,抽象能力越弱。

    \n

    因此,在大部分情况下,我们应关注行为而不是具体类型。

    \n

    4. struct 中未考虑字段声明顺序

    下边的代码不会出现错误,但会有使用更多的内存:

    \n
    type BadOrderedPerson struct {
    \tVeteran bool // 1 byte
    \tName string // 16 byte
    \tAge int32 // 4 byte
    }
    \n

    上边的 struct 看起来会分配 21 bytes 的内存,但实际上分配的是 32 bytes。出现这个情况原因是数据结构对齐。在 64 位架构中,内存以 8 bytes 为一个连续单元,改成下边的声明顺序可以优化到分配 24 bytes:

    \n
    type OrderedPerson struct {
    \tName string
    \tAge int32
    \tVeteran bool
    }
    \n

    在频繁使用不合理字段顺序的类型时,会导致额外的内存开销。

    \n

    不过,我们也不必手动计算和优化结构体内存,可以使用 go tool 提供的 fieldalignment 工具来检测并修复不合理的声明顺序。

    \n

    fieldalignment 安装:

    cd $GOPATH
    git clone git@github.com:golang/tools.git src/golang.org/x/tools
    src/golang.org/x/tools/go/analysis/passes/fieldalignment/cmd/fieldalignment
    go install
    \n

    fieldalignment 使用

    ➜ fieldalignment .
    /Users/jiapan/Projects/tantan-live-distribution/app/domain/recommend_service.go:179:30: struct of size 88 could be 80
    /Users/jiapan/Projects/tantan-live-distribution/app/domain/voice_recommend_service.go:63:35: struct with 40 pointer bytes could be 24

    // 修复字段顺序
    ➜ fieldalignment -fix .
    \n

    5. test 时未使用 race 检测器

    数据竞争会导致一些很迷的问题,而且通常是在部署一段时间后才会发生。所以此类问题在并发系统中是最常见而且最难排查的 bug。为了更方便找出此类 bug,Go 1.1 中引入了一个内置的数据竞争检测器,只需加上 -race 标识就可以了。

    \n
    $ go test -race pkg    // to test the package
    $ go run -race pkg.go // to run the source file
    $ go build -race // to build the package
    $ go install -race pkg // to install the package
    \n

    当开启竞争检测器时,编译器会记录代码对内存进行了何时、何种方式的访问,同时 runtime 监控共享变量的非同步访问。

    \n

    发现数据竞争时,竞争检测器会打印包含访问冲突的调用栈记录,如下所示:

    \n
    WARNING: DATA RACE
    Read by goroutine 185:
    net.(*pollServer).AddFD()
    src/net/fd_unix.go:89 +0x398
    net.(*pollServer).WaitWrite()
    src/net/fd_unix.go:247 +0x45
    net.(*netFD).Write()
    src/net/fd_unix.go:540 +0x4d4
    net.(*conn).Write()
    src/net/net.go:129 +0x101
    net.func·060()
    src/net/timeout_test.go:603 +0xaf
    Previous write by goroutine 184:
    net.setWriteDeadline()
    src/net/sockopt_posix.go:135 +0xdf
    net.setDeadline()
    src/net/sockopt_posix.go:144 +0x9c
    net.(*conn).SetDeadline()
    src/net/net.go:161 +0xe3
    net.func·061()
    src/net/timeout_test.go:616 +0x3ed
    Goroutine 185 (running) created at:
    net.func·061()
    src/net/timeout_test.go:609 +0x288
    Goroutine 184 (running) created at:
    net.TestProlongTimeout()
    src/net/timeout_test.go:618 +0x298
    testing.tRunner()
    src/testing/testing.go:301 +0xe8
    \n
    \n
    \n

    写在最后:人类从历史中学到的唯一教训,就是人类无法从历史中学到任何教训。

    \n
    \n"},{"title":"Docker 部署 Go 服务并实现热加载","url":"/2019/go-docker-reload/","content":"

    Docker 足够轻量、也非常易用,并且可以确保我们所有的运行环境保持一致。

    \n

    在这篇文章中,我将通过创建 Docker 容器来部署一个 Go API 服务。当我对源码进行修改时,这个 Go 服务也会立即重新加载。

    \n

    通过这个方式我们就不需要再在开发过程中多次重新编译 Docker 镜像了。

    \n

    \"\"

    \n

    创建 Go 模块

    官方在 Go 的 1.13 版本中介绍了模块的概念。这意味着我们不再需要把整个工程放在 Go 的工作空间下了。

    \n

    开始前,我创建一个新的目录 go-docker 来放置所有文件。

    \n

    然后初始化一个 Git 仓库并创建 Go 模块。

    \n
    git init
    git remote add origin git@github.com:Panmax/go-docker.git
    go mod init github.com/Panmax/go-docker
    \n

    你会看到在项目目录下出现了一个 go.mod 文件。这个文件将存有这个模块下所有的依赖,类似于 Node 开发中用到的 package.json 或 Python 中的 requirements.txt

    \n

    构建 API

    模块设置好了,现在来构建一个简单的 API 服务。

    \n

    我准备在构建这个 API 服务时使用 gorilla/mux 路由包。我也可以只用 Go 中提供的标准模块来实现路由,但我想确保模块依赖可以按照预期工作,并且利用 mux 可以支持我们构建更加复杂的应用。

    \n
    go get -u github.com/gorilla/mux
    \n

    执行这个命令后,你会看到它被作为依赖写入了 go.mod 文件。

    \n
    ### module github.com/Panmax/go-docker

    go 1.13

    require github.com/gorilla/mux v1.7.3 // indirect
    \n

    接下来,创建这个 Go 项目的主文件 commands/runserver.go

    \n
    package main

    import (
    \t"fmt"
    \t"github.com/gorilla/mux"
    \t"net/http"
    )

    func main() {
    \tr := mux.NewRouter()

    \tr.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
    \t\tfmt.Fprintf(w, "Hello, World!")
    \t})

    \tfmt.Println("Server listening!")
    \thttp.ListenAndServe(":80", r)
    }
    \n

    这个 API 只是简单返回一条消息:「Hello World!」

    \n

    在把这个程序放进 Docker 容器前我们最好先来测试一下。通过 go run 命令来运行这个服务。

    \n
    go run commands/runserver.go
    Server listening!
    \n

    \"\"

    \n

    API 服务可以正常工作。

    \n

    配置 Docker

    我们开始为这个项目构建 Docker 镜像。Docker 镜像包含一组用来告诉 Docker 需要提供什么环境的指令。

    \n
    FROM golang:latest

    WORKDIR /app

    COPY ./ /app

    RUN go mod download

    ENTRYPOINT go run commands/runserver.go
    \n

    使用 golang:latest 镜像作为这个自定义镜像的基础镜像。这样就可以免去 Go 开发环境的配置。

    \n

    将整个项目拷贝到了镜像的 /app 目录下,然后通过 go mod download 下载依赖。

    \n

    最后,我们告诉 Docker 执行 go run commands/runserver.go 命令来启动服务。

    \n

    执行以下命令来构建这个镜像:

    \n
    docker build -t go-docker-image .
    \n

    现在我已经构建好了 Docker 镜像,接下来我们实际启动一下这个 Docker。

    \n
    docker run go-docker-image
    Server listening!
    \n

    服务已经监听在了 Docker 容器中,但是当我通过浏览器中打开 localhost 时却发现无法访问。

    \n

    \"\"

    \n

    出现这个情况的原因是,虽然程序在 Docker 容器内监听了 80 端口的传入请求,但是它并没有在宿主机的 80 端口上进行监听。因此我们给 localhsot 发送一个 GET 请求,它是找不到正在运行的服务的。

    \n

    我用一张逻辑图来表述一下这个问题:

    \n

    \"\"

    \n

    为了解决这个问题,我们需要把容器内的 80 端口映射到主机的 80 端口。

    \n
    docker run -p 80:80 go-docker-image
    \n

    端口映射后的逻辑图如下:

    \n

    \"\"

    \n

    现在再来访问 localhost,就可以看到「Hello World!」显示在了页面上。

    \n

    修改源码

    我们来对这个 API 做一点调整:

    \n
    package main

    import (
    \t"fmt"
    \t"github.com/gorilla/mux"
    \t"net/http"
    )

    func main() {
    \tr := mux.NewRouter()

    \tr.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
    \t\tfmt.Fprintf(w, "Hello, World!\\n世界,你好!")
    \t})

    \tfmt.Println("Server listening!")
    \thttp.ListenAndServe(":80", r)
    }
    \n

    我在这个 API 的返回结果中新加了一行消息,我们再来启动一个新的 Docker 容器。

    \n
    docker run -p 80:80 go-docker-image
    \n

    但是如果我现在访问 localhost,看到的仍然是旧消息。

    \n

    \"\"

    \n

    这是因为 Docker 镜像没有变化。为了使变更生效,我们必须重新构建这个镜像。

    \n
    docker build -t go-docker-image .
    docker run -p 80:80 go-docker-image
    \n

    现在就可以看到更新后的消息了。

    \n

    \"\"

    \n

    配置热加载

    每次对代码修改后,重新构建 Docker 镜像会花费很长时间,我们来让这个系统更好用一点。

    \n

    我要使用的是 Compile Daemon 包。如果有任何 Go 源码发生了变更,这个包会重新编译并重启我们的 Go 程序。

    \n
    FROM golang:latest

    WORKDIR /app

    COPY ./ /app

    RUN go mod download

    RUN go get github.com/githubnemo/CompileDaemon

    ENTRYPOINT CompileDaemon --build="go build commands/runserver.go" --command=./runserver
    \n

    我修改了 Dockerfile 来下载 CompileDaemon 包。

    \n

    之后修改了 ENTRYPOINT 后面的命令来运行 CompileDaemon 程序,同时为它指定了项目编译和服务启动命令。每次有文件变化后,以上命令就会被执行。

    \n

    重新编译这个镜像:

    \n
    docker build -t go-docker-image .
    \n

    启动 Docker 时,我添加了 -v ~/Projects/go-docker:/app 参数。这样就可以把我本机的 go-docker 目录挂载到 Docker 容器内的 /app 目录下了。

    \n

    当我修改了本机 go-docker 目录内的文件时,容器 /app 目录下的文件也会变化。

    \n

    最终的启动命令如下。

    \n
    docker run -v ~/Projects/go-docker:/app -p 80:80 go-docker-image
    \n

    容器运行过程中,尝试修改源码,你会看到更改自动生效了。

    \n
    2019/11/20 11:59:53 Running build command!
    2019/11/20 11:59:53 Build ok.
    2019/11/20 11:59:53 Hard stopping the current process..
    2019/11/20 11:59:53 Restarting the given command.
    2019/11/20 11:59:53 stdout: Server listening!
    2019/11/20 12:00:24 Running build command!
    2019/11/20 12:00:25 Build ok.
    2019/11/20 12:00:25 Hard stopping the current process..
    2019/11/20 12:00:25 Restarting the given command.
    2019/11/20 12:00:25 stdout: Server listening!
    \n

    使用 Docker Compose

    每次运行容器时,我都要输入很长的启动命令:docker run -v ~/Projects/go-docker:/app -p 80:80 go-docker-image。在这个项目中倒没有太大问题,毕竟我才只有一个容器要启动。

    \n

    但假设我有一个需要启动很多容器的项目,执行多个 docker run 命令会非常麻烦。

    \n

    解决方案是使用 Docker Compose。利用这个工具,我们可以指定运行 docker-compose up 命令时要启动哪些容器。

    \n

    为了配置它,我们需要创建一个 docker-compose.yml 文件:

    \n
    version: "3"
    services:
    go-docker-image:
    build: ./
    ports:
    - '80:80'
    volumes:
    - ./:/app
    \n

    这里,我声明了要创建一个名为 go-docker-image 的镜像。这个镜像使用当前目录下的 Dockerfile 来构建。同时我配置了端口映射和目录挂载。

    \n

    执行 docker-compose up 来启动 docker-compose.yml 中指定的容器。

    \n

    现在,我有了一个运行在 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 来维护的。

    \n
    func fillNewUserLiveRightAttribute(ctx context.Context, userLiveRight *po.UserLiveRight, right *po.LiveRight) error {
    \tif right.Type == util.LiveRightTypeMystery {
    \t\tvar incrId int64
    \t\t// 跳过 ID 中包含 4 的情况
    \t\tfor {
    \t\t\tincrId, err := cache.GetUserLiveRightCacheRepository().GetUserLiveRightIncrID(ctx, right.ID)
    \t\t\tif err != nil {
    \t\t\t\treturn err
    \t\t\t}

    \t\t\tincrIdStr := strconv.FormatInt(incrId, 10)

    \t\t\tif !strings.Contains(incrIdStr, "4") {
    \t\t\t\tbreak
    \t\t\t}
    \t\t}
    \t\tuserLiveRight.Attribute = &po.UserLiveRightAttribute{ID: incrId}
    \t}
    \treturn nil
    }
    \n

    但是之后发现填充进去的 ID 永远是 0,检查了一下 Redis 中 那个自增 ID 也确实存在。

    \n

    这里当时还饶了一下远路,因为那个 Attribute 字段在数据库中使用 jsonb 存储的,所以我前期先检查了插入时执行的 SQL 语句,发现每次 Attribute 都是打印的 {},就以为是我自己没有赋上值。真实的原因是我指定了 Attribute 中的 ID 字段在转 Json 时启用 omitempty ,即:

    \n
    type UserLiveRightAttribute struct {
    ID int64 `json:"id,omitempty"`
    }
    \n

    使用 omitempty 可以告诉 Marshal 函数如果 field 的值是对应类型的 zero-value,那么序列化之后的 JSON object 中不包含此 字段,所以 ID=0 转 Json 后自然就没有这个字段了。

    \n

    回到为啥上边的代码拿到的 ID 总是 0 的问题:因为 for 中赋值的 incrId 是在一个新的作用域内,只在 for 的花括号内有效,退出 for 后拿到的是最开始初始化 incrId 的 0 值,这里使用的 := 进行的赋值,因为 err 是个新字段,所以并没有提示错误。

    \n

    修复方法很简单,err 也在作用域外声明,里边使用 = 来赋值。

    \n
    func fillNewUserLiveRightAttribute(ctx context.Context, userLiveRight *po.UserLiveRight, right *po.LiveRight) error {
    \tif right.Type == util.LiveRightTypeMystery {
    \t\tvar incrId int64
    \t\tvar err error
    \t\t// 跳过 ID 中包含 4 的情况
    \t\tfor {
    \t\t\tincrId, err = cache.GetUserLiveRightCacheRepository().GetUserLiveRightIncrID(ctx, right.ID)
    \t\t\tif err != nil {
    \t\t\t\treturn err
    \t\t\t}

    \t\t\tincrIdStr := strconv.FormatInt(incrId, 10)

    \t\t\tif !strings.Contains(incrIdStr, "4") {
    \t\t\t\tbreak
    \t\t\t}
    \t\t}
    \t\tuserLiveRight.Attribute = &po.UserLiveRightAttribute{ID: incrId}
    \t}
    \treturn nil
    }
    \n"},{"title":"make slice 后 append 产生的问题","url":"/2020/go-make-slice-question/","content":"
    \"\"
    \n\n

    今天又踩到了一个 go 语言的坑,其实也不算坑,本质上还是自己对这门语言的不熟悉。

    \n

    来看一下我犯的错误,直接上代码:

    \n
    func Int64ToStrings(ids []int64) []string {
    \tstrs := make([]string, len(ids))
    \tfor i, id := range ids {
    \t\tstrs = append(strs, strconv.FormatInt(id, 10))
    \t}
    \treturn strs
    }
    \n

    我的需求很简单,将一个 int64 的切片转为 string 类型的切片,写这段代码的时候想到可以预先分配 slice 的大小,所以写了开头的 make,后边就直接逐个将转为 string 的元素 append 进去了。

    \n

    但是,make 时实际已经帮我完成了里边指定数量元素的初始化,即:

    \n
    s = make([]string, 5) // s == []string{"", "", "", "", ""}
    \n

    所以我之后再往里边 append 时是往最后一个空字符串后边追加元素。

    \n

    修复后的代码如下:

    \n
    func Int64ToStrings(ids []int64) []string {
    \tstrs := make([]string, len(ids))
    \tfor i, id := range ids {
    \t\tstrs[i] = strconv.FormatInt(id, 10)
    \t}
    \treturn strs
    }
    \n

    或者在初始化时指定 slice 长度为0,容量为我们需要的长度:

    \n
    func Int64ToStrings(ids []int64) []string {
    \tstrs := make([]string, 0, len(ids))
    \tfor i, id := range ids {
    \t\tstrs = append(strs, strconv.FormatInt(id, 10))
    \t}
    \treturn strs
    }
    \n","tags":["go"]},{"title":"Go 中由切片引起的内存泄露","url":"/2021/go-memory-leak-by-slice/","content":"

    与 C/C++ 不同,Go 有 GC,所以我们不需要手动处理内存的分配和释放。不过,我们仍然应该谨慎对待内存泄漏问题。

    \n

    来看一个由 slice 引起的内存泄漏案例。

    \n
    package main
    import (
    \"fmt\"
    )
    type Object struct {}
    func main() {
    var a []*Object
    for i := 0; i < 8; i++ {
    a = append(a, new(Object))
    }
    fmt.Println(cap(a), len(a)) // 输出: 8, 8
    a = remove(a, 5)
    fmt.Println(cap(a), len(a)) // 输出: 8, 7
    }
    func remove(s []*Object, i int) []*Object {
    return append(s[:i], s[i+1:]...)
    }
    \n

    我们可以看到,即使有一个对象被删除,a 的容量仍然是8,这意味着remove 函数可能导致潜在的内存泄漏。

    \n

    为什么会发生这种情况?

    来看一个例子

    \n
    package main
    import (
    \"fmt\"
    )
    func main() {
    // a 和 b 代表同一个数组 [1,2] 的两个部分
    a := []int{1,2}
    b := a[0:1]
    fmt.Println(a, b) // 输出: [1 2] [1]

    \t// 底层数组的容量是2,b在 append 后的长度将是2,只需将b的范围增加到数组[0:1]。
    \t// array[1]将被改为3,因为 a 和 b 是在同一个数组上,所以a[1]也是3。
    b = append(b, 3)
    fmt.Println(a, b) // 输出: [1 3] [1 3]

    \t// 因为 b 的长度将比数组的容量大3,所以将创建一个新的数组
    \t// 新数组的容量将是 2*cap(old) = 4
    b = append(b, 4)
    b[0] = 0
    // 现在 a 和 b 在不同的数组上
    fmt.Println(a, b) // 输出: [1 3] [0 3 4]
    fmt.Println(cap(a), cap(b)) // 输出: 2 4
    }
    \n

    如何避免内存泄漏?

    在这种情况下,有两种内存泄漏。

    \n

    1. 底层数组

    底层数组的容量只会增加,但不会减少,第一个例子已经证明了这一点。

    \n

    如果我们认为容量太大,我们可以创建一个新的 slice,并将原 slice 中的所有元素复制到新 slice 中。这是一个复制操作(时间)和内存使用(空间)之间的权衡。

    \n
    func remove(s []*Object, i int) []*Object {
    s = append(s[:i], s[i+1:]...)
    a := make([]*Object, len(s))
    copy(a, s) // 时间换空间
    return a
    }
    \n

    2. 指向数组元素的内存,其类型为指针

    解决方法:将未使用的元素设置为nil,它将会被 GC 释放。

    \n
    func remove(s []*Object, i int) []*Object {
    old := s
    s = append(s[:i], s[i+1:]...)
    old[len(old)-1] = nil
    return s
    }
    \n"},{"title":"Go Struct 不指定 JSON tag 时的默认规则","url":"/2023/go-struct-with-json-tag/","content":"

    Golang 在序列化和反序列化一个 Struct 时,如果指定了 JSON tag 会严格按照指定的 tag 内容来执行,在没有指定 tag 或 tag 大小写不精准时,会有一些默认规则。

    \n

    序列化

    序列化的情况比较简单:

    \n
      \n
    • 指定了 tag 的可导出字段,按照 tag 的命名进行序列化
    • \n
    • 没有指定 tag 的但可以导出的字段(首字母大写)会完全按照变量命名来进行序列化
    • \n
    \n
    type A struct {
    \tCase int
    \tcasE int
    \tCas_E int
    \tCaSE int `json:\"ok\"`
    }

    func main() {
    \ta := A{1, 2, 3, 4}
    \ts, _ := json.Marshal(&a)
    \tmt.Println(string(s))
    }
    \n

    上边这段代码输出:

    \n
    {\"Case\":1,\"Cas_E\":3,\"ok\":4}
    \n
      \n
    • casE 这个字段没有输出,原因是因为他是个不可导出的私有字段,即使设置了 tag 也不可序列化。
    • \n
    • CaSE 序列话后的 key 为 ok 是因为我们给它指定了 tag
    • \n
    • 其余字段都是按照我们原本的拼写格式进行的输出
    • \n
    \n

    反序列化

    序列化的情况稍微有点复杂,其整体的优先级为:

    \n
      \n
    • 先按 tag 匹配,后按字段名匹配
    • \n
    • 有 tag 的仅匹配 tag,没有tag 的可参与字段名匹配
    • \n
    • 先精确匹配,后模糊匹配
    • \n
    • 多个模糊匹配的按照声明在前的匹配
    • \n
    \n

    我们看几个例子:

    \n

    情况1,带 tag 的两个字段都无法匹配上(精准匹配+模糊匹配),不带 tag 的两个字段都可以模糊匹配上,优先赋值给前边声明的字段:

    \n
    type B struct {
    \tCase int `json:\"a\"`
    \tCaSE int `json:\"b\"`
    \tCasE int
    \tCaSe int
    }

    func main() {
    \ts := []byte(`{\"CAsE\":2}`)
    \tvar b B
    \tjson.Unmarshal(s, &b)
    \tfmt.Printf(\"%#v\\\\n\", b)
    }
    // 输出:main.B{Case:0, CaSE:0, CasE:2, CaSe:0}
    \n

    情况2,带 tag 的其中一个字段可以模糊匹配上:

    \n
    type B struct {
    \tCase int `json:\"case\"`
    \tCaSE int `json:\"b\"`
    \tCasE int
    \tCaSe int
    }

    func main() {
    \ts := []byte(`{\"CAsE\":2}`)
    \tvar b B
    \tjson.Unmarshal(s, &b)
    \tfmt.Printf(\"%#v\\\\n\", b)
    }
    // 输出:main.B{Case:2, CaSE:0, CasE:0, CaSe:0}
    \n

    情况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 那套东西搬来用导致的问题。

    \n

    为了简化,我就不把业务代码搬上来了,写个简单的示例:

    \n
    package main

    import (
    \t"encoding/json"
    \t"fmt"
    )

    type Student struct {
    \tID int
    \tName string
    \tAge int
    }

    func main() {

    \tvar students []Student
    \tstudents = append(students, Student{
    \t\tID: 1,
    \t\tName: "张三",
    \t\tAge: 18,
    \t})
    \tstudents = append(students, Student{
    \t\tID: 2,
    \t\tName: "李四",
    \t\tAge: 19,
    \t})
    \tstudents = append(students, Student{
    \t\tID: 3,
    \t\tName: "王五",
    \t\tAge: 20,
    \t})

    \tstudentMap := make(map[int]*Student, len(students))
    \tfor _, student := range students {
    \t\tstudentMap[student.ID] = &student
    \t}

    \tbs, _ := json.Marshal(studentMap)
    \tfmt.Println(string(bs))

    }
    \n

    上边代码输出如下:

    \n
    {"1":{"ID":3,"Name":"王五","Age":20},"2":{"ID":3,"Name":"王五","Age":20},"3":{"ID":3,"Name":"王五","Age":20}}
    \n

    可以看到所有的 value 是同一个 Student,为什么会出现这样的问题呢?因为 students 存储的是 Student 的值,在给 for 循环中的 student 赋值时,是复制了一个新的值给它,而 for 循环中的 student 变量所指向的地址是不变的。

    \n

    可以打印 student 的地址看一下:

    \n
    for _, student := range students {
    \tfmt.Printf("%p \\n", &student)
    \tstudentMap[student.ID] = &student
    }
    \n

    输出为:

    \n
    0xc0000a6040 
    0xc0000a6040
    0xc0000a6040
    {"1":{"ID":3,"Name":"王五","Age":20},"2":{"ID":3,"Name":"王五","Age":20},"3":{"ID":3,"Name":"王五","Age":20}}
    \n

    这种情况下我们应该用 students 中索引对应数据的指针,上边 for 循环修改如下:

    \n
    for i, student := range students {
    \tfmt.Printf("%p \\n", &students[i])
    \tstudentMap[student.ID] = &students[i]
    }
    \n

    输出为:

    \n
    0xc0000b8000 
    0xc0000b8020
    0xc0000b8040
    {"1":{"ID":1,"Name":"张三","Age":18},"2":{"ID":2,"Name":"李四","Age":19},"3":{"ID":3,"Name":"王五","Age":20}}
    \n

    上边的情况给 student 赋值也是有问题的:

    \n
    for _, student := range students {
    \tstudent.Name = "test"
    }

    bs, _ := json.Marshal(students)
    fmt.Println(string(bs))
    \n

    输出:

    \n
    [{"ID":1,"Name":"张三","Age":18},{"ID":2,"Name":"李四","Age":19},{"ID":3,"Name":"王五","Age":20}]
    \n
    \n

    Java 写习惯了就以为迭代时的 student 指向的是 students 中的地址。

    \n"},{"title":"GoLand Protobuf 语法检查","url":"/2020/goland-protobuf-syntax-check/","content":"
    \"\"
    \n\n

    公司的服务间调用使用的 gRPC,所以开发过程中需要写一些 .proto 文件来生成 pb,写的时候借助语法检查可以更高效一些。

    \n

    GoLand 中提供了一个叫 Protoco Buffer Editor 的插件,但是这个插件在我的环境中是有 bug 的,无法处理 import 进来的包。

    \n

    询问同事他们的都没有问题,所以我从网上查了一下这个问题,网上推荐的插件名叫 Protobuf Support,显然我的 IDE 的插件市场中没有这个插件,又借助搜索引擎找到了这个插件的描述页面,看到已经被打上 deprecated 的标签了。

    \n
    \"\"
    \n\n\n

    我猜测这个插件之前可以用,后来被官方下掉了,我的同事们是在下掉之前安装的,为了印证我的想法,让其中一个同事看了一下他的插件名,果然是 Protobuf Support

    \n

    好歹顺着官方页面找到了GitHub 的地址 https://github.com/ksprojects/protobuf-jetbrains-plugin ,又在 Releases 页面中找到了插件的压缩包,通过压缩包在本地进行了安装,并 disable 了 Protoco Buffer Editor 插件,重启 GoLand 后问题解决。

    \n
    \n

    今天一下午光处理坑了,上线了一个限流功能,一直没有拿到打点数据,和治理组同事检查了所有地方都没有问题,最后治理组同事想起来,需要触发限流后才会打点,在监控中看到数据。

    \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

    AirPods Pro2

    戴上它,播放你喜欢的音乐,不管周围多么嘈杂,仿佛整个世界都是你的。

    \n

    在地铁上,戴着它不播放音乐或者播放轻音乐,捧起一本书,享受一段安静的阅读时光。

    \n

    Apple Watch

    我最贴身的助理。

    \n

    我的是S6,已经用了3年了,感觉还能再战3年。如果觉得旧了,30元左右在淘宝买个新表带,换上后跟新的一样。

    \n

    HHKB 键盘

    最适合程序员使用的键盘,没有之一。

    \n

    如果不知道给你的程序员朋友送什么礼物,送这个键盘准没错。

    \n

    湿厕纸

    用之前很抗拒,用习惯后爽死了。现在如果不用它就觉得粑粑没擦干净。

    \n

    戴森吹风机

    动力十足,气流吹到头上后又很轻柔。向我这种头发不太长的,1分钟以内结束战斗。

    \n

    戴森吸尘器

    用它吸一吸你的床,就知道你每天睡的床有多脏了。用它打扫卫生,看着集尘桶内的灰尘越来越多,很有成就感也很解压。

    \n

    罗技 MX Master3 鼠标

    贴合手型,就像握住了D罩杯。滚轮像指尖陀螺,没有思路时可以用它来解压。侧边按键写代码,浏览网页时前后推很实用。

    \n

    这个鼠标是跟一个朋友交换的,她买这个鼠标后觉得太大,刚好我买了个小的,就和她换了。刚用的时候没觉得太好用,等习惯后就发现离不开了。

    \n

    植观洗发水

    我用的去屑清爽那一款,我之前一直尝试各重洗发水,自从用了这款后就再没有换过。

    \n

    各位男程序员同胞,给自己换个好洗发水把。

    \n

    菁华3合1洗衣凝珠

    这个味道我太爱了,穿着用它洗过的衣服去上班,无意间闻到它的芬香后,整个人都快乐了许多。

    \n

    烘干机

    被烘过的衣服穿起来太舒服了,松松软软,和在夏日的暖阳下晾干的一样,一股清新阳光的味道。

    \n

    Usmile 牙刷

    之前我一直用的是飞利浦电动牙刷,用坏两个后,在被安利下购买了 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":"

    最近团队封装了个 springbootstarter,用起来很爽,后来优化代码的时候,看到下边的代码中已经指定了 profile

    \n
    cloud:
    config:
    discovery:
    enabled: true
    service-id: config-server
    label: master
    profile: ${spring.profiles.active:dev}
    \n

    所以理所当然的认为不需要指定 spring 的 active 了,就把 active 给删掉了(如下):

    \n
    spring:
    profiles:
    active: dev
    \n

    发布到 maven 仓库后,重新测试没啥问题。结果过了个周末来了再编译,发现程序无法启动了,找了很多原因才发现是上边的操作导致的。

    \n

    后来将配置改了回来,发现还是不行,又鼓捣了好久发现这次的问题是 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 可以强制刷新缓存。

    \n

    虽然问题解决了,但是我还有个疑问,我们的 starter 明明已经指定版本号为 0.0.1-SNAPSHOT 了,按理说应该在 build 的时候无条件的重新拉取最新的依赖,但是这个时候为什么没有生效?

    \n"},{"title":"图数据库入门:ThinkerPop 介绍","url":"/2017/graph-database-thinkerpop/","content":"

    Apache TinkerPop 是一个开源的图计算框架。在这其中,TinkerPop 代表了很多的功能和技术,并且在它广阔的生态系统下还另外扩展了第三方贡献图库和系统的世界。TinkerPop 的生态系统对于新手来说可能是复杂的,尤其是第一次浏览参考文档的时候。

    \n

    所以,你要从哪里开始使用 TinkerPop 呢?你如何快速入门并且获得成果?

    \n

    Gremlin,TinkerPop 世界里最知名的公民,让它来帮助你完成入门,之后,你也可以使用 TinkerPop 构建图应用程序了。

    \n

    \"\"

    \n

    认识 Gremlin

    \"\"

    \n

    Gremlin 可以帮助你浏览一个图中的点和边。他本质上是你用来查询图数据库的语言,就和 SQL 是用来查询关系型数据库的语言一样。为了告诉 Gremlin 他应该如何「遍历」图(也就是你想做的查询)你需要一种方法来用他能明白的语言下达命令,这个语言当然被叫做「Gremlin」。对于这个任务,你需要一个 TinkerPop 的最重要的工具:Gremlin 控制台

    \n
    \n

    你现在可能还不知道点和边是什么,这会在后文中进行介绍,不过请允许我带你先认识一下 Gremlin 控制台,让你能够了解这个可以帮助你学习体验的工具。

    \n
    \n

    我们来下载控制台然后解压并启动它:

    \n
    $ unzip apache-tinkerpop-gremlin-console-3.3.0-bin.zip
    $ cd apache-tinkerpop-gremlin-console-3.3.0
    $ bin/gremlin.sh

    \\,,,/
    (o o)
    -----oOOo-(3)-oOOo-----
    plugin activated: tinkerpop.server
    plugin activated: tinkerpop.utilities
    plugin activated: tinkerpop.tinkergraph
    gremlin>
    \n

    Gremlin 控制台是个 REPL 环境,它提供了很 nice 的方式来学习 Gremlin,因为你可以在输入代码后立刻得到反馈。这消除了需要「创建项目」才能尝试的复杂方式。控制台不仅仅是用来「入门」的,你将发现你会使用它来进行和 TinkerPop 相关的各种活动,比如加载数据、管理图、编写复杂的遍历等等。

    \n

    为了让 Gremlin 遍历一个图,你需要一个 Graph 实例,它保存着图的结构和数据。TinkerPop 是不同图数据库和图处理器之上的图抽象层,所以控制台中有很多可以实例化的实例供你选择。开始时最好的 Grahp 实例当然是 TinkerGraph。TinkerGraph 是一个快速、运行于内存的图数据库,有少量配置项,使其成为初学者不错的选择。

    \n
    \n

    TinkerGraph 不仅仅是提供给初学者的玩具。它在以下几个场景也是非常有用的:分析从大图中取出的子图时,使用不会有太大变化的静态图时,编写单元测试和其他能适应内存的图用例时。

    \n
    \n

    待续

    \n","tags":["BigData Graph"]},{"title":"面向程序员的数据挖掘指南-知识点","url":"/2021/guidetodatamining-points/","content":"

    前段时间读了《面向程序员的数据挖掘指南》,原文链接:https://dataminingguide.books.yourtion.com/,把里边的知识点做下整理。

    \n

    曼哈顿距离

    x之差的绝对值加上y之差的绝对值
    \"\"

    \n

    欧几里得距离

    \"\"

    \n

    勾股定理
    \"\"

    \n

    闵可夫斯基距离

    \"\"

    \n
      \n
    • r = 1 该公式即曼哈顿距离
    • \n
    • r = 2 该公式即欧几里得距离
    • \n
    • r = ∞ 极大距离
    • \n
    \n

    r值越大,单个维度的差值大小会对整体距离有更大的影响。

    \n

    协同过滤

    利用他人的喜好来进行推荐,也就是说,是大家一起产生的推荐。

    \n

    皮尔逊相关系数

    用于衡量两个变量之间的相关性,它的值在-1至1之间,1表示完全吻合,-1表示完全相悖。

    \n

    皮尔逊相关系数的计算公式是:
    \"\"

    \n

    皮尔逊相关系数的近似值:
    \"\"

    \n

    余弦相似度

    \"\"

    \n

    “·”号表示数量积。

    \n

    “||x||”表示向量x的模,计算公式是:
    \"\"

    \n

    如:
    \"\"

    \n

    它们的模是:
    \"\"

    \n

    数量积的计算:
    \"\"

    \n

    因此余弦相似度是:
    \"\"

    \n

    余弦相似度的范围从1到-1,1表示完全匹配,-1表示完全相悖。

    \n

    应该使用哪种相似度?

      \n
    • 如果数据存在“分数膨胀”问题,就使用皮尔逊相关系数。
    • \n
    • 如果数据比较“密集”,变量之间基本都存在公有值,且这些距离数据是非常重要的,那就使用欧几里得或曼哈顿距离。
    • \n
    • 如果数据是稀疏的,则使用余弦相似度。
    • \n
    \n

    K最邻近算法

    \"\"

    \n

    用户的评价类型可以分为显式评价和隐式评价

      \n
    • 显式评价指的是用户明确地给出对物品的评价
    • \n
    • 所谓隐式评价,就是我们不让用户明确给出对物品的评价,而是通过观察他们的行为来获得偏好信息。
        \n
      • 另一种隐式评价是用户的实际购买记录
      • \n
      \n
    • \n
    \n

    显式评价的问题

      \n
    • 问题1:人们很懒,不愿评价物品
    • \n
    • 问题2:人们会撒谎,或存有偏见
    • \n
    • 问题3:人们不会更新他们的评论
    • \n
    \n

    基于用户的协同过滤弊端

      \n
    1. 扩展性 上文已经提到,随着用户数量的增加,其计算量也会增加。这种算法在只有几千个用户的情况下能够工作得很好,但达到一百万个用户时就会出现瓶颈。
    2. \n
    3. 稀疏性 大多数推荐系统中,物品的数量要远大于用户的数量,因此用户仅仅对一小部分物品进行了评价,这就造成了数据的稀疏性。
    4. \n
    \n

    基于用户的协同过滤和基于物品的协同过滤区别

      \n
    • 基于用户的协同过滤是通过计算用户之间的距离找出最相似的用户,并将他评价过的物品推荐给目标用户;
    • \n
    • 而基于物品的协同过滤则是找出最相似的物品,再结合用户的评价来给出推荐结果。

      \n
    • \n
    • 基于用户的协同过滤又称为内存型协同过滤,因为我们需要将所有的评价数据都保存在内存中来进行推荐。

      \n
    • \n
    • 基于物品的协同过滤也称为基于模型的协同过滤,因为我们不需要保存所有的评价数据,而是通过构建一个物品相似度模型来做推荐。
    • \n
    \n

    修正的余弦相似度

    修正的余弦相似度是一种基于模型的协同过滤算法。这种算法的优势之一是扩展性好,对于大数据量而言,运算速度快、占用内存少。

    \n

    用户的评价标准是不同的,比如喜欢一个歌手时有些人会打4分,有些打5分;不喜欢时有人会打3分,有些则会只给1分。修正的余弦相似度计算时会将用户对物品的评分减去用户所有评分的均值,从而解决这个问题。

    \n

    \"\"

    \n

    U表示同时评价过物品i和j的用户集合

    \n

    \"\"

    \n

    表示将用户u对物品i的评价值减去用户u对所有物品的评价均值,从而得到修正后的评分。

    \n

    s(i,j)表示物品i和j的相似度,分子表示将同时评价过物品i和j的用户的修正评分相乘并求和,分母则是对所有的物品的修正评分做一些汇总处理。

    \n

    修正的余弦相似度示例

    计算Kacey Musgraves和Imagine Dragons的相似度
    \"\"

    \n

    我已经标出了同时评价过这两个歌手的用户,代入到公式中:
    \"\"

    \n

    所以这两个歌手之间的修正余弦相似度为0.5260

    \n

    使用修正余弦相似度进行预测

    比如我想知道David有多喜欢Kacey Musgraves?
    \"\"

    \n

    p(u,i)表示我们会来预测用户u对物品i的评分,所以p(David, Kacey Musgraves)就表示我们将预测David会给Kacey打多少分。
    N是一个物品的集合,有如下特性:

    \n
      \n
    • 用户u对集合中的物品打过分
    • \n
    • 物品i和集合中的物品有相似度数据(即上文中的矩阵)
    • \n
    \n

    Si,N表示物品i和N的相似度,Ru,N表示用户u对物品N的评分。

    \n

    为了让公式的计算效果更佳,对物品的评价分值最好介于-1和1之间。
    \"\"

    \n

    MaxR表示评分系统中的最高分(这里是5),MinR为最低分(这里是1),Ru,N是用户u对物品N的评分,NRu,N则表示修正后的评分(即范围在-1和1之间)。

    \n

    若已知NRu,N,求解Ru,N的公式为:
    \"\"

    \n

    比如一位用户打了2分,那修正后的评分为:
    \"\"

    \n

    反过来则是:
    \"\"

    \n

    修正David对各个物品的评分:
    \"\"

    \n

    结合物品相似度矩阵,代入公式:
    \"\"

    \n

    将其转换到5星评价体系中:
    \"\"

    \n

    Slope One算法

    一种比较流行的基于物品的协同过滤算法

    \n

    分为两个步骤:

    \n
      \n
    • 首先需要计算出两两物品之间的差值(可以在夜间批量计算)。
    • \n
    • 第二步则是进行预测
    • \n
    \n

    Slope One算法计算差值

    计算物品之间差异的公式是:
    \"\"

    \n

    card(S)表示S中有多少个元素;X表示所有评分值的集合;card(Sj,i(X))则表示同时评价过物品j和i的用户数。

    \n

    计算Taylor Swift 和 PSY之间的差值
    \"\"

    \n

    card(Sj,i(X))的值是2——因为有两个用户(Amy和Ben)同时对PSY和Taylor Swift打过分。

    \n

    分子uj-ui表示用户对j的评分减去对i的评分,代入公式得:
    \"\"

    \n

    即用户们给Taylor Swift的评分比PSY要平均高出两分。

    \n

    Slope One算法更新

    比如说Taylor Swift和PSY的差值是2,是根据9位用户的评价计算的。当有一个新用户对Taylor Swift打了5分,PSY打了1分时,更新后的差值为:
    \"\"

    \n

    使用加权的Slope One算法进行预测
    公式为:
    \"\"
    \"\"

    \n

    PWS1(u)j表示我们将预测用户u对物品i的评分。
    \"\"

    \n

    表示遍历Ben评价过的所有歌手,除了Whitney Houston以外(也就是-{j}的意思)。

    \n

    整个分子的意思是:对于Ben评价过的所有歌手(Whitney Houston除外),找出Whitney Houston和这些歌手之间的差值,并将差值加上Ben对这个歌手的评分。同时,我们要将这个结果乘以同时评价过两位歌手的用户数。

    \n

    Ben的评分情况和两两歌手之间的差异值展示如下:
    \"\"

    \n
      \n
    1. Ben对Taylor Swift打了5分,也就是ui
    2. \n
    3. Whitney Houston和Taylor Swift的差异是-1,即devj,i
    4. \n
    5. devj,i + ui = 4
    6. \n
    7. 共有两个用户(Amy和Daisy)同时对Taylor Swift和Whitney Houston做了评价,即cj,i = 2
    8. \n
    9. 那么(devj,i + ui) cj,i = 4 × 2 = 8
    10. \n
    11. Ben对PSY打了2分
    12. \n
    13. Whitney Houston和PSY的差异是0.75
    14. \n
    15. devj,i + ui = 2.75
    16. \n
    17. 有两个用户同时评价了这两位歌手,因此(devj,i + ui) cj,i = 2.75 × 2 = 5.5
    18. \n
    19. 分子:8 + 5.5 = 13.5
    20. \n
    21. 分母:2 + 2 = 4
    22. \n
    23. 预测评分:13.5 ÷ 4 = 3.375
    24. \n
    \n

    向量

    在线性代数中,向量(vector)指的是具有大小和方向的几何对象。向量支持多重运算,包括相加、相减及数乘等。

    \n
      \n
    • 当我们用这种方式定义特征后,就可以运用线性代数中的向量运算法则了。
    • \n
    \n

    在数据挖掘中,向量则可简单认为是物品的一组特征,比如音乐乐曲的特征。做文本挖掘时,会将一篇文章也用向量来表示——每个元素的位置表示一个特定的单词,这个位置上的值表示单词出现的次数。

    \n
      \n
    • 用「向量」一词比用「物品的一组特征」要来的专业
    • \n
    \n

    分类器

    分类器是指通过物品特征来判断它应该属于哪个组或类别的程序。

    \n

    分类器程序会基于一组已经做过分类的物品进行学习,从而判断新物品的所属类别。

    \n

    标准化

    要让数据变得可用我们可以对其进行标准化,最常用的方法是将所有数据都转化为0到1之间的值。

    \n

    标准分计算公式:
    \"\"

    \n

    mean:平均值
    standard deviation:标准差
    标准差的计算公式是:
    \"\"

    \n

    card(x)表示集合x中的元素个数。

    \n

    修正的标准分

    计算方法:将标准分公式中的均值改为中位数,将标准差改为绝对偏差。
    \"\"

    \n

    中位数指的是将所有数据进行排序,取中间的那个值。如果数据量是偶数,则取中间两个数值的均值。

    \n

    计算工资的对偏差:
    首先将所有人按薪水排序,找到中位数,然后计算绝对偏差:
    \"\"

    \n

    可以计算得出Yun的修正标准分:
    \"\"

    \n

    是否需要标准化?

    当物品的特征数值尺度不一时,就有必要进行标准化。

    \n

    需要进行标准化的情形:

    \n
      \n
    1. 我们需要通过物品特性来计算距离;
    2. \n
    3. 不同特性之间的尺度相差很大。
    4. \n
    \n

    十折交叉验证

    将数据集随机分割成十个等份,每次用9份数据做训练集,1份数据做测试集,如此迭代10次。

    \n

    留一法

    在数据挖掘领域,N折交叉验证又称为留一法。

    \n

    上面已经提到了留一法的优点之一:我们用几乎所有的数据进行训练,然后用一个数据进行测试。

    \n

    留一法的另一个优点是:确定性。

    十折交叉验证是一种不确定的验证。相反,留一法得到的结果总是相同的,这是它的一个优点。

    \n

    缺点

    最大的缺点是计算时间很长。

    \n

    留一法的另一个缺点是分层问题。

    \n

    在留一法中,所有的测试集都只包含一个数据。所以说,留一法对小数据集是合适的,但大多数情况下我们会选择十折交叉验证。

    \n

    混淆矩阵

    表格的行表示测试用例实际所属的类别,列则表示分类器的判断结果。

    \n

    混淆矩阵可以帮助我们快速识别出分类器到底在哪些类别上发生了混淆,因此得名。

    \n

    这个数据集中有300人,使用十折交叉验证,其混淆矩阵如下:
    \"\"

    \n

    可以看到,100个体操运动员中有83人分类正确,17人被错误地分到了马拉松一列;92个篮球运动员分类正确,8人被分到了马拉松;85个马拉松运动员分类正确,9人被分到了体操,16人被分到了篮球。

    \n

    混淆矩阵的对角线(绿色字体)表示分类正确的人数,因此求得的准确率是:
    \"\"

    \n

    从混淆矩阵中可以看出分类器的主要问题。

    \n

    在这个示例中,我们的分类器可以很好地区分体操运动员和篮球运动员,而马拉松运动员则比较容易和其他两个类别发生混淆。

    \n

    Kappa指标

    Kappa指标可以用来评价分类器的效果比随机分类要好多少。

    \n

    Kappa指标可以用来衡量我们之前构造的分类器和随机分类器的差异,公式为:
    \"\"

    \n

    P(c)表示分类器的准确率,P(r)表示随机分类器的准确率。
    \"\"

    \n

    动手实践

    以下是该分类器的混淆矩阵,尝试计算出它的Kappa指标并予以解释。
    \"\"

    \n

    准确率 = (50+75+123+170)/600= 0.697

    \n

    计算列合计和百分比:
    \"\"

    \n

    然后根据百分比来填充随机分类器的混淆矩阵:
    \"\"

    \n

    随机分类器准确率 = (8 + 24 + 51 + 92) / 600 = (175 / 600) = 0.292

    \n

    最后,计算Kappa指标:
    \"\"

    \n

    这说明分类器的效果还是要好过预期的。

    \n

    kNN算法

    考察这条新记录周围距离最近的k条记录,而不是只看一条,因此这种方法称为k近邻算法(kNN)。

    \n

    每个近邻都有投票权,程序会将新记录判定为得票数最多的分类。比如说,我们使用三个近邻(k = 3),其中两条记录属于体操,一条记录属于马拉松,那我们会判定x为体操。

    \n

    KNN 算法预测举例

    我们需要预测Ben对Funky Meters的喜好程度,他的三个近邻分别是Sally、Tara、和Jade。

    \n

    下表是这三个人离Ben的距离,以及他们对Funky Meters的评分:
    \"\"

    \n

    在计算平均值的时候,我希望距离越近的用户影响越大,因此可以对距离取倒数,从而得到下表:
    \"\"

    \n

    下面,我们把所有的距离倒数除以距离倒数的和(0.2 + 0.1 + 0.067 = 0.367),从而得到评分的权重:
    \"\"

    \n

    我们可以注意到两件事情:权重之和是1;原始数据中,Sally的距离是Tara的二分之一,这点在权重中体现出来了。

    \n

    最后,我们求得平均值,也即预测Ben对Funky Meters的评分:
    \"\"

    \n

    近邻算法 vs 贝叶斯算法

    近邻算法又称为被动学习算法。这种算法只是将训练集的数据保存起来,在收到测试数据时才会进行计算。

    \n

    贝叶斯算法则是一种主动学习算法。它会根据训练集构建起一个模型,并用这个模型来对新的记录进行分类,因此速度会快很多。

    \n

    贝叶斯算法的两个优点

    能够给出分类结果的置信度

    \n

    它是一种主动学习算法

    \n

    概率

    我们用符号P(h)来表示,即事件h发生的概率:

    \n
      \n
    • 投掷硬币:P(正面) = 0.5
    • \n
    • 掷骰子:P(1) = 1/6
    • \n
    • 青少年:P(女生) = 0.5
    • \n
    \n

    P(h|D)来表示D条件下事件h发生的概率。比如:P(女生|弗兰克学院的学生) = 0.86

    \n

    计算的公式是:
    \"\"

    \n

    概率计算

    下表是一些人使用笔记本电脑和手机的品牌:
    \"\"

    \n

    使用iPhone的概率是多少?
    \"\"

    \n

    如果已知这个人使用的是Mac笔记本,那他使用iPhone的概率是?
    \"\"

    \n

    首先计算出同时使用Mac和iPhone的概率:
    \"\"

    \n

    使用Mac的概率则是:
    \"\"

    \n

    从而计算得到Mac用户中使用iPhone的概率:
    \"\"

    \n

    为了简单起见,我们可以直接通过计数得到:
    \"\"

    \n

    贝叶斯法则

    贝叶斯法则描述了P(h)、P(h|D)、P(D)、以及P(D|h)这四个概率之间的关系:
    \"\"

    \n

    现实问题中要计算P(h|D)往往是很困难的

    \n

    朴素贝叶斯

    朴素贝叶斯计算得到的概率其实是真实概率的一种估计,而真实概率是对全量数据做统计得到的。

    \n

    在朴素贝叶斯中,概率为0的影响是很大的,甚至会不顾其他概率的大小。此外,抽样统计的另一个问题是会低估真实概率。

    \n

    如何解决概率为0的影响?

    解决方法是将公式变为以下形式:
    \"\"

    \n

    n表示训练集中y类别的记录数;nc表示y类别中值为x的记录数。

    \n

    m是一个常数,表示等效样本大小。

    \n

    决定常数m的方法有很多,我们这里使用值的类别来作为m,比如投票有赞成和否决两种类别,所以m就为2。

    \n

    p则是相应的先验概率,比如说赞成和否决的概率分别是0.5,那p就是0.5。

    \n

    标准差

    \"\"

    \n

    标准差是用来衡量数据的离散程度的,如果所有数据都接近于平均值,那标准差也会比较小。

    \n

    样本标准差的公式是:
    \"\"

    \n

    我们把有限集合A的元素个数记为card(A)。例如A={a,b,c},则card(A)=3

    \n

    高斯分布

    正态分布、钟型曲线、高斯分布等术语,他们指的是同一件事:68%的数据会落在标准差为1的范围内,95%的数据会落在标准差为2的范围内:
    \"\"

    \n

    概率计算公式:
    \"\"

    \n

    假设我们要计算P(100k|i500)的概率,即购买i500的用户中收入是100,000美元的概率。之前我们计算过购买i500的用户平均收入(106.111)以及样本标准差(21.327),我们用希腊字母μ(读“谬”)来表示平均值,σ(读“西格玛”)来表示标准差。
    \"\"

    \n

    xi = 100 指的是收入100k
    \"\"
    \"\"
    \"\"

    \n

    e是自然常数,约等于2.718。
    \"\"

    \n

    监督式和非监督式学习

    当我们使用已经标记好分类的数据集进行训练时,这种类型的机器学习称为“监督式学习”。文本分类就是监督式学习的一种。

    \n

    如果训练集没有标好分类,那就称为“非监督式学习”,聚类就是一种非监督式学习

    \n

    聚类

    通过物品特征来计算距离,并自动分类到不同的群集或组中。

    \n

    k-means算法可概括为

      \n
    1. 随机选取k个元素作为中心点;
    2. \n
    3. 根据距离将各个点分配给中心点;
    4. \n
    5. 计算新的中心点;
    6. \n
    7. 重复2、3,直至满足条件。
    8. \n
    \n

    评判聚类结果的好坏

    我们可以使用误差平方和(或称离散程度)来评判聚类结果的好坏,它的计算方法是:计算每个点到中心点的距离平方和。
    \"\"

    \n

    上面的公式中,第一个求和符号是遍历所有的分类,比如i=1时计算第一个分类,i=2时计算第二个分类,直到计算第k个分类;第二个求和符号是遍历分类中所有的点;Dist指代距离计算公式(如曼哈顿距离、欧几里得距离);计算数据点x和中心点ci之间的距离,平方后相加。

    \n

    k-means++

    前面我们提到k-means是50年代发明的算法,它的实现并不复杂,但仍是现今最流行的聚类算法。不过它也有一个明显的缺点。在算法一开始需要随机选取k个起始点,正是这个随机会有问题。
    有时选取的点能产生最佳结果,而有时会让结果变得很差。k-means++则改进了起始点的选取过程,其余的和k-means一致。

    \n

    以下是k-means++选取起始点的过程:

    \n
      \n
    1. 随机选取一个点;
    2. \n
    3. 重复以下步骤,直到选完k个点:
        \n
      1. 计算每个数据点(dp)到各个中心点的距离(D),选取最小的值,记为D(dp);
      2. \n
      3. 根据D(dp)的概率来随机选取一个点作为中心点。
      4. \n
      \n
    4. \n
    \n

    k-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

    \n

    经过 Google 发现是因为当前目录下存在一个名为:.fb_friend.csv.crc 的文件,将此文件删除后即可成功上传。

    \n

    究其原因是因为 Hadoop 的 CRC 数据校验机制,Hadoop 系统为了保证数据的一致性,会对文件生成相应的校验文件,并在读写的时候进行校验,确保数据的准确性。

    \n

    在上传的过程中,Hadoop 将通过 FSInputChecker 判断需要上传的文件是否存在进行校验的 crc 文件,即 .fb_friend.csv.crc,如果存在 crc 文件,将会对其内容一致性进行校验,如果校验失败,则停止上传该文件。

    \n

    在使用 hadoop fs -getmerge srcDir destFile 命令时,本地磁盘一定会生成相应的 .crc 文件。

    \n

    所以如果需要修改 getmerge 获取的文件的内容,再次上传到 DFS 时,可以采取以下 2 种策略进行规避:

    \n
      \n
    1. 删除 .crc 文件

      \n
    2. \n
    3. getmerge 获取的文件修改后重新命名,如使用 mv 操作,再次上传到 DFS 中。

      \n
    4. \n
    \n"},{"title":"离别好难说出口","url":"/2023/hard-to-say-goodbye/","content":"

    我是个喜聚不喜散的人,而且我觉得这和喜不喜欢social没有关系。聚了、认识了、熟悉了就不想再散。

    \n

    黛玉也说过:“人有聚就有散,聚时欢喜,到散时岂不清冷?既清冷,则生伤感,所以不如倒是不聚的好。比如那花开时令人爱慕,谢时则增惆怅,所以倒是不开的好。”

    \n

    在经过5、6两个月努力招聘后,我的小组加上我在内有9个人,本来应该是10个但被砍了一个HC。看起来一切都在步入正轨,但上周老板通知考虑到我们大部门的成本问题和另一个创新部门的业务扩张,我们需要为那边提供十几个后端人力,我们团队也要背一个名额,让我从团队中挑选一个合适的人。

    \n

    当时也没有定好具体过去的时间,我们只是大致讨论了人选,因为还没实际执行,那时候我对这件事还没太大的想法。今天下班前突然通知本周内就要调过去,需要尽快沟通。我本来打算后天再和那个同学沟通,今天先问了问他手中的项目进展,提醒了一下他尽量不要延期。最后他问我是不是给他排了其他工作,此刻我当然可以说没有,然后等后天再和他说这件事,但我觉得我不应该为了让他把需求做完或者只图自己一时心里安稳就骗他,应该实事求是坦诚相告,于是把他带到会议室沟通了前边的内容,唯一没有提到的是为了缩减这边的成本,只说了各种好处、那边的成长空间之类的。

    \n

    沟通完后我能看出他失落的表情,说实话我自己也非常失落,尽管这不是在聊裁员,但我还是很失落,感觉是自己没有保护好队友。我们一起磨合了一个多月,帮他度过过了onboarding期,他马上就要大展宏图做一些重要工作的时候被调走了。

    \n

    和他沟通过程中也能感觉到自己没有那么坚定,如果真的让我和一个同学聊优化的事情也许会更难过吧。

    \n

    回到开头黛玉那句话,如果我知道有这次的散,我还会招进那个人吗?我可能还是会的,虽然相处时间不是很长,但对他来说还是有成长,对我们团队来说他也确实有相应的贡献,而且整个过程中也是比较愉悦的。

    \n"},{"title":"提供健康的工作环境","url":"/2023/health-work-environment/","content":"

    近三个月我的团队除了入职一位去年招到的校招生外,还通过社招渠道加入了4位新同学,他们中有两位上家公司就职于字节跳动、一位上家就职于快手、一位上家就职于小米。

    \n

    从大厂跳到中小厂一般几个原因:

    \n
      \n
    1. 上升空间受限,不想再在大厂做螺丝钉。具体来说有以下几点:
        \n
      • 工作内容单一、枯燥。
      • \n
      • 员工长期从事重复性工作,产生职业倦怠。
      • \n
      • 个人发展受限。在当前公司难以获得切实的学习与发展机会。
      • \n
      \n
    2. \n
    3. 入职即巅峰,工作一两年后对薪资不满。亦或是过度追求股权激励,基本薪资偏低,与工作量不匹配。
    4. \n
    5. 工作压力大,加班时间长。卷不动了。
    6. \n
    \n

    关于第1点,跳槽的前提条件一般都是薪资待遇不能比之前差,既然他们选择了跳槽,说明第1点现阶段是基本满足了,再往后还要看自己的能力和公司的发展。

    \n

    关于第2点,我目前的团队业务包含了公司的核心场景和推荐工程领域,有非常多的事情可以做,有足够的挑战和 scope 来提供给大家。

    \n

    关于第3点,之前我做不了主,现在有了一定可以做主的空间,我会给团队提供一个我认为是比较的健康工作环境,具体有以下几点:

    \n
      \n
    1. 周一早上不开晨会,我们每日例行的会议只有一个晨会,大概是在上午10点半左右人到全以后开,作为当日开启工作的kickoff。晨会上不用汇报工作,只需要说昨天做了什么,今天准备做什么,遇到了什么问题即可。我在周一取消掉的原因是过个周末容易记不住上周五做了什么,而且包括我在内有周一上班恐惧症,所以取消掉周一早会好让大家更好调整状态,过个没有负担的周末。
    2. \n
    3. 周一到周四下午过6点半后不开会。下午6点半后大家的精力和体力基本已经耗尽,如果这一天会议已经比较多,此时应该静下心来把自己的羊放一放。一天的工作即将结束,此时适当调整自己的情绪,梳理一下当天的得失。更进一步,如果有工贼拉我在下午6点半后讨论问题,我也会毫不留情的拒绝,如果是拉我的下属被我知道了,我也会和对方重新协商时间(老板和紧急状况除外,毕竟还要恰饭,不过这种情况少之又少,可能一个季度也就一两次)。
    4. \n
    5. 周五下午尽量不开会。我希望团队中每个同学在工作之外都有自己向往的生活,有更多美好的事情可以做,所以规定周五无必要不加班,所以大部分同学都会在7点前走,很少有7点半后吃了饭再走的情况。周五下午不开会还有个原因是,如果有同学打算周末去稍远一点的地方玩,一般会选择周五下午出发,不安排会议可以让同学们放心请假。但实际上我自己周五下午被安排了3个会,不过还好会议都安排在了5点前。
    6. \n
    7. 并行需求不超过两个。我会尽量让每个人手中doing状态的工作保持在两个以内,人的注意力和专注力是有限的,切换上下文的成本极高,频繁切换的后果就是工作质量变差。说实话,我现在时间片就已经被切的支离破碎,很难再像之前那样进入心流的状态,但我不想让每个人都有这样的不好体验,所以会由我去对接那些外部乱七八糟的事情,需求接不过来时我会自己多接几个,或者往后压一压。
    8. \n
    9. 不抢工作成果。我不会在绩效自评或者在和其他人交流时把下属的工作成果归功于自己,甚至有时候在有老板夸赞某个项目做的不错时,我还会主动说明这是 xxx 做的。不过作为偏管理的职务,我肯定要对整个团队负责,所以我是会在绩效评定时将整个团队取得的成果进行展现的,这放在哪里都是合理且必要的。
    10. \n
    \n

    尽管我会在可行范围内提供比较好的工作环境,且在绩效考核时我更看重的是结果,不会用类似不够「不卷」的原因来打差绩效,但有一点要说明的事实就是,通常在工作上投入更多时间确实更有可能取得好的结果。我提供了健康工作的可选性,至于更看重什么、最终如何选择还要看个人,比如现阶段你更追求金钱个个人成功,那把大部分精力放在工作上没有问题,好的绩效确实可以带来客观的现金回报。我也希望每个人可以现在跟长远的角度来思考这个问题。

    \n

    我的某些行为有点像宝玉在守护着大观园中的姐姐妹妹,不知道这种环境是会豢养出偷虾须镯的坠儿,还是会培育出香菱那样的诗仙。虽然不知道最终会结出什么果,但我会坚持自己认为正确的事。

    \n"},{"title":"Homebrew 修改国内源","url":"/2020/homebrew-china-mirror/","content":"

    \"1\"

    \n

    Homebrew 是 Mac 上最常用的软件包管理工具,可以简化 macOS 系统是的软件安装过程。但由于国内网络环境,每次更新时速度都不忍直视。为了提升速度和体验,建议修改为国内源。

    \n

    目前有两个常用 Homebrew 源,分别是阿里镜像清华大学镜像。其中清华镜像在阿里镜像已有的 brewhomebrew-core 之外,还额外提供了 homebrew-cask 源。所以我采用的策略是:brewhomebrew-core 使用阿里的,homebrew-cask 使用清华的,原因是阿里在程序员心目中的地位是要高于清华的。

    \n

    直接复制以下命令在终端运行即可:

    \n
    git -C "$(brew --repo)" remote set-url origin https://mirrors.aliyun.com/homebrew/brew.git
    git -C "$(brew --repo homebrew/core)" remote set-url origin https://mirrors.aliyun.com/homebrew/homebrew-core.git
    git -C "$(brew --repo homebrew/cask)" remote set-url origin https://mirrors.tuna.tsinghua.edu.cn/git/homebrew/homebrew-cask.git
    \n

    没有安装使用 homebrew-cask 的情况下,最后一条命令会报错,可以忽略。

    \n

    之后执行 brew update 使配置生效并测试工作是否正常。

    \n

    Homebrew 还提供了一个核心组件 Homebrew-bottles,可以提供一些包的二进制预编译版本,省去本地下载源码、编译源码的时间,提升安装效率,所以可以把 Homebrew-bottles 的源地址也进行替换,Homebrew-bottles 的地址是通过环境变量加载的,所以有两种修改方式:

    \n

    临时生效:

    \n
    export HOMEBREW_BOTTLE_DOMAIN=https://mirrors.aliyun.com/homebrew/homebrew-bottles
    \n

    永久生效(以 zsh 为例):

    \n
    echo 'export HOMEBREW_BOTTLE_DOMAIN=https://mirrors.aliyun.com/homebrew/homebrew-bottles' >> ~/.zshrc
    source ~/.zshrc
    \n

    Enjoy!


    \n

    下边记录两个通过 homebrew 更新软件包后可能会出现的问题

    更新 openssl 后新开命令行窗口报错

    报错内容:

    \n
    ERROR:root:code for hash md5 was not found.
    Traceback (most recent call last):
    File "/usr/local/Cellar/python@2/2.7.15_3/Frameworks/Python.framework/Versions/2.7/lib/python2.7/hashlib.py", line 147, in <module>
    globals()[__func_name] = __get_hash(__func_name)
    File "/usr/local/Cellar/python@2/2.7.15_3/Frameworks/Python.framework/Versions/2.7/lib/python2.7/hashlib.py", line 97, in __get_builtin_constructor
    raise ValueError('unsupported hash type ' + name)
    ValueError: unsupported hash type md5
    ERROR:root:code for hash sha1 was not found.
    ……
    File "/usr/local/Cellar/mercurial/4.9/lib/python2.7/site-packages/hgdemandimport/demandimportpy2.py", line 151, in __getattr__
    return getattr(self._module, attr)
    AttributeError: 'module' object has no attribute 'md5'
    \n

    修复方法:

    \n

    执行:ls /usr/local/Cellar/openssl,可以看到当前可用的 openssl 版本:

    \n
    ➜ ls /usr/local/Cellar/openssl

    1.0.2o_1 1.0.2q
    \n

    根据列出的版本,执行 brew switch openssl <版本号> 来指定版本(有可能在你本地只存在一个版本或和我这里有其他区别):

    \n
    ➜ brew switch openssl 1.0.2q

    // 正常情况下会返回一下内容
    Cleaning /usr/local/Cellar/openssl/1.0.2q
    Opt link created for /usr/local/Cellar/openssl/1.0.2q
    \n

    问题解决~

    \n

    更新 MySQL 后出问题

    Python 程序在连接 MySQL 时,报错:

    \n
    Traceback (most recent call last):
    File "/Users/jiapan/.virtualenvs/bossku/lib/python2.7/site-packages/flask/app.py", line 1997, in __call__
    return self.wsgi_app(environ, start_response)
    ……
    ……
    ……
    File "/Users/jiapan/.virtualenvs/bossku/lib/python2.7/site-packages/sqlalchemy/dialects/mysql/mysqldb.py", line 102, in dbapi
    return __import__('MySQLdb')
    File "/Users/jiapan/.virtualenvs/bossku/lib/python2.7/site-packages/MySQLdb/__init__.py", line 19, in <module>
    import _mysql
    ImportError: dlopen(/Users/jiapan/.virtualenvs/bossku/lib/python2.7/site-packages/_mysql.so, 2): Library not loaded: /usr/local/opt/mysql/lib/libmysqlclient.20.dylib
    Referenced from: /Users/jiapan/.virtualenvs/bossku/lib/python2.7/site-packages/_mysql.so
    Reason: image not found
    \n

    你的报错可能和我这里稍有区别,主要是倒数第三行,我这里是 libmysqlclient.20.dylib,你那里可能是 libmysqlclient.18.dylib 或其他的,不过理论上都可以通过这个方法解决。

    \n

    修复方法:

    \n

    执行 ls /usr/local/lib | grep libmysqlclient,我这里可以看到如下内容:

    \n
    libmysqlclient.21.dylib -> ../Cellar/mysql/8.0.19/lib/libmysqlclient.21.dylib
    libmysqlclient.a -> ../Cellar/mysql/8.0.19/lib/libmysqlclient.a
    libmysqlclient.dylib -> ../Cellar/mysql/8.0.19/lib/libmysqlclient.dylib
    \n

    查看列表中有没有和报错中完全相同的文件,如果存在完全匹配的就直接建立对应软链到 /usr/local/opt/mysql/lib/,没有的话就用 libmysqlclient.dylib 代替。

    \n

    我这里没有 libmysqlclient.20.dylib,所以我使用的命令如下:

    \n
    ln -s /usr/local/lib/libmysqlclient.dylib /usr/local/opt/mysql/lib/libmysqlclient.20.dylib
    \n

    问题解决~

    \n
    \n

    参考:

    \n\n"},{"title":"红楼梦谐音梗第一次出现的句子","url":"/2021/hongloumeng-homophonic-first-appear-sentence/","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
    \n

    里面凤姐见日期有限,也预先逐细分派料理,一面又派荣府中车轿人从跟王夫人送殡,又顾自己送殡去占下处。目今正值缮国公诰命亡故,王邢二夫人又去打祭送殡,西安郡王妃华诞,送寿礼,镇国公诰命生了长男,预备贺礼,又有胞兄王仁连家眷回南,一面写家信禀叩父母并带往之物,又有迎春染病,每日请医服药,看医生启帖,症源,药案等事,亦难尽述。

    \n
      \n
    • 王仁——忘仁(巧姐的舅舅,凤姐死后想把巧姐卖与技院)
    • \n
    \n

    贾蔷又近前回说:“下姑苏聘请教习,采买女孩子,置办乐器行头等事,大爷派了侄儿,带领着来管家两个儿子,还有单聘仁,卜固修两个清客相公,一同前往,所以命我来见叔叔。”贾琏听了,将贾蔷打谅了打谅,笑道:“你能在这一行么?这个事虽不算甚大,里头大有藏掖的。”贾蔷笑道:“只好学习着办罢了。”

    \n
      \n
    • 不固修——不顾羞
    • \n
    \n

    贾珍因想着贾蓉不过是个黉门监,灵幡经榜上写时不好看,便是执事也不多,因此心下甚不自在。可巧这日正是首七第四日,早有大明宫掌宫内相戴权,先备了祭礼遣人来,次后坐了大轿,打伞鸣锣,亲来上祭。贾珍忙接着,让至逗蜂轩献茶。贾珍心中打算定了主意,因而趁便就说要与贾蓉捐个前程的话。

    \n
      \n
    • 戴权——大权
    • \n
    \n

    此一匾一联书于正殿“大观园”园之名。“有凤来仪”赐名曰“潇湘馆”。“红香绿玉”改作“怡红快绿”即名曰“怡红院”。“蘅芷清芬”赐名曰“蘅芜苑”。

    \n
      \n
    • 潇湘馆——消香馆
    • \n
    • 怡红院——遗红怨
    • \n
    • 蘅芜苑——恨无缘
    • \n
    \n

    这里林黛玉见宝玉去了,又听见众姊妹也不在房,自己闷闷的。正欲回房,刚走到梨香院墙角上,只听墙内笛韵悠扬,歌声婉转。

    \n
      \n
    • 梨香院——离乡怨
    • \n
    \n

    贾芸出了荣国府回家,一路思量,想出一个主意来,便一径往他母舅卜世仁家来。原来卜世仁现开香料铺,方才从铺子里来,忽见贾芸进来,彼此见过了,因问他这早晚什么事跑了来。

    \n
      \n
    • 卜世仁——不是人
    • \n
    \n

    一时,只见一个小丫头子跑来,见红玉站在那里,便问道:“林姐姐,你在这里作什么呢?”红玉抬头见是小丫头子坠儿。红玉道:“那去?”坠儿道:“叫我带进芸二爷来。”说着一径跑了。这里红玉刚走至蜂腰桥门前,只见那边坠儿引着贾芸来了。

    \n
      \n
    • 坠儿——赘儿、罪儿
    • \n
    \n

    丫头方进来时,忽有人来回话:“傅二爷家的两个嬷嬷来请安,来见二爷。”宝玉听说,便知是通判傅试家的嬷嬷来了。那傅试原是贾政的门生,历年来都赖贾家的名势得意,贾政也着实看待,故与别个门生不同,他那里常遣人来走动。

    \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/hongloumeng-short-video/","content":"

    \n

    这个启发来自「谐星聊天会」的第三季第 6 期,大概 46 分钟左右家宇提出的。

    \n

    在红楼梦中有这么一回,贾瑞勾引王熙凤后被王熙凤下了「相思局」,致使贾瑞卧床不起。破足道人为了救他给了他一把风月宝鉴,也就是一面镜子,并告诉贾瑞只能看镜子背面,不要看正面,但是贾瑞不听劝告,非要看正面,每一次看正面就看到凤姐在里边搔首弄姿勾引他,笑盈盈的招手让他进去和她交欢,这么几十次后,贾瑞就因下溺连精,精尽人亡而死。

    \n

    贾瑞明知道把镜子翻过来能治自己的病,可是翻过来看到的是个骷髅,而另一面有个王熙凤在里边扭动,他在欲望面前无法控制自己,最后使自己命丧黄泉。

    \n

    这个就很像我们现在刷短视频,比如快手、抖音这些。我们一刷就几个小时过去了,刷完之后很有负罪感。短视频是这个时代的精神鸦片,过一段时间不刷心里就会发痒,刷之前认为自己有足够的控制力,只刷几分钟就停下来。刷的过程中也明知道自己只要把手机扣过来就可以解决问题,可自己就是控制不住不停的往下刷,像贾瑞一样不停的看镜子中虚幻的王熙凤。贾瑞丢掉的是性命,我们何尝不是在消耗自己的生命呢。

    \n

    跛足道人作为红楼梦中菩萨的象征,是无法直接救贾瑞和我们的,菩萨只能点化我们,能救赎我们的只有自己。

    \n"},{"title":"CDN 是如何工作的?","url":"/2022/how-cdn-work/","content":"

    维基百科给 CDN 的定义如下:

    \n
    \n

    内容分发网络Content Delivery Network 或Content Distribution Network,缩写:CDN)是指一种透过互联网互相连接的电脑网络系统,利用最靠近每位用户的服务器,更快、更可靠地将音乐、图片、视频、应用程序及其他文件发送给用户,来提供高性能、可扩展性及低成本的网络内容传递给用户。

    \n
    \n

    我们用更精简的语句概括一下:CDN 是一种利用分布在各个地理位置的服务器来提供快速内容交付的技术。

    \n
      \n
    • 这里的服务器我们也称为边缘(edge)服务器
    • \n
    • 交付的内容包括静态内容和动态内容
    • \n
    \n

    假如住在北京的小贾想要访问一个部署在杭州的电商网站,如果这个请求历经大半个中国进入位于杭州的服务器再返回,响应会非常慢。因此,那个电商网站可以在小贾居住地附近部署 CDN 服务器,网站的内容将从附近的 CDN 服务器加载。

    \n

    下图说明了这个过程:

    \n

    \"20220626201736.jpg\"

    \n
      \n
    1. 小贾在浏览器中输入 www.taobao.com ,浏览器在本地 DNS 缓存中查找该域名对应的 IP 地址。
    2. \n
    3. 如果没有在本地 DNS 缓存中找到该域名,浏览器就会去找 DNS 解析器进行域名解析。DNS 解析器通常位于互联网服务供应商(ISP,如中国联通、中国电信)。
    4. \n
    5. DNS 解析器通过递归的方式解析域名,最终它会要求权威名称服务器(Authoritative Name Server)查找该域名。
    6. \n
    7. 如果我们不使用 CDN,权威名称服务器会返回 www.taobao.com 位于杭州的 IP 地址。使用 CDN 后,权威名称服务器会返回一个别名指向 www.taobao.cdn.com (CDN 服务器的域名,这里只是举例,taobao 的 CDN 域名以实际为准)。
    8. \n
    9. DNS 解析器找到 CDN 权威名称服务器解析 www.taobao.cdn.com
    10. \n
    11. CDN 权威名称服务器再次返回一个别名:CDN 负载均衡器的域名 www.taobao.lb.com
    12. \n
    13. DNS 解析器继续要求 CDN 负载均衡器解析 www.taobao.lb.com ,负载均衡器根据用户的 IP 地址、ISP、请求的内容和服务器负载状况等条件选择一个最佳的 CDN 边缘服务器。
    14. \n
    15. CDN 负载均衡器返回 CDN 边缘服务器的 IP 地址。
    16. \n
    17. DNS 解析器将得到的 CDN 边缘服务器 IP 地址返回给浏览器。
    18. \n
    19. 浏览器访问 CDN 边缘服务器加载网站内容。CDN 服务器上缓存了静态和动态两种类型的内容,前者包含静态页面、图片、视频,后者包含边缘计算的结果。
    20. \n
    21. 如果 CDN 边缘服务器的缓存中没有找到用户需要的内容,它就将请求发给该地区(如华北大区)的 CDN 服务器。如果仍然没有找到,会将继续请求更上一级的中央 CDN 服务器,以此类推最终有可能会请求到源站,也就是位于杭州的服务器。这就是所谓的 CDN 分布式网络,其中服务器被部署在不同的地理位置。
    22. \n
    \n"},{"title":"如何在 Python 中判断列表是否为空","url":"/2019/how-to-check-if-a-list-is-empty-in-python/","content":"
    \n

    在判断列表是否为空时,你更喜欢哪种方式?决定因素是什么?

    \n
    \n

    \"\"

    \n

    在 Python 中有很多检查列表是否是空的方式,在讨论解决方案前,先说一下不同方法涉及到的不同因素。

    \n

    我们可以把判断表达式分为两个阵营:

    \n
      \n
    1. 对空列表的显式比较
    2. \n
    3. 对空列表的隐式求值
    4. \n
    \n

    这是什么意思?

    \n

    显式比较

    我们从显式比较开始说起,无论我们使用列表符号 [] 还是声明空列表的函数 list(),遵循的策略是查看待检查列表是否与空列表完全相等。

    \n
    # 都是用来创建空列表
    a = []
    b = list()
    print(a == b) # True
    \n

    另外,我们可以使用 len() 函数返回列表中的元素个数。

    \n
    a = []
    if len(a) == 0:
    print("The list is empty")
    \n

    隐式求值

    和显式比较相反,隐式求值遵循的策略是:将空列表求值为布尔值的 False,将有元素填充的列表求值为布尔值的 True

    \n
    a = []
    b = [1]
    if a:
    print("Evaluated True")
    else:
    print("Evaluated False")
    if b:
    print("Evaluated True")
    else:
    print("Evaluated False")


    # 输出
    Evaluated False
    Evaluated True
    \n

    那么,显式比较和隐式求值有什么区别呢?

    \n

    很多人习惯于使用显式比较的方式。但是如果你遵循鸭子类型的设计风格,那么会更加偏向于使用的是隐式方法。

    \n

    什么是鸭子类型

    「鸭子类型」这个词来自以下短语:

    \n
    \n

    当看到一只鸟走起来像鸭子、游泳起来像鸭子、叫起来也像鸭子,那么这只鸟就可以被称为鸭子。

    \n
    \n

    从功能上讲,这是对对象实际数据类型压力较小的一种确认。在鸭子类型中,关注点在于对象的行为,能做什么(比如,可迭代 iterable);而不是关注对象所属的类型。鸭子类型在动态语言中经常使用,非常灵活。

    \n

    鸭子类型优先考虑便利性而非安全性,从而可以使用更灵活的代码来适应更广泛的用途,它不会像传统方式那么严格。

    \n

    我们应该使用哪种方式?

    当我们越了解隐式求值,就越倾向于使用这种方式,因为我们知道空列表将被求值为 False

    \n
    a = []
    print(bool(a)) # False
    \n

    这使得我们可以合并那些很长的检查表达式,如:

    \n
    # 之前
    if isinstance(a, list) and len(a) > 0:
    print("Processing list...")
    # 之后
    if a:
    print("Processing list...")
    \n

    当然,最终的选择还取决于本次判断的意图:

    \n
      \n
    • 如果你检查空列表是为了对其进行迭代,那么隐式求值是更合适的方法。
    • \n
    • 如果你检查空列表是为了在之后调用列表中的方法,那么可以考虑使用显式比较来同时验证数据类型。
    • \n
    \n"},{"title":"如何提升编程能力","url":"/2019/how-to-improve-programming-skills/","content":"
    \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
    • 期待每个月20日公积金到账
    • \n
    \n

    有了期待,每一天就会有盼头。引用《基督山伯爵》的一句话:“人类的一切智慧是包含在这四个字里面的:’等待’和’希望’!”

    \n

    奖励自己

    自己完成一件工作后一定要奖励一下自己,可以是奖励自己一个包包、一块手表。或者简单一些的,一个项目上线后,奖励自己一杯咖啡、奶茶都可以。

    \n

    在你完成自己觉得很有成就感的事情后,你的老板、同事不一定能给你即时的正向反馈,我们可以自己给自己即时正反馈。

    \n

    建立及时正反馈很有必要的,只有这样才能积小胜成大胜。

    \n

    奖励自己的时机并不限于完成了一个工作之后,当自己失落、状态不好时也可以奖励自己。比如今天早上下着雨,我来公司的路上淋湿了,加上昨天晚上有些没睡好,心情有些糟糕,所以到了公司楼下去瑞幸点了一杯自己爱喝的咖啡。

    \n

    对自己好一点,先学会爱自己,才能好好爱别人。

    \n

    留出属于自己的时间

    尽管我们一天当中大部分时间都在公司度过,但这并不意味着在公司的一定要把全部精力投在公司的工作上,要有自己可以掌控的时间。

    \n

    我这里并不是说要让大家在公司接私活,主要指的利用有限的时间高效工作,留出一些时间来提升自己。

    \n

    拿我自己为例,我每天有效的工作时间只有4小时,超过这个时间工作效率会很低下,所以我会给自己安排一些工作工作以外属于自己的事情,比如读书、写 leetcode、学习一些专项课程,最近还加入的写流水账的时间。这些事情和工作都是相辅相成的。

    \n

    学会说不

    这一点在职场中太重要了,同事、领导交给你的任务不要全盘接收。接的工作太多,哪一个也完成不好,最后还搞的自己压力巨大、身心俱疲。

    \n

    现在我会参与每周的需求会,根据人力情况决定下周接几个需求,在已经得知人力不足的情况下,我会好不犹豫把需求拒掉,同时也不会给每个同学排的太紧,结合上一条,我会刻意给他们留出一些自己的时间。

    \n

    要把自己遇到的困难告诉领导,俗话说会哭的孩子有奶吃。

    \n

    关于说不这件事,我想再用个其他人没有用过的角度补充一点:不要觉得自己比别人聪明,屁股决定脑袋,别人不说是因为他的屁股没有在那个位置上。用宝钗的处事哲学就是「事不关己莫开口,一问摇头三不知」。尤其是在人多的场合开会的时候,不要做话唠,不要试图在他人面前证明自己。与其给一个模糊不清的信息,倒不如大方承认自己不知道。

    \n

    Permission to be human

    中文翻译为:允许自己为人,听起来感觉有些别扭。

    \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

    \n

    1) 向某人讲述你的故事。挑选一个你爱的或钦佩的或想与之联系的人,把整个故事直接写给他们——就像你在写一封信。这将带来你的自然声音。无论你做什么,都不要写信给人群。

    \n

    (想象我们所写的内容是在给一个喜欢的姑娘或者我钦佩的人写信,而不是写给一群人的说教。)

    \n

    2) 从故事的开头开始,写出发生的事情,一直写到最后。

    \n

    (有头有尾,循序渐进)

    \n

    3) 使用极其简单的句子。

    \n

    (能说清楚事情就行,不追求华丽的辞藻)

    \n

    4) 不要担心它是否好;只要完成它。无论你的项目是否好,结束后你会成为一个不同的人,这总是值得做的。

    \n

    (完成比完美更重要,只要完成就会有成长。写作不是一蹴而就的,而来你有的是机会来修改、打磨它。)

    \n

    5) 不要以改变任何人的生活为目的而写作。这将导致沉重的、令人恼火的散文。只需分享令你高兴、愤怒或着迷的东西。如果有人的生活因此而改变,那是一种奖励。

    \n

    (写作的目的不是要改变其他人,而是记录自己的所思所想。曹雪芹的《红楼梦》中从来没有批判过一个人的好坏,只是在做客观的描写。)

    \n

    6) 只要你可以,就讲故事而不是解释东西。人类喜欢故事,而我们讨厌别人向我们解释东西。以耶稣为例。他几乎只用比喻说话,并允许每个人从他伟大的讲故事中吸取自己的教训。而且他做得非常好。

    \n

    (多讲故事,人们更容易记住有画面感的东西)

    \n

    7) 你的作品不必是任何特定的长度,或为任何特定的市场而写。它甚至不一定要被另一个人看到。如何以及是否出版你的作品是另一个问题。今天,就写吧。

    \n

    (想写什么写什么,不追求写多长。)

    \n

    8) 记住,您一直在研究自己的一生,只是因为存在。你是你自己经验中的唯一专家。拥抱这一点是你的最高资格。

    \n

    (这句话没明白什么意思,是只有自己才了解自己的意思吗?)

    \n

    9) 每位作家在第一天都是从同一个地方开始的:超级兴奋,并准备好做大事。第二天,每个作家都看着她在第一天写的东西,恨死自己了。专业作家和非专业作家区别在于,专业的作家在第三天回到他们的任务中。让你达到目的的不是骄傲而是怜悯。向自己表示宽恕,因为你不够好。然后继续前进。

    \n

    (成功最大的威胁不是失败,而是倦怠。当你感到心烦意乱,苦不堪言或筋疲力竭时,是鼓足干劲还是萌生退意,这是专业人士和业余人士的分水岭。)

    \n

    10) 愿意让它变得简单。你可能会感到惊讶。

    \n

    (这句话也没有太理解,是想说我们应该把写作当成一件简单的事情去做的意思吗?)

    \n"},{"title":"HTTPie介绍——一个轻量级HTTP客户端","url":"/2019/httpie-introduce/","content":"
    \n

    HTTPie 是一个用于与 HTTP 服务器进行交互的命令行客户端。

    \n
    \n

    \"\"

    \n

    概览

    HTTPie(发音为 H-T-T-派)是一个基于命令行的 HTTP 客户端,可以提供更加人类友好的命令行交互,HTTPie 可用于测试、调试以及与 HTTP 服务器进行交互。

    \n

    HTTPie 提供了一个 http 命令,这个命令可以使用简单自然的语法发送任意 HTTP 请求,并以精美的彩色输出作为响应结果。

    \n

    在这篇文章中,我们将学习如何使用此工具访问 REST 服务。

    \n

    功能

    作为一个现代化命令行工具,HTTPie 提供了如下功能:

    \n
      \n
    • 简单、直观的 HTTP 命令语法
    • \n
    • 漂亮的格式化输出
    • \n
    • 天然的 JSON 支持
    • \n
    • 表单和文件上传
    • \n
    • 支持自定义 HTTP 头
    • \n
    • 主流操作系统支持 —— Linux、macOS、Windows
    • \n
    • 通过插件扩展额外功能
    • \n
    \n

    在后边的文章中,你将看到这些功能的介绍。

    \n

    安装

    可以通过多种方式来安装 HTTPie。

    \n

    macOS

    brew install httpie
    \n

    Linux(Ubuntu)

    apt-get install httpie
    \n

    Windows

    pip install --upgrade pip setuptools
    pip install --upgrade httpie
    \n

    或者

    \n
    easy_install httpie
    \n

    使用

    现在 HTTPie 已经安装在了本地电脑上,可以来调用各种 HTTP 接口。

    \n

    后边的文章中,我将会使用下边三个网站来演示相关功能:

    \n\n

    调用 http

    HTTPie 提供 http 命令来访问 HTTP 服务器。以下是 http 命令最基本的用法,返回了 HTTP 响应头和其他服务器信息。

    \n
    ➜ http httpie.org

    HTTP/1.1 301 Moved Permanently
    CF-RAY: 543ebdd4cad6eb79-LAX
    Cache-Control: max-age=3600
    Connection: keep-alive
    Date: Thu, 12 Dec 2019 09:41:15 GMT
    Expires: Thu, 12 Dec 2019 10:41:15 GMT
    Location: https://httpie.org/
    Server: cloudflare
    Transfer-Encoding: chunked
    Vary: Accept-Encoding
    \n

    获取数据

    最常见的 HTTP 操作是从服务器检索信息,通常通过 HTTP GET 方法来实现。HTTP GET 请求的查询参数是可选的。

    \n

    下边是一个 HTTPie 的 GET 方法示例(无查询参数):

    \n
    http GET http://httpbin.org/get
    \n

    但是,不带查询参数的 GET 请求很少见。可以通过在原始请求后边追加 param==value 的方式来添加参数。

    \n

    下边的示例演示了如何在 GET 请求中携带参数。我们来获取 userId 为 1 的所有帖子。

    \n
    ➜ http https://jsonplaceholder.typicode.com/posts userId==1
    \n

    下边是多个参数的例子:

    \n
    ➜ http https://jsonplaceholder.typicode.com/posts userId==1 id==9

    HTTP/1.1 200 OK
    // 忽略响应头

    [
    {
    "body": "consectetur animi nesciunt iure dolore\\nenim quia ad\\nveniam autem ut quam aut nobis\\net est aut quod aut provident voluptas autem voluptas",
    "id": 9,
    "title": "nesciunt iure omnis dolorem tempora et accusantium",
    "userId": 1
    }
    ]
    \n

    在 HTTP 请求头中携带信息是很常见的做法,在 HTTPie 中我们可以使用 Header:Value 格式添加 HTTP 请求头,如下所示:

    \n
    http example.org X-Foo:Bar Sample:Value
    \n

    发布和更新数据

    HTTP 的 POST 方法通常用于在服务器上创建资源,下边的示例演示了如何使用内联方式提供 JSON 数据并发送 POST 请求,注意:非字符串类型参数的格式为 Param:=Value

    \n
    ➜ http POST https://jsonplaceholder.typicode.com/posts title=foo body=bar userId:=9

    HTTP/1.1 201 Created
    // 忽略响应头

    {
    "body": "bar",
    "id": 101,
    "title": "foo",
    "userId": 9
    }
    \n

    HTTPie 允许我们将 JSON 数据存入文件中,并在命令行中指定这个文件的路径。

    \n
    ➜ cat data.txt
    {
    "title": "foo",
    "body": "bar",
    "userId": 9
    }

    ➜ http POST https://jsonplaceholder.typicode.com/posts data=@data.txt

    HTTP/1.1 201 Created
    // 忽略响应头

    {
    "data": "{\\n \\"title\\": \\"foo\\",\\n \\"body\\": \\"bar\\",\\n \\"userId\\": 9\\n}\\n",
    "id": 101
    }
    \n

    HTTP PUT 方法通常用于更新服务器中已存在的资源,用法和 POST 类似:

    \n
    ➜ http PUT https://jsonplaceholder.typicode.com/posts/10002 data=@data.txt
    \n

    删除数据

    HTTP DELETE 方法用于删除 HTTP 服务器中的资源,示例如下:

    \n
    ➜ http DELETE https://jsonplaceholder.typicode.com/posts/1
    \n

    通过 HTTPie 进行认证

    上边的示例中我们演示了 HTTPie 的核心用法,在这些示例中,我们假设资源都是可以在不需要任何身份认证的情况下就能够访问的。但在实际场景中很少有这种情况,大多数服务都有安全防护,并强制要求它的用户在访问资源前进行身份认证。

    \n

    现代化 HTTP 客户端程序为多种认证模式提供了很好的支持,HTTPie 也不例外,支持主流如:Basic、摘要、密码等认证类型。

    \n

    使用 Basic 认证访问资源

    HTTP Basic 认证是 HTTP 协议中的身份验证方案。在 Basic 认证中,HTTP Authorization 请求头设置为 Basic,用户名和密码以明文形式提供。Basic 认证总是需要配合其他安全机制,如:HTTPS。

    \n

    以下示例演示如何访问一个要求用户通过 Basic 认证来进行身份验证的资源:

    \n
    ➜ http --default-scheme=https https://httpbin.org/basic-auth/username/password

    HTTP/1.1 401 UNAUTHORIZED
    Access-Control-Allow-Credentials: true
    Access-Control-Allow-Origin: *
    // 其他响应头
    \n

    可以看到,请求在未提供用户名和密码的情况下,服务器的响应状态码为 401 UNAUTHORIZED。HTTPie 通过以 -a username:password 的方式提供 Basic 认证所需要的用户名密码:

    \n
    ➜ http --default-scheme https https://httpbin.org/basic-auth/username/password -a username:password
    HTTP/1.1 200 OK
    Access-Control-Allow-Credentials: true
    Access-Control-Allow-Origin: *
    Connection: keep-alive
    // 其他响应头

    {
    "authenticated": true,
    "user": "username"
    }
    \n

    使用摘要认证访问资源

    Basic 认证的主要问题是它将用户名和密码以明文的方式发送至服务器。摘要认证略有不同,在摘要认证而非明文模式中,它采用基于哈希的方法与服务器传递凭据。

    \n

    以下是摘要认证的流程:

    \n
      \n
    1. 客户端请求一个需要认证的页面,但是不提供用户名和密码。
    2. \n
    3. 服务器返回 401 Unauthorized 响应代码,并提供认证域(realm),以及一个随机生成的、只使用一次的数值,称为密码随机数 nonce。
    4. \n
    5. 客户端以上一步中得到的随机数(nonce)、用户名、密码和 realm 的哈希值作为响应
    6. \n
    7. 服务器利用这些信息对客户端进行身份验证,如果身份验证成功,则返回客户端所请求的资源
    8. \n
    \n

    HTTPie 使用 -A 摘要标志 并通过 -a 参数提供相应的用户名和密码即可进行摘要认证,如下所示:

    \n
    ➜ http --default-scheme https -A digest -a aa:bb https://httpbin.org/digest-auth/auth/aa/bb
    HTTP/1.1 200 OK
    Access-Control-Allow-Credentials: true
    Access-Control-Allow-Origin: *
    Connection: keep-alive
    // 其他响应头

    {
    "authenticated": true,
    "user": "aa"
    }
    \n

    插件方面,HTTPie 还支持其他身份验证机制,如:jwt-auth、OAuth 等。

    \n

    总结

    HTTPie 是一个轻量但强大的工具,可以轻松与 HTTP 服务器通信。通过 http 命令并配合合理参数调用各种 HTTP 方法的能力使其成为 RESTful 和微服务生态的理想选择。

    \n"},{"title":"我阳了,我好了","url":"/2022/i-am-positive-i-am-ok/","content":"

    结论先行,先说说我是吃了什么药康复的:

    \n
      \n
    • 第一晚和第二晚每次一粒对乙酰氨基酚
    • \n
    • 第二天白天吃了一小瓶桃罐头
    • \n
    \n

    是的,就这么些东西。

    \n

    下边记录一些中间的过程。

    \n

    第一天

    12月13日,星期二

    \n

    中午的时候嗓子开始不舒服,有点沙沙的感觉,而且腰有些酸,以为是坐姿有问题,总想躺着,多亏是在家办公,工作一会躺一会。

    \n

    5:30左右测了个体温,37.3℃,没太当回事,但是明显感觉体力开始急剧下降。开完公司晚会后,7:30再次测了个体温 38.5℃,这时候已经完全不想动弹了,浑身发冷一直打寒颤,冷到想盖上10层棉被。

    \n

    晚上挣扎着洗了澡,睡前吃了粒对乙酰氨基酚,钻被窝盖了两层被子,半昏半睡、睡一会醒一会,中间还出现过幻觉,虽然一晚上无法动弹没有测过体温,但我自己估摸着肯定到了40℃,晚上摸身上火烧火燎的,浑身疼,甚至蛋蛋也疼。。。

    \n

    第二天

    2022年12月14日,星期三

    \n

    艰难的爬起床,浑身疼,那种疼像是跑了10公里步或者被揍了一顿似的,于是和老板请假,之后将手机通知关闭、静音。这一天除了吃饭喝水上厕所其余时间就是躺着,中间还吃了个黄桃罐头,冰冰凉凉的吃下去的时候很舒服。

    \n

    \n

    就这么难受我还加持把今天的多邻国学习了,为了不破坏200多天的连胜记录😂

    \n

    我把两层窗帘拉紧,灯关掉,屋里完全黑的,就这样睡一会、醒一会、刷一会小红书,这天还有些拉肚子,但不是很稀。晚上睡觉前吃了一粒对乙酰氨基酚。

    \n

    晚上八点多睡的,到第二天早上5点,加上白天的时间,这应该是我近些年卧床时间最长的一次。

    \n

    第三天

    2022年12月16日,星期四

    \n

    一觉醒来感觉舒服多了,测了下体温也基本退烧了,身上也没那么疼了,就是嗓子巨疼无比,开始咳嗽,咳嗽时喉咙和肺疼,能咳出浓痰。

    \n

    考虑的目前是居家办公,也不用通勤,实在累了也可以躺会,于是就没有再请假,强行开机开始上班搬砖了。

    \n

    下午的时候测了个抗原,阳气十足。

    \n

    \n

    第四天(今天)

    2022年12月16日,星期五

    \n

    今天算起来是得病的第4天,嗓子有些疼、咳嗽,说话非常非常吃力且沙哑,除此之外就没有其他症状了。

    \n

    嗅觉味觉还在,但是貌似不那么灵敏了,预计还需要3、5天才能转阴。

    \n

    昨晚睡的有些晚,而且睡前玩了会手机,之前从来不玩,但是根据前两天生病的经验发现玩手机也能睡着。将近12点放下手机的时候感觉还是没有困意就开始看书,看到胳膊举不动书了放下书开始尝试入睡,两点多还是没睡着我意识到失眠了,于是起来吃了安眠药。

    \n

    只有生病最严重的那两天我真正让自己放轻松了,不管再晚再难受也不会感觉有什么焦虑,不再考虑工作或者其他烦心的事情,可能是身体的本能告诉我狗命要紧,别考虑乱七八糟的了。

    \n

    我得新冠后只耽误了一天工作,真是个合格的打工人。🙂

    \n

    P.S. 我发现这几天都没有晨勃过了,可能是不行了吧。

    \n
    \n

    我前几天续订了独库的2023全年阅读计划,今天刚好收到了一份读库提前送来的小礼物,如果今年让我推荐一本书的话,毫无疑问我会推荐读库,他不是一本书,而是一个每两个月发行一期的综合性人文社科读物,以中篇非虚构文章为主,内容包括传记、书评、影评、历史事件等。

    \n


    \n"},{"title":"可能即将拥有人生中第一台摩托车","url":"/2023/i-want-a-motorcycle/","content":"
    \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

    \n

    重启 IDEA,发现已经不会卡顿了。

    \n

    再有一个是我使用 IDEA 生成的 gettersetterprotected 的,我用同事电脑测了一下,他的生成的确是 public 的,经过如下设置改回了正常:

    \n
    File | Settings | Editor | Code Style | Java
    |
    Code Generation
    |
    Default Visibility
    \n

    改为 Public 即可。

    \n"},{"title":"解决在 CentOS 通过 yum 安装的 Java 没有 jps 的问题","url":"/2018/install-jps-on-centos/","content":"

    在公司的 CentOS 上通过 yum 安装了一个 Java,但是使用时发现没有 jps 命令,解决方法是安装 jdk-devel 这个包,它提供了 jps 工具。

    \n

    先查看有哪些可用的安装包:

    \n

    yum list | grep jdk-devel

    \n

    \"\"

    \n

    然后找到对应自己 Java 版本和系统的那个包进行安装:

    \n

    sudo yum install java-1.8.0-openjdk-devel.x86_64

    \n

    搞定~

    \n"},{"title":"Linux 安装 node 和 npm","url":"/2019/install-node-and-npm-on-linux/","content":"

    网上介绍 Node 如何安装的文章数不胜数,但我还是决定自己写一篇记录一下,最主要的原因是网上的文章比较混乱,有的建议通过包管理工具安装,还有的让一步步编译源码来安装。

    \n

    通过包管理工具安装的通常版本不会太新,通过源码安装的方式非常麻烦,还需要提前安装 gcc 之类的,只有极少部分良心博主介绍了通过二进制文件直接安装的方式,但操作上都不是特别规范。

    \n

    网上已有的文章还有一个很严重的问题,就是没有考虑国内的网络环境,不管从 Node 官方下载源码包还是二进制包,都巨慢无比,所以我把已经下载好的包放在 CDN 上供自己和大家之后使用。同时我还提供了其他常用软件的安装包,如 Nginx,Java,Neo4j 等等,后边有机会列个清单出来,并准备长期维护更新版本。

    \n
    \n

    下边进入正题:

    \n
    \n

    我推荐以下操作在 /opt 目录下进行

    \n
    \n

    下载压缩包

    wget http://developer.jpanj.com/node-v10.15.3-linux-x64.tar.xz

    \n

    解压为 tar 包

    xz -d node-v10.15.3-linux-x64.tar.xz

    \n

    解压

    tar -xvf node-v10.15.3-linux-x64.tar

    \n

    当前目录下软链一个 node 目录出来

    \n

    这样做的好处是,未来升级版本非常方便,只需要更新这个软链就行

    \n
    \n

    ln -s ./node-v10.15.3-linux-x64 ./node

    \n

    通过软链接,将可执行程序放入系统环境变量的路径中

      \n
    • 查看当前系统中都有哪些环境变量路径
    • \n
    \n
    # echo $PATH
    /usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/root/bin
    \n

    可以看到我的列表中有:

    \n
      \n
    • /usr/local/bin
    • \n
    • /usr/bin
    • \n
    \n

    大家约定成俗逻辑是:

    \n
      \n
    • /usr/bin下面的都是系统预装的可执行程序,会随着系统升级而改变。
    • \n
    • /usr/local/bin 目录是给用户放置自己的可执行程序的地方
    • \n
    \n

    所以我推荐将软链放在 /usr/local/bin 目录下:

    \n
    ln -s /opt/node/bin/node /usr/local/bin/node
    ln -s /opt/node/bin/npm /usr/local/bin/npm
    ln -s /opt/node/bin/npx /usr/local/bin/npx
    \n

    检查是否安装成功

    [root@dc8 ~]# node -v
    v10.15.3
    [root@dc8 ~]# npm -v
    6.4.1
    \n

    Done

    "},{"title":"离线安装 Python requests 包","url":"/2019/install-requests-offline/","content":"
    \n

    requests 是一个简单优雅的 Python HTTP 库,相较于 Python 标准库中的 urlliburllib2requests 更加的便于理解使用。

    \n
    \n

    背景介绍

    由于某地区热点事件持续升温,我们的客户想要通过我们系统的搜索功能导出一批数据,目前我们的搜索结果是不支持导出的,并且搜索功能也是通过调用几个子服务后对数据进行了合并,所以无法直接通过 ElasticSearch 来捞数据。

    \n

    我们在评估需求后,预计编写这个统计程序大概需要 1 天的时间,但是客户认为事态紧急,当天就要结果,我们本着顾客就是上帝的原则,又进行了一番讨论,结论是可以写一个类似爬虫的工具,来「爬取」我们自己的搜索接口来拿到这些数据。

    \n

    Python 来实现最合适不过,而且我对编写爬虫也比较熟悉,所以就采用了最简单粗暴的方法:用 requests 包作为一个 HTTP Client 来收发请求,但是客户现场是个离线环境,之前我们也没有安装过 requests,所以才有了本文:在离线环境中安装 requests

    \n

    正文

    资源准备

    为方便后期使用,我将所有用到的文件打包在了一起,可直接解压使用,无需从网上东奔西走寻找资源。

    \n

    压缩包内涉及到的文件如下:

    \n
    setuptools-41.1.0.post1.tar
    pip-19.2.2.tar.gz
    certifi-2019.9.11-py2.py3-none-any.whl
    chardet-3.0.4-py2.py3-none-any.whl
    idna-2.8-py2.py3-none-any.whl
    urllib3-1.25.7-py2.py3-none-any.whl
    requests-2.22.0.tar.gz
    \n

    打包资源下载链接:http://developer.jpanj.com/requests-offline.tar.gz

    \n

    安装

    解压 requests-offline.tar.gz 后进入 requests-offline 目录开始安装。

    \n

    安装 setuptools

    \n

    setuptools 能帮助我们更好的创建和分发 Python 的包,尤其是具有复杂依赖关系的包。

    \n
    \n
    tar -zxvf setuptools-41.1.0.post1.tar.gz
    cd setuptools-41.1.0.post1/
    python setup.py install
    \n

    安装 pip

    \n

    pip 是 Python 官方推荐的包管理工具。

    \n
    \n
    tar -zxvf pip-19.2.2.tar.gz
    cd pip-19.2.2/
    python setup.py install
    \n

    安装 requests 所需的其他依赖

    # CA 认证模块
    pip install certifi-2019.9.11-py2.py3-none-any.whl
    # 字符编码检测模块
    pip install chardet-3.0.4-py2.py3-none-any.whl
    # 域名解析模块
    pip install idna-2.8-py2.py3-none-any.whl
    # 线程安全的 HTTP 库
    pip install urllib3-1.25.7-py2.py3-none-any.whl
    \n

    安装 requests

    tar -zxvf requests-2.22.0.tar.gz
    cd requests-2.22.0/
    python setup.py install
    \n

    测试是否成功成功

    ➜ python
    >>> import requests
    \n

    后记

    客户是下午两点半提出的需求,内部讨论好实现方案后三点半,我在准备好安装 requests 所需资源后直接奔赴现场,而不是在公司编码完再过去,因为我相信只要有我们的人到达现场客户就可以踏下心来了,同时我也想挑战一下自己,所以本次点亮了一个技能点:现场使用 vi 进行 coding 和 debug。

    \n"},{"title":"融入一个城市","url":"/2023/integrating-city/","content":"

    作为一个北漂或在大城市打工的人,您是否有过融入这个城市的瞬间?如果有,是在什么样的时刻产生的?

    \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":"

    一个 IPv4 地址有多少位?

    32 位

    \n

    比如, 127.0.0.1:

    \n
      127         0    
    01111111 00000000
    0 1
    00000000 00000001
    \n

    192.168.1.0/24 代表哪些 IP 地址?

    192.168.1.0 到 192.168.1.255

    \n

    192.168.1.0/24 表示所有与 192.168.1.0 前 24 位相同的地址

    \n

    有 2^(32-24)=256 个这样的 IP 地址

    \n

    这被称为「CIDR 表示法」

    \n

    每个 TCP 数据包都有 IP 地址吗?

    是的,有两个

    \n

    下面是 TCP 包的结构:

    \n
    +-+-+-+-+-+-+-+-+--+-
    | Ethernet header |
    +-+-+-+-+-+-+-+-+--+-
    | IP header |
    +-+-+-+-+-+-+-+-+--+-
    | TCP header |
    +-+-+-+-+-+-+-+-+-+-+
    | packet contents |
    +-+-+-+-+-+-+-+-+-+-+
    \n

    IP 头包含一个源和目的 IP 地址,一个 TTL,一个长度字段,以及一些其他东西。

    \n

    每个 UDP 数据包都有 IP 地址吗?

    是的

    \n

    它的结构与 TCP 数据包相同,只是有一个 UDP 头而非 TCP 头。

    \n

    每个 IP 数据包都有端口吗?

    不是

    \n

    IP 报头含一个 IP 地址,但没有端口

    \n

    例如,ICMP 数据包(ping 使用的数据包)有一个IP地址,但没有端口

    \n

    TCP 和 UDP 数据包有端口

    \n

    计算机的公网 IP 地址是否允许为10.11.12.13?

    不允许

    \n

    以下 3 个 IP 范围是为私有网络保留的:

    \n
      \n
    • 10.0.0.0 – 10.255.255.255
    • \n
    • 172.16.0.0 – 172.31.255.255
    • \n
    • 192.168.0.0 – 192.168.255.255
    • \n
    \n

    127.0.0.0/8 也被保留用于同一台计算机内部的连接。

    \n

    如果你的计算机在本地网络上的 IP 地址是 192.168.1.123,它向 google.com 发送了一个数据包,当数据包到达 google.com 时,数据包上的源 IP 地址是 192.168.1.123 吗?

    不是

    \n

    192.168.1.123是本地 IP 地址,所以谷歌将无法联系到你的电脑

    \n

    路由器会改写数据包,将数据包的源 IP 改写为计算机的公网 IP

    \n

    这被称为「网络地址转换」 或 NAT(network address translation)

    \n

    是否可以发送一个 2M 的 IPv4 数据包?

    不行

    \n

    IPv4 数据包的长度字段为 16 位,所以最大长度是 65535。

    \n

    在实践中,数据包往往必须比这个数字更小(常见的限制是1500字节)。

    \n

    你的 ISP 的路由器使用数据包的哪一部分将数据包发送到正确的服务器?

    目的IP地址

    \n

    它们一般不使用数据包中的任何其他信息。

    \n

    网站如何知道你所在的国家?

    根据你的源 IP 地址

    \n

    很多服务使用你的源 IP 地址来确定你在哪个国家。这就是为什么很多人使用 VPN:他们通过 VPN 进行代理,从而使数据包的源 IP 地址在一个不同的国家。

    \n

    将 IP 头中的 TTL 字段被设置为 64 有什么用?

    它被允许进行 63 次跳跃

    \n

    这意味着在它完成了大约 63 跳(到 63 个服务器或路由器)之后,就不会被进一步发送。

    \n

    TTL(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 来加载,然后还是感觉各种麻烦。

    \n

    这时候想到了谷歌,查找资料后发现一个 jQuery 插件是专门来实现这个需求的,而且实现方法跟我设想的完全不一样,大致原理是:就像做普通翻页那样,告诉它下一页的地址,再告诉它需要加载更多部分的节点,这个插件会异步请求那个页面,然后把相应部分取出,加载到当前页面的底部。

    \n

    我了个擦,我居然没有想到这种方法,我之前设想方法,还要单独去写个用来翻页的接口,增加了很多工作量,这种方法简直是棒呆了!

    \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 什么的,里边写着上拉加载更多这样的提示信息。当加载更多被触发时,这个部分会自动隐藏掉。

    \n

    下边来看看 js 代码部分:

    \n
    $(document).ready(function () {
    $("#masonny-div").infinitescroll({
    navSelector: "#next:last", // 页面分页元素(成功后会被隐藏)
    nextSelector: "a#next:last", // 需要点击的下一页链接
    itemSelector: "div.section", // ajax回来之后,每一项的selecter
    animate: true, //加载完毕是否采用动态效果
    extraScrollPx: 100, //向下滚动的像素,必须开启动态效果
    debug: true, //调试的时候,可以打开,
    path: function (index) {

    return "?page=" + index;
    },
    loading: {
    finished: undefined,
    finishedMsg: '没有更多内容了', //当加载失败,或者加载不出内容之后的提示语
    img: '/static/pic/loading-new.gif', //自定义loadding的动画图
    msgText: '正在加载中...', //加载时的提示语
    }
    });

    })
    ;
    \n

    因为我这里需要提取出来加载更多的部分是这样的:

    \n
    <div class="col-xs-12 section">
    <div onclick="location.href='{{ artcle.url }}'">
    <div><h4><a>{{ artcle.title }}</a></h4></div>
    <div class="content">{{ artcle.abstract }}</div>
    </div>
    <div class="row tag">
    <div class="col-xs-6">
    <div class="row" style="white-space:nowrap;">
    {% for tag in artcle._tags[:3] %}
    <div class="col-xs-4" onclick="location.href='/tag/{{ tag.tag_id }}'">#{{ tag.name }}</div>
    {% endfor %}
    </div>
    </div>
    <div class="col-xs-4 col-xs-offset-2"
    style="text-align: end">{{ artcle.publish_date | datetime('date') }}</div>
    </div>
    </div>
    \n

    所以我的 itemSelector 的值为 div.section

    \n

    还有一点,我这个用这个插件的时候,刚开始的时候一直有问题,是因为没有给 path 写值, path 的作用是每次加载下一页的时候所对应的地址。

    \n

    还可以给加载更多时候的 loading 编写样式,例如:

    \n
    #infscr-loading {
    text-align: center;
    z-index: 100;
    position: fixed;
    left: 45%;
    bottom: 40px;
    width: 200px;
    padding: 10px;
    background: #000;
    opacity: 0.8;
    color: #FFF;
    -webkit-border-radius: 10px;
    -moz-border-radius: 10px;
    border-radius: 10px;
    }
    \n

    至此,我又 get 到一个新技能。

    \n","categories":["Code"],"tags":["JS","jQuery","Web"]},{"title":"Java 8 StringJoiner 示例","url":"/2019/java-8-stringjoiner/","content":"

    在 Java8 中,java.util 包中引入了一个新类 StringJoiner。利用这个类,我们可以使用指定分隔符连接多个字符串,还可以为最终字符串添加前缀和后缀。

    \n

    下边介绍一些 StringJoiner 类的示例。

    \n

    示例1:通过指定分隔符连接字符串

    在这个例子中,我们使用 StringJoiner 连接多个字符串,在创建 StringJoiner 实例时我们将分隔符指定为连字符(-)。

    \n
    import java.util.StringJoiner; 

    public class Example {
    public static void main(String[] args) {
    // 传递连字符(-)作为分隔符
    StringJoiner mystring = new StringJoiner("-");

    // 通过 add() 方法连接多个字符串
    mystring.add("张三");
    mystring.add("李四");
    mystring.add("王五");
    mystring.add("赵六");

    System.out.println(mystring);
    }
    }
    \n

    输出:

    \n
    张三-李四-王五-赵六
    \n

    示例2:为输出字符串添加前缀和后缀

    import java.util.StringJoiner;  

    public class Example {
    public static void main(String[] args) {
    /* 传递逗号(,)作为连字符
    * 左括号为前缀右括号为后缀
    */
    StringJoiner mystring = new StringJoiner(",", "(", ")");

    mystring.add("张三");
    mystring.add("李四");
    mystring.add("王五");
    mystring.add("赵六");

    System.out.println(mystring);
    }
    }
    \n

    输出:

    \n
    (张三,李四,王五,赵六)
    \n

    示例3:合并两个 StringJoiner 对象

    import java.util.StringJoiner;

    public class Example {
    public static void main(String[] args) {
    StringJoiner mystring = new StringJoiner(",", "(", ")");

    mystring.add("张三");
    mystring.add("李四");
    mystring.add("王五");
    mystring.add("赵六");

    System.out.println("First String: " + mystring);

    StringJoiner myanotherstring = new StringJoiner("-", "pre", "suff");

    myanotherstring.add("小张");
    myanotherstring.add("小李");
    myanotherstring.add("小王");
    myanotherstring.add("小赵");

    System.out.println("Second String: " + myanotherstring);

    /* 合并两个字符串要注意的是
    * 输出字符串将具有第一个字符串的分隔符前缀和后缀
    */
    StringJoiner mergedString = mystring.merge(myanotherstring);
    System.out.println(mergedString);
    }
    }
    \n

    输出:

    \n
    First String: (张三,李四,王五,赵六)
    Second String: pre小张-小李-小王-小赵suff
    (张三,李四,王五,赵六,小张-小李-小王-小赵)
    \n

    在上边的例子中我们学习了 StringJoiner 类的 add()merge() 方法,再来看看这个类的其他方法。

    \n

    setEmptyValue(),length() 和 toString() 方法

    import java.util.StringJoiner;  

    public class Example {
    public static void main(String[] args) {

    StringJoiner mystring = new StringJoiner(",");

    /* 使用 setEmptyValue() 方法可以设置 StringJoiner 实例
    * 的默认值如果 StringJoiner 为空并且
    * 我们打印它的值就会显示此默认值
    */
    mystring.setEmptyValue("这是个默认字符串");

    /* 我们还没有向 StringJoiner 添加任何字符串
    * 所以这应该显示 StringJoiner 的默认值
    */
    System.out.println("Default String: " + mystring);


    mystring.add("苹果");
    mystring.add("香蕉");
    mystring.add("橘子");
    mystring.add("猕猴桃");
    mystring.add("葡萄");
    System.out.println(mystring);

    /* StringJoiner 类的 length() 方法返回
    * 字符串的长度(StringJoiner 实例中的字符数)
    */
    int length = mystring.length();
    System.out.println("Length of the StringJoiner: " + length);

    /* toString() 方法用于将 StringJoiner
    * 实例转换为字符串
    */
    String s = mystring.toString();
    System.out.println(s);
    }
    }
    \n

    输出:

    \n
    Default String: 这是个默认字符串
    苹果,香蕉,橘子,猕猴桃,葡萄
    Length of the StringJoiner: 15
    苹果,香蕉,橘子,猕猴桃,葡萄
    \n

    参考:

    Java 8 – StringJoiner JavaDoc

    \n"},{"title":"贾攀的 Macbook 使用指北","url":"/2018/jiapan-macbook/","content":"
    \n

    工欲善其事,必先利其器

    \n
    \n

    算起来我用 Macbook 也有三年多的时间了,中间由于一次工作原因,开发环境一直有问题,就重装了一次系统,也仅仅这一次,而且这次其实也是冤枉了系统,当时是因为我的 host 文件没有配置正确导致的,可以说 macOS 是相当稳定的,并且也是开发利器,我写这篇文章的目的主要是为了之后自己在换新的 Macbook 时有记录可寻,同时帮助其他 Macbook 用户来发现一些好用的工具。

    \n

    以下神器排名分先后

    iTerm2:终端神器

    iTerm2 是 Mac 下最好的终端工具,大部分功能都是开箱即用,简单介绍下 iTerm2 的特色功能:

    \n

    智能选中

    在 iTerm2 中,双击选中单词,三击选中整行,四击智能选中(智能规则可配置),可以识别网址,引号引起的字符串,邮箱地址等。(很多时候双击的选中就已经很智能了)

    \n

    在 iTerm2 中,选中即复制。即任何选中状态的字符串都被放到了系统剪切板中。

    \n

    巧用 Command 键

    按住⌘键:

    \n
      \n
    • 可以拖拽选中的字符串
    • \n
    • 点击 URL:调用默认浏览器访问该网址
    • \n
    • 点击文件:调用默认程序打开文件
    • \n
    • 点击文件夹:在 finder 中打开该文件夹
    • \n
    • 同时按住 Option 键,可以以矩形选中
    • \n
    \n

    常用快捷键

      \n
    • 切换 tab:⌘+←, ⌘+→, ⌘+{, ⌘+}⌘+数字 直接定位到该 tab
    • \n
    • 新建 tab:⌘+t
    • \n
    • 顺序切换 pane:⌘+[, ⌘+]
    • \n
    • 按方向切换 pane:⌘+Option+方向键
    • \n
    • 切分屏幕:⌘+d 水平切分,⌘+Shift+d 垂直切分
    • \n
    • 智能查找,支持正则查找:⌘+f
    • \n
    \n

    \"\"

    \n

    用于搜索关键字,按 Tab 键可以自动补全单词,且补全的单词可以直接粘贴到其他地方

    \n

    \"\"

    \n

    分屏功能很实用啊有木有

    \n

    自动完成

    iTerm2 可以自动补齐命令,输入若干字符,按 ⌘+; 弹出自动补齐窗口,列出当前可用的命令。

    \n

    Exposé Tabs

    ⌘+Option+e 全屏展示所有的 tab,可以搜索

    \n

    \"\"

    \n

    高亮当前鼠标的位置

    一个标签页中开的窗口太多,有时候会找不到当前的鼠标,⌘+/ 找到它。

    \n

    \"\"

    \n

    配色

    你可以自由定制喜欢的配色,这里 收集了大量 iTerm2 的主题,你可以选择使用。我用的是Zenburn。在其 github repo 里下载对应的xxx.itermcolors文件,双击安装使用。

    \n
    \n

    zsh:最强 shell

    都用了这么好用的终端了,不考虑再换个 shell 吗?

    \n

    zsh 的安装方法和介绍见:http://macshuo.com/?p=676

    \n
    \n

    Moom:窗口调节神器

    macOS 系统不能像 Windows 那样最大化是不是很不爽?一言不合就全屏!用 Moom 来解决这个问题吧!

    \n

    安装后,将鼠标悬浮在你想调整窗口的全屏按钮上(就是那个绿色按钮),下方就会出现一些扩展选项:

    \n

    \"\"

    \n

    从左到右依次为:最大化、将窗口平铺在屏幕左半边、将窗口平铺在屏幕右半边、将窗口平铺在屏幕上半边、将窗口平铺在屏幕下半边

    \n

    这几个选项非常实用,比如你想打开一个网页同时打开一个笔记工具,这时你就可以直接让浏览器占用左半边,笔记工具占用右半边,不需要自己手动拖拽调整大小啦。

    \n
    \n

    Paste:剪切板神器

    复制粘贴是我们日常工作和开发中常用的功能,Paste 为我们提供了剪切板历史记录的功能,我们可以通过 cmd+shift+v 来查看记录,通过方向键选择我们需要的内容后敲回车完成之前复制内容的粘贴:

    \n

    \"\"

    \n
    \n

    Keyboard Maestro:设置快捷键神器

    我日常用 HHKB 来码字,这个键盘最大的优点就是小巧,最大的缺点也是小巧,很多键是没有的,比如方向键。

    \n

    我通过 Keyboard Maestro 来设置一些组合键作为方向键,同时设置另外一些组合键作为 App 启动热键。

    \n

    介绍看我另一篇博客:使用-KM-处理-HHKB-方向键/

    \n
    \n

    Karabiner-Elements

    这条是后来补充的,此时我已经将 HHKB 组合实现方向键的功能由楼上的 Keyboard Maestro 改为了 Karabiner-Elements,Keyboard Maestro 只留下了通过组合键启动应用的功能,Karabiner-Elements 可以更多的对键盘进行自定义,比如为了防止误触发,我开起了敲击 command+q 两次才退出应用的功能,同时还开启了当我接入外接键盘时,自动禁用自带键盘的功能。

    \n

    \"\"

    \n

    \"\"

    \n
    \n

    Go2shell

    \n

    已有楼下的 OpenInTerminal 代替

    \n
    \n

    当你在 finder 中进入一个目录后,这时你想用命令行在这个目录中做一些操作,你需要手动打开终端然后一层一层 cd 进去。

    \n

    Go2shell 来解救你吧,安装完之后,会在你的 finder 上部出现它的 logo,不管你当前在哪个目录,如果你想让你的命令行也进到这个目录中时,只需点一下那个小 logo 就行了:

    \n

    \"\"

    \n
    \n

    OpenInTerminal

    比 Go2shell 功能更丰富,OpenInTerminal 不仅可以直接打开终端 并 cd 到相应目录,同时还提供了复制路径、用编辑器打开的便捷功能。

    \n

    \"\"

    \n
    \n

    Surge

    不多介绍,官方定义为:「高级网络工具箱」。

    \n

    \"\"

    \n
    \n

    Things3

    我最喜欢的 GTD 应用,没有之一。

    \n

    \"\"

    \n
    \n

    Enpass

    我们普遍都有很多不同的帐号,生活中还有各种重要信息需要记忆,单纯靠脑子真的很难记忆和管理。而所有账号使用同一密码绝对是巨大的安全隐患,因此我们还需要一款安全可靠,而且足够方便使用的密码管理器软件。

    \n

    Enpass 是一款安全可靠的跨平台密码管理器软件,提供了包括 Windows、Mac、Linux 以及 iOS、Android 在内的几乎所有平台的客户端,并且提供主流浏览器的一键登录扩展,基本能覆盖你所有的密码应用场景。

    \n

    \"\"

    \n
    \n

    Bartender 3

    Bartender 3 是一款Mac菜单栏自定义工具,简单说就是可以将指定的程序图标隐藏起来,需要时呼出。

    \n

    \"\"

    \n
    \n

    TODO…

    \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\n

    又找到了一个教陆冲很好的 Up 主,比之前找到的教学内容更全面、更详细:纷飞的大脚

    \n"},{"title":"陆地冲浪板学习-week4","url":"/2022/land-surfboard-study-week4/","content":"

    这周陆冲学习了三个技能,折叠 pumping、单膝跪板转弯、slide。

    \n

    slide⬇️:

    \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年后,我回来了。

    \n

    Life 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
      \n
    1. 多想,做好规划
    2. \n
    3. 不再想着做团队内超级兵
    4. \n
    5. 让其他人了解我而不用认同我
    6. \n
    7. 稳定情绪
    8. \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

    今年一月底,春节后从老家开车回北京,在服务区休息的时候遇到了另外一对也在休息的家长,稍微攀谈了一会发现是老乡,聊着聊着就聊到的对方爸爸的工作。对方爸爸在北京的少年宫里做老师,细问之下是在我们住的丰台区少年宫,再细问他教的是我娃一直想学的陶艺课程。

    \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

    flag 就像个咒语,立了基本都会反向达成。比如:今年我一定要减5斤,现在已经涨了5斤。

    \n
    \n

    6月12日的时候,我写了一篇流水账,当时立了个 flag 说要在一天之内考下摩托车驾照,但第二天在科目二考试中挂了,拖着狼狈的身体在回家的高铁上,又写了篇流水账记录当时丧气、失望的心情。

    \n

    前一天还「男人至死是少年」的豪言壮志,第二天就成了「摩托车也不是我的必需品」的泄了气的气球。

    \n

    经过那次考试失利后,我脑海中一直回荡着考试过程的景象,反复在脑子里对那次考试进行复盘,设想如果我当时怎么怎么做就不会挂了。越是想让自己不去回忆这件事,反而回忆的更多(白熊效应)。

    \n

    经过了好几天都无法消化这次失败,机缘巧合在一篇知乎回答里看到有用户推荐「哈佛幸福课(积极心理学)」,我觉得应该会对我有帮助,所以开始学习这门课程。

    \n

    学到一半多的时候,我挥之不去的挫败感基本被课程中的观点治愈了,尤其是那些关于失败的论点:

    \n
      \n
    • 学会失败,从失败中学习,要想进步就必须学会失败。
    • \n
    • 要像接受我们所爱的人的失败那样去接受自己的失败。
    • \n
    • 失败避免不了,你要从失败中学习。
    • \n
    • 最成功的人往往是失败次数最多的。
    • \n
    • 把失败看成成长的工具,这可以更好的了解自己。
    • \n
    • 在成功或失败过后,会有大起大落,但我们会恢复过来,我们一生基本沿基准的幸福发展。
    • \n
    • ……
    • \n
    \n

    于是在给自己做好心理建设后,我准备二战。

    \n

    上一次考试是在我休陪产假期间,我选择了周二这个工作日。考试地点一周有三天可以考试:周二、周四、周六。这一次为了不耽误工作,只能选择周六考试。我在周四报名并缴纳了补考费。周五下午5点我去吃了个驴肉火烧,打包了一个火烧准备路上吃。6点多我提前下班从公司出发,7点多到达大巴车集合地点,再次坐上去德州的大巴车。这一天是6月30日。

    \n

    为了积攒一些好运,我花了99元在小宇宙购买了「谐星聊天会特别季」节目。第一次去的时候就看到这个节目,但没有购买。当时心中有个念头一闪而过:我不会因为没买而挂吧?最后果然挂了,我在复盘的时候也想过有没有可能是没买这个节目导致的😂。

    \n

    认真练习

    7月1日凌晨1点多到达训练地点,开始为期7小时的「特种兵」训练,因为这次我只需要补考科目二,所以可以把所有精力都放在科目二上。由于是周末,考试的人很多,每练一轮要等20多分钟,这中间我没有休息,一直练到天亮。

    \n

    凌晨2点:

    \n

    凌晨3点:

    \n

    凌晨4点:

    \n

    凌晨5点:

    \n

    早上6:30又吃了一次非常难吃的包子加小米粥:

    \n

    准备考试

    8点左右,我们被拉到车管所办事大厅办理考试报名,由于我是补考,手续比较简单。

    \n

    这一批有3个人要补考,8点半提前把我们拉到了考试地点,9点开始考试,我们几个是当天最先考试的三个人,我是第二个考试,三个人都过了。

    \n

    然后就是等待科目四考试,十点半左右满分考完了科目四,至此我的摩托车考试流程结束了,所有科目都是满分。

    \n

    考完试后第一件事就是卸载了「驾考宝典」APP,感谢它的陪伴😂

    \n

    考完科目四后在车管所门口的小卖店买了个雪糕奖励自己:

    \n

    11点多新证就下来了

    这一天是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":"

    ping

    「这些计算机还在线吗?」

    \n

    curl

    发送任何你需要的 HTTP 请求。

    \n

    httpie

    和 curl 一样,但操作更简单

    \n

    wget

    下载文件

    \n

    tc

    流量控制命令,可以降低其他人的网速

    \n

    dig / nslookup

    「这个域名的 IP 地址是多少?」(DNS 查询)

    \n

    whois

    「这个域名注册了吗?」

    \n

    ssh

    安全的 shell

    \n

    scp

    通过 SSH 协议拷贝文件

    \n

    rsync

    只拷贝有过改动的文件(通过 SSH 协议)

    \n

    ngrep

    网络版的 grep 命令

    \n

    tcpdump

    「把 80 端口的所有网络包展示给我!」

    \n

    wireshark

    通过 GUI 查看 tcpdump 抓的包

    \n

    tshark

    非常强大的网络报分析命令行工具

    \n

    tcpflow

    抓取与聚合 TCP 流

    \n

    ifconfig

    「我的 IP 地址是多少?」

    \n

    route

    查看和修改路由表

    \n

    ip

    用于代替 ifconfig、route 等其他命令

    \n

    arp

    查看你的 ARP 表

    \n

    mitmproxy

    具有 SSL/TLS 功能的交互式拦截侦听代理

    \n
    \n

    MITM 是 Man-in-the-middle 的缩写。

    \n
    \n

    nmap

    网络连接端扫描软件

    \n

    zenmap

    nmap 的 GUI 版本

    \n

    p0f

    被动网络指纹识别工具

    \n

    openvpn

    VPN 软件

    \n

    wireguard

    新的 VPN 软件

    \n

    nc

    Netcat,手动建立 TCP 连接

    \n

    socat

    Netcat 的加强版,主要特点是在两个数据流之间建立通道

    \n

    telnet

    类似于 ssh,但不安全

    \n

    ftp / sftp

    用于文件拷贝,sftp 是基于 ssh 的。

    \n

    netstat / ss / lsof / fuser

    「服务器的哪些端口号被占用了?」

    \n

    iptables

    配置防火墙和 NAT

    \n

    nftables

    新版 iptables

    \n

    hping3

    TCP/IP 数据包组装/分析工具

    \n

    traceroute / mtr

    「数据包到达服务器的路径是什么?」

    \n

    tcptraceroute

    使用 TCP 包代替 ICMP 包的 traceroute 命令

    \n
    \n

    现代网络广泛使用防火墙,导致传统路由跟踪工具发出的(ICMP应答(ICMP echo)或UDP)数据包都被过滤掉了,所以无法进行完整的路由跟踪。尽管如此,许多情况下,防火墙会准许TCP数据包通过防火墙到达指定端口,这些端口是主机内防火墙背后的一些程序和外界连接用的。通过发送TCP SYN数据包来代替UDP或者ICMP应答数据包,tcptraceroute可以穿透大多数防火墙。

    \n
    \n

    ethtool

    管理物理以太网连接和网卡

    \n

    iw / iwconfig

    管理无线网络设备的配置工具

    \n

    sysctl

    配置 Linux 内核的网络栈

    \n

    openssl

    用 SSL 证书做任何事

    \n

    stunnel

    为不安全的服务器做一个SSL代理

    \n

    iptraf / nethogs / iftop / ntop

    查看什么在占用带宽

    \n

    ab / nload / iperf

    基准测试工具

    \n

    python -m SimpleHTTPServer

    搭建当前目录下的文件服务器

    \n

    ipcalc

    IP 地址计算器,比如查看 13.21.2.3/15 是什么意思

    \n
    ~ ➜ ipcalc 13.21.2.3/15

    Address: 13.21.2.3 00001101.0001010 1.00000010.00000011
    Netmask: 255.254.0.0 = 15 11111111.1111111 0.00000000.00000000
    Wildcard: 0.1.255.255 00000000.0000000 1.11111111.11111111
    =>
    Network: 13.20.0.0/15 00001101.0001010 0.00000000.00000000
    HostMin: 13.20.0.1 00001101.0001010 0.00000000.00000001
    HostMax: 13.21.255.254 00001101.0001010 1.11111111.11111110
    Broadcast: 13.21.255.255 00001101.0001010 1.11111111.11111111
    Hosts/Net: 131070 Class A
    \n

    nsenter

    进入一个容器进程的网络命名空间

    \n"},{"title":"Linux 笔记","url":"/2017/linux-note/","content":"

    添加用户

    在添加用户时,最好用 adduser,虽然 adduseruseradd 这两个命令在其他发行版的 Linux 系统下一样,但是在 Ubuntu 下是有区别的:adduser 会自动创建用户的 home 目录,并且创建用户同名的组,而 useradd 不会。

    \n

    如果不小心将用户 home 目录删除了,可以使用下边的方法来重建:

    \n
    sudo mkdir /home/user   # 这里的 /home/user 里的 user 最好改成跟你原来用户名一样 
    sudo chown -R user:user /home/user # 这里的 user:user 要改成你之前的“用户名:用户组”的格式
    sudo chmod -R 755 /home/user # 这里权限给 755
    \n

    755 是同组的还有别的组的用户可以查看并且可以执行的。如果不想同组的和别的组的用户查看,可以把权限设置为 700。

    \n

    赋予 sudo 权限

    新建用户后可能还需要给用户添加 sudo 权限,有两种方法:

    \n
      \n
    1. sudo usermod -aG sudo username
    2. \n
    3. 通过修改 /etc/sudoers
    4. \n
    \n

    ssh 免密登录

    将自己电脑上的公钥内容插入到主机用户 home 目录下的 .ssh/authorized_keys 中,通常新建的用户没有这个目录文件,需要手动创建一下。

    \n

    如果本地没有生成过公钥和私钥,或者想生成新的,可使用 ssh-keygen

    \n

    运行上面的命令后,系统会出现一系列提示,可以一路回车。特别说明,其中有一个问题是,要不要对私钥设置口令(passphrase),如果担心私钥的安全,可以设置一个。运行结束以后,会在 ~/.ssh/ 目录下新生成两个文件:id_rsa.pubid_rsa,前者是公钥,后者是私钥。

    \n

    ubuntu 安装 zsh

    查看默认安装了哪些 shell

    jiapan@ubuntu:~$ cat /etc/shells
    # /etc/shells: valid login shells
    /bin/sh
    /bin/dash
    /bin/bash
    /bin/rbash
    /usr/bin/tmux
    /usr/bin/screen
    \n

    当前正在运行的是哪个 shell

    echo $SHELL/bin/bash
    \n

    安装 zsh、git 和 wget

    sudo apt-get install zsh git wget

    wget --no-check-certificate https://github.com/robbyrussell/oh-my-zsh/raw/master/tools/install.sh -O - | sh

    chsh -s /bin/zsh # 替换 bash 为 zsh
    \n

    Ubuntu 下安装官方 JDK

    sudo add-apt-repository ppa:webupd8team/java  # 添加仓库源
    sudo apt-get update # 更新软件包列表
    sudo apt-get install oracle-java8-installer
    \n

    安装过程中需要接受协议,选择 Yes

    \n

    查看 Java 版本: java -version (我每次都输成 --version)

    \n

    查看修改时区

      \n
    1. 查看当前时区
    2. \n
    \n

    date -R

    \n
      \n
    1. 修改时区
    2. \n
    \n

    tzselect

    \n
      \n
    1. 赋值相应时区文件,替换系统时区文件
    2. \n
    \n

    cp /usr/share/zoneinfo/Asia/Shanghai /etc/localtime

    \n

    telent 退出

    Control + ]

    quit
    \n

    调整 ssh 登录时的提示信息

    修改 /etc/update-motd.d/ 下的几个文件就行了。

    \n

    scp 拷贝整个目录

    scp -r ~/local_dir user@host.com:/var/www/html/target_dir
    \n

    查看 CUP 信息

    # 总核数 = 物理CPU个数 X 每颗物理CPU的核数 
    # 总逻辑CPU数 = 物理CPU个数 X 每颗物理CPU的核数 X 超线程数

    # 查看物理CPU个数
    cat /proc/cpuinfo| grep "physical id"| sort| uniq| wc -l

    # 查看每个物理CPU中core的个数(即核数)
    cat /proc/cpuinfo| grep "cpu cores"| uniq

    # 查看逻辑CPU的个数
    cat /proc/cpuinfo| grep "processor"| wc -l
    \n"},{"title":"清单体与愉悦感的巧合","url":"/2022/list-or-pleasure/","content":"

    今天在读《掌控习惯》和听得到拆解《清单革命》这本书的时候,发现这两本书使用了不同的视角来解释同一个现象,而且最终将达成效果的原因各自归功于自己要介绍的方案,有点公说公有理婆说婆有理的感觉,很有意思(也很巧合,同一天读到同一个case)。

    \n

    这两本书都介绍了巴基斯坦卡拉奇这里的平民窟,由于卫生条件差导致死亡率高的问题,解决方法是培养那里的人使用香皂的习惯。

    \n

    《掌控习惯》认为能培养起他们习惯的原因是给他们使用的香皂是「舒肤佳」这种高品质香皂,因为使用起来会产生大量泡沫、洗完手后有香味,给使用者带来极大的愉悦感,所以人们就逐渐养成了使用香皂的习惯。

    \n

    而在《清单革命》这本书中,作者认为是专家给什么时候使用肥皂列了个包含6条内容的清单,人们只要照着做就可以了,由于清单体的有效性所以培养起了人们使用肥皂的习惯。

    \n

    我觉得《清单革命》介绍的方法和《掌控习惯》的第三个原则:「让它简便易行」也是一个意思。

    \n"},{"title":"负载均衡方案介绍","url":"/2022/load-balancing/","content":"

    本文只讨论请求进入数据中心后的负载均衡方案,DNS 负载均衡不在讨论范围内。

    \n

    负载均衡(Load Balancing)定义:调度后方的多台机器,以统一的接口对外提供服务,承担此职责的技术组件。

    \n

    总体来说负载均衡只有两种:

    \n
      \n
    • 四层负载均衡
    • \n
    • 七层负载均衡
    • \n
    \n

    四层负载均衡的优势是性能高,七层负载均衡的优势是功能强。

    \n

    “四层”的来历:“四层负载均衡”其实是多种均衡器工作模式的统称,“四层”的意思是说这些工作模式的共同特点是维持着同一个 TCP 连接,而不是说它只工作在第四层,如:

    \n
      \n
    • 通过改写 MAC 实现的负载均衡(又叫数据链路层负载)工作在二层
    • \n
    • 通过改写 IP 实现的负载均衡(又叫网络层负载均衡)工作在三层
    • \n
    \n

    出于习惯和方便,现在几乎所有的资料都把它们统称为四层负载均衡。

    \n

    如果在某些资料上看见“二层负载均衡”、“三层负载均衡”的表述,描述就是它们工作的层次。

    \n

    对于一些大的网站,一般会采用 DNS+四层负载+七层负载的方式进行多层次负载均衡。

    \n

    四层负载均衡

    数据链路层负载均衡

    数据链路层负载均衡所做的工作,是修改请求的数据帧中的 MAC 目标地址,让用户原本是发送给负载均衡器的请求的数据帧,被二层交换机根据新的 MAC 目标地址转发到服务器集群中对应的服务器的网卡上,这样真实服务器就获得了一个原本目标并不是发送给它的数据帧。

    \n

    负载均衡服务器和集群内的真实服务器配置相同的虚拟 IP 地址(Virtual IP Address,VIP),也就是说,在网络通信的 IP 层面,负载均衡服务器变更 MAC 地址的操作是透明的,不影响 TCP/IP 的通信连接。所以真实的搜索服务器处理完搜索请求,发送应答响应的时候,就会直接发送回请求的客户端,不会再经过负载均衡服务器,避免负载均衡器网卡带宽成为瓶颈,因此数据链路层的负载均衡效率是相当高的。

    \n

    \"Pasted

    \n

    只有请求经过负载均衡器,而服务的响应无须从负载均衡器原路返回的工作模式,整个请求、转发、响应的链路形成一个“三角关系”,所以这种负载均衡模式也常被很形象地称为 “三角传输模式”(Direct Server Return,DSR),也有叫“单臂模式”(Single Legged Mode)或者“直接路由”(Direct Routing)。

    \n

    二层负载均衡器直接改写目标 MAC 地址的工作原理决定了它与真实的服务器的通信必须是二层可达的,通俗地说就是必须位于同一个子网当中,无法跨 VLAN。

    \n

    数据链路层负载均衡最适合用来做数据中心的第一级均衡设备,用来连接其他的下级负载均衡器。

    \n

    网络层负载均衡

    我们可以沿用与二层改写 MAC 地址相似的思路,通过改变数据包里面的 IP 地址来实现数据包的转发。

    \n

    有两种常见的修改方式。

    \n

    IP 隧道

    保持原来的数据包不变,新创建一个数据包,把原来数据包的 Headers 和 Payload 整体作为另一个新的数据包的 Payload,在这个新数据包的 Headers 中写入真实服务器的 IP 作为目标地址,然后把它发送出去。

    \n

    设计者给这种“套娃式”的传输起名叫做“IP 隧道”(IP Tunnel)传输。

    \n

    IP 隧道的转发模式仍然具备三角传输的特性,即负载均衡器转发来的请求,可以由真实服务器去直接应答,无须在经过均衡器原路返回。

    \n

    IP 隧道工作在网络层,所以可以跨越 VLAN,因此摆脱了直接路由模式中网络侧的约束。

    \n

    \"Pasted
    IP 隧道的缺点:

    \n
      \n
    1. 要求真实服务器必须支持“IP 隧道协议)”(IP Encapsulation),就是它得学会自己拆包扔掉一层 Headers(现在几乎所有的 Linux 系统都支持 IP 隧道协议)。
    2. \n
    3. 这种模式仍必须通过专门的配置,必须保证所有的真实服务器与均衡器有着相同的虚拟 IP 地址,因为回复该数据包时,需要使用这个虚拟 IP 作为响应数据包的源地址,这样客户端收到这个数据包时才能正确解析。
    4. \n
    \n

    NAT

    NAT(Network Address Translation) 模式通过改变目标数据包:直接把数据包 Headers 中的目标地址改掉,修改后原本由用户发给均衡器的数据包,也会被三层交换机转发送到真实服务器的网卡上。

    \n

    NAT 模式需要让应答流量先回到负载均衡,由负载均衡把应答包的源 IP 改回自己的 IP,再发给客户端,这样才能保证客户端与真实服务器之间的正常通信。在流量压力比较大的时候,NAT 模式的负载均衡会带来较大的性能损失,比起直接路由和 IP 隧道模式,甚至会出现数量级上的下降,此时整个系统的瓶颈很容易就出现在负载均衡器上。

    \n

    \"Pasted

    \n

    七层负载均衡

    应用层负载均衡

    工作在四层之后的负载均衡模式就无法再进行转发了,只能进行代理,此时真实服务器、负载均衡器、客户端三者之间由两条独立的 TCP 通道来维持通信。

    \n

    \"Pasted
    我们先对代理做个简单介绍,根据“哪一方能感知到”的原则,可以分为“正向代理”、“反向代理”和“透明代理”三类。

    \n
      \n
    • 正向代理就是我们通常简称的代理,指在客户端设置的、代表客户端与服务器通信的代理服务,它是客户端可知,而对服务器透明的。
    • \n
    • 反向代理是指在设置在服务器这一侧,代表真实服务器来与客户端通信的代理服务,此时它对客户端来说是透明的。
    • \n
    • 透明代理是指对双方都透明的,配置在网络中间设备上的代理服务,譬如,架设在路由器上的透明翻墙代理。
    • \n
    \n

    七层负载均衡器它就属于反向代理中的一种。

    \n

    言归正传,七层均衡器工作在应用层,可以感知应用层通信的具体内容,往往能够做出更明智的决策,玩出更多的花样来。

    \n

    列举了一些七层代理可以实现的功能:

    \n
      \n
    • CDN 可以做的缓存方面的工作,如:静态资源缓存、协议升级、安全防护、访问控制
    • \n
    • 智能路由
    • \n
    • 抵御安全攻击
    • \n
    • 微服务链路治理
    • \n
    \n

    参考:

    \n\n"},{"title":"常见逻辑漏洞","url":"/2021/logic-loopholes/","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
    • 举例:(上司对下属):“别跟我说工资太低。我像你这么大的时候,一个星期才赚100美元。”
    • \n
    \n

    强求不相关的目标或功能

      \n
    • 说明:当我们因为某个规定或计划不能满足某个不相关的目标而拒绝接受它时,我们就在犯这种逻辑错误。
    • \n
    • 举例:皮特:“你真的以为学习逻辑就能解决这个世界的问题吗?”
    • \n
    \n

    蒂凡尼:“应该不能。”

    \n

    皮特:“那我们干吗浪费时间学它呢?”

    \n

    随心所欲

      \n
    • 说明:当我们因为自己极其希望某件事是真的(或假的)就去这么假定时,我们就时在犯「随心所欲」的逻辑错误。
    • \n
    • 举例:不管我们的球队之前表现怎么样,这次我们都会在第一轮比赛中打败卫冕冠军。我们的队员都很有信心,铆足了劲儿要大获全胜。
    • \n
    \n

    倚老卖老

      \n
    • 说明:蒂姆,去安纳波利斯这件事你别太当真了!你们家祖祖辈辈——包括你的父亲、兄弟、祖父、叔父等等——一直都是当兵的,而且以后也会一直留在部队。所以小伙子,你的未来在西点军校!
    • \n
    \n

    诉诸公众观点

      \n
    • 说明:当我们因为大家都认同某个观点而去接受或支持它们时,就是在犯这个逻辑错误。
    • \n
    • 举例:我要给税法修正案投赞成票。根据最近一项民意测验的结果,在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

    学生乙:“是的,可我曾祖父每天一包烟,现在都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
    • 举例:在莎士比亚的时代,每四个人中就有一个人不喜欢莎士比亚的戏剧。
    • \n
    \n

    推理过程中的逻辑错误

    断定结果的错误

      \n
    • 说明:我们不能因为『当 A 成立时 B 就成立』,就断定『当 B 成立时 A 就成立』。也称为「转化错误」
    • \n
    • 举例:每次去度假时,我都会感觉很放松。所以当我感觉很放松时,我就一定是在度假。
    • \n
    \n

    否定前项

      \n
    • 说明:我们不能因为『只要 A 成立,B就成里』,就断定『只要 A 不成立,B 就不成立』
    • \n
    • 举例:每次一下雨,地面就变湿。昨夜没下雨,所以地面不可能变湿。
    • \n
    \n"},{"title":"2022年第一次彻夜失眠","url":"/2022/lose-sleep-in-2022/","content":"

    现在时间是2022年02月05日凌晨5点10分,春节假期的倒数第二天,我经历了新年的第一次彻夜失眠。2点半的时候吃了一粒安眠药但到现在还是没睡着,索性就不睡了。

    \n

    我基本上每周会有一次小失眠(差不多睡3、4小时),每几个月有一次大失眠。

    \n

    我的失眠好像有规律,又好像没有过滤,昨晚睡得确实比较晚,差不多0点才上床,躺下后感觉心口疼,一直辗转反侧到现在。

    \n

    上床晚的一个原因是晚上的时候发现了一个有趣的功能,QSpace 可以连接各个网盘,我把每个网盘进行了授权,并尝试了一下文件上传和下载。

    \n

    另一个原因是白天的时候打开了几个页面,给自己定下了学习目标准备今天学完,但是由于拖延只进行了一半,到了晚上有些焦虑。

    \n

    说到失眠有规律,是因为有一部分时候的失眠是因为白天喝了咖啡,但我自己总觉得跟喝咖啡关系不大,因为我几乎不会在下午喝咖啡。

    \n

    昨天上午我喝了一个大杯美食,下午又喝了两杯啤酒,我自己本身有酒精过敏,但是为了消遣没事又想和两口🤷🏻‍♂️

    \n

    前段时间看到一个人提到的「妈妈法则」,我也准备给自己制定几项,虽然有些是我暗自里回尽量去做的,但终究没有落到纸面上,这次索性写下来,起到监督自己的目的:

    \n
    \n

    妈妈法则:你长大成人,背井离乡,没人再盯着你刷牙,洗衣服,完成作业,所以你需要成为自己的妈妈,制定一些规则,和自己约法三章。
    它们是一些简单,有效的规则,让你处理好自己的生活,健康,甚至情绪。

    \n
    \n
      \n
    • 晚上10:30前上床
    • \n
    • 绝不再刷短视频(快手、抖音)
    • \n
    • 每日游戏时间绝不超过30分钟
    • \n
    • 绝不带手机进卧室
    • \n
    • 少喝咖啡,并且绝不在午后喝咖啡
    • \n
    • 绝不再沾酒
    • \n
    • 每周至少一次5公里慢跑
    • \n
    • 每天至少2小时阅读
    • \n
    \n

    今年也没做什么总结,生活上平淡无奇,平日里读了40多本书,公司里给了个优秀员工。

    \n

    2022年,我想把 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

    2018.06.18 - 2018.06.24

      \n
    • 星期一:77.4
    • \n
    • 星期二:77
    • \n
    • 星期三:77.3
    • \n
    • 星期四:76.5
    • \n
    • 星期五:76.5
    • \n
    • 星期六:76.3
    • \n
    • 星期日:76.5
    • \n
    \n

    2018.06.11 - 2018.06.17

      \n
    • 星期一:79.4
    • \n
    • 星期二:78.2
    • \n
    • 星期三:77.4
    • \n
    • 星期四:77.1
    • \n
    • 星期五:76.7
    • \n
    • 星期六:76.8
    • \n
    • 星期日:77.6
    • \n
    \n

    2018.06.04 - 2018.06.10

      \n
    • 星期一:81.2
    • \n
    • 星期二:79.7
    • \n
    • 星期四:78.5
    • \n
    • 星期五:78.2
    • \n
    • 星期六:78.6
    • \n
    • 星期日:79.3
    • \n
    \n

    2018.05.28 - 2018.06.03

      \n
    • 星期一:80.8
    • \n
    • 星期二:80.2
    • \n
    • 星期三:79.6
    • \n
    • 星期四:79.3
    • \n
    • 星期五:78.9
    • \n
    • 星期六:79.7
    • \n
    \n

    2018.05.21 - 2018.05.27

    \n

    本周计划:80.5

    \n
    \n
      \n
    • 星期一:81.3
    • \n
    • 星期二:80.5
    • \n
    • 星期三:79.9
    • \n
    • 星期四:79.9
    • \n
    • 星期五:79.9
    • \n
    • 星期六:81
    • \n
    \n

    2018.05.14 - 2018.05.20

      \n
    • 星期一:82.6
    • \n
    • 星期三:81.8
    • \n
    • 星期四:81.6
    • \n
    • 星期五:81.5
    • \n
    • 星期日:81.5
    • \n
    \n

    2018.05.07 - 2018.05.13

      \n
    • 星期一:82.9
    • \n
    • 星期二:82.9
    • \n
    • 星期三:82.6
    • \n
    • 星期四:82.8
    • \n
    • 星期五:82.0
    • \n
    • 星期日:82.0
    • \n
    \n

    2018.04.30 - 2018.05.06

      \n
    • 星期二:83.5
    • \n
    • 星期四:83.1
    • \n
    • 星期五:82.7
    • \n
    \n

    2018.04.23 - 2018.04.29

      \n
    • 星期一:85.3
    • \n
    • 星期二:84.4
    • \n
    • 星期三:83.4
    • \n
    • 星期四:83.4
    • \n
    • 星期五:83.5
    • \n
    • 星期六:84.1
    • \n
    \n

    2018.04.16 - 2018.04.22

      \n
    • 星期一:85.9
    • \n
    • 星期二:85.9
    • \n
    • 星期三:84.7
    • \n
    • 星期四:84.4
    • \n
    • 星期五:83.2
    • \n
    \n

    2018.04.09 - 2018.04.15

    \n

    本周计划:85.0

    \n
    \n
      \n
    • 星期一:85.6
    • \n
    • 星期二:85.4
    • \n
    • 星期三:85.7
    • \n
    • 星期五:85.4
    • \n
    • 星期六:84.9
    • \n
    • 星期日:85.2
    • \n
    \n

    2018.04.02 - 2018.04.08

    \n

    本周计划:85.4(未完成。本周过了个清明,没有好好控制,体重也没称)

    \n
    \n
      \n
    • 星期一:86.2
    • \n
    • 星期二:85.1(今晚团建吃自助,估计明天会爆炸)
    • \n
    • 星期五:86.1
    • \n
    \n

    2018.03.26 - 2018.04.01

    \n

    本周计划:86.0(未达成)

    \n
    \n
      \n
    • 星期一:87.0
    • \n
    • 星期二:86.9
        \n
      • 练臂
      • \n
      \n
    • \n
    • 星期三:86.5
        \n
      • 练背
      • \n
      \n
    • \n
    • 星期四:86.0
        \n
      • 练腿
      • \n
      \n
    • \n
    • 星期五:85.5
    • \n
    • 星期六:86.3
    • \n
    • 星期日:86.7(周末两天太放纵了)
    • \n
    \n

    2018.03.19 - 2018.03.25

    \n

    本周计划:87.2(达成)

    \n
    \n
      \n
    • 星期一:87.8(上周末搬家,没有好好运动,吃的东西也没有控制特别注意)
    • \n
    • 星期二:87.8
    • \n
    • 星期三:87.6
    • \n
    • 星期四:87.5
    • \n
    • 星期五:86.7
        \n
      • 卧推:10 * 4
      • \n
      • 哑铃卧推:10 * 2
      • \n
      • 器械推胸:10 * 4
      • \n
      • 跑步:40min
      • \n
      \n
    • \n
    • 星期六:86.5
        \n
      • 硬拉:10 * 6
      • \n
      • 哑铃划船:10 * 6
      • \n
      • 水平划船:10 * 6
      • \n
      • 高位下拉:10 * 6
      • \n
      • 椭圆机:40min
      • \n
      \n
    • \n
    • 星期日:86.4
        \n
      • 腿屈伸 10 * 6
      • \n
      • 哈克深蹲 10 * 6
      • \n
      • 私教体验课 60 min
      • \n
      • 椭圆机:45min
      • \n
      \n
    • \n
    \n

    2018.03.12 - 2018.03.18

    \n

    本周计划:87.8(达成)

    \n
    \n
      \n
    • 星期二:88.3
    • \n
    • 星期三:87.8
    • \n
    • 星期四:87.7
    • \n
    • 星期五:87.7
    • \n
    • 星期六:87.7
    • \n
    • 星期日:87.7(WTF???连续4天87.7🙃)
    • \n
    \n

    2018.03.05 - 2018.03.11

    \n

    本周目标:88.5(达成)

    \n
    \n
      \n
    • 星期一:90.1
    • \n
    • 星期二:89.8
    • \n
    • 星期三:89.8
    • \n
    • 星期四:89.1
    • \n
    • 星期五:88.7
    • \n
    • 星期六:88.9(一顿羊蝎子回到解放前)
    • \n
    • 星期日:88.4(💪🏻)
    • \n
    \n"},{"title":"如果我可以娶红楼梦中的一位女子","url":"/2022/marry-with-hongloumeng/","content":"

    今天脑补一下,如果我能够娶红楼梦中一位女子为妻,我会选谁。用正排的方法有点困难,尝试用排除法逐个过滤掉我无法接受性格的人物。另外,红楼梦中所有女子范围太大,我从中选取一些有鲜明性格的人出来。

    \n

    \"20220704125543.png\"

    \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
    \n

    我们一家蜗居在一个不到 70 平的小房子里,平时我爸妈也都在

    \n

    我爸喜欢出去玩,而且不喜欢在北京挤着,所以经常离京

    \n

    他在今年年初的时候离京,后来疫情严重就一直没回来

    \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

    可是我现在个人也不成功,相对幸福的生活也没有

    \n

    懦弱而又无力的自己

    \n
    \n

    \n

    照片拍摄于年 5 月,一念在认真的给我扎小辫。

    \n

    一念和黛玉一样容易咳嗽,所以一般很少让她吃凉东西

    \n

    作为我俩的小秘密,也是为了贿赂她

    \n

    每次我带她出去玩都会和她一起吃冰淇淋

    \n

    \n
    \n

    一日不见,甚是想念

    \n
    \n"},{"title":"将博客字体修改为「霞鹜文楷」","url":"/2022/modify-blog-font/","content":"

    \"wenkai-1.png\"

    \n

    之前访问过我博客的人可能会发现我博客的字体变了,这款字体是我前段时间在 Twitter 上看到一个很喜欢的博主推荐的,是一款开源字体,名字叫「霞鹜文楷」,非常适合作中文展示,阅读起来使人感受愉悦。

    \n

    \n

    我也将这款字体改为了我 Drafts 上的默认字体,试用了几天感受确实不错,所以想作为博客字体来使用。因为官方仓库的下载链接只提供了 ttf 格式,我不知道如何应用在 Web 上,所以搁置了几天,今天想再次折腾下看,再次阅读官方文档看到注意事项中写着:

    \n

    \n
    \n

    正应了我前两天说的:我想的事情其他人已经想到了

    \n
    \n

    我打开那个 Issue 提供的另一个项目地址,看到里边提供了好几种安装、使用方式。因为我是在其他人模板的基础上进行修改,所以准备用引入现成 CDN 的方式来做字体修改。

    \n

    我的博客当前使用的是 Next 主题,使用其他主题的也可参考这个方式。

    \n

    编辑博客根目录下的 themes/next/layout/_layout.swig,在 head 中插入如下代码:

    \n
    <link rel=\"stylesheet\" href=\"https://cdn.jsdelivr.net/npm/lxgw-wenkai-lite-webfont@1.0.0/style.css\" />
    <style>
    body,div.post-body,h1,h2,h3,h4 {
    font-family: \"LXGW WenKai LITE\", sans-serif;
    font-size: 108%;
    }
    </style>
    \n

    我对 CSS 不太精通,刚开始只在 style 中填入了 body,发现有些正文部分没有生效,我觉得应该是优先级问题:博客的正文指定了其它字体所以将我这里的设置进行了覆盖。

    \n

    我通过 Chrome 的元素查找定位到了 div 下 的 class="post-body" 为正文部分的筛选器,于是加上了 div.post-body,接下来后发现 h1、h2 这些格式也没生效,于是有逐个进行了添加。这样基本上所有地方都能生效了,有两处我故意没做处理,一处是左上角博客标题另一处是文章标题下方的 meta 区域,这两块我觉得可以保留更正式一些的字体。

    \n

    这个字体有些偏小,于是我还将字号做了稍许放大,也就是 style 中的 font-size: 108%

    \n

    如果你也喜欢这款字体,不妨参考这篇文章也自己尝试修改一下。另外如果你知道如何只在 style 中指定 body 就可以让全局字体生效,请留言告诉我。

    \n"},{"title":"单体仓库与多仓库——两种源码组织模式介绍","url":"/2020/multi-repo-vs-mono-repo/","content":"

    \"\"

    \n
    \n

    我在去年和前年主导了公司两个产品后端的技术选型和整体架构,并分别尝试了两种源码组织模式:多仓库和单体仓库。对两种仓库的利弊也有了很大程度上的感受,基于这个前提对这两种模式做个总结。

    \n
    \n

    阅读本文后你会明白:什么是单体仓库?为什么 Google 采用单体仓库?

    \n

    单体应用和微服务应用

    在介绍单体仓库和多仓库前,先来说说什么叫单体应用和微服务应用。

    \n

    微服务相比单体应用最大的好处是可以独立的开发测试部署和扩展。单体应用一般采用单体仓库,但是微服务的代码仓库该如何组织呢?一定是每个服务一个仓库吗?

    \n

    其实也不一定,针对微服务的代码组织,业界有两种主要的实践,一种是多仓库(multi-repo)也就是每个服务开一个源码仓库,另一种叫单体仓库(mono-repo)所有源码都在同一个仓库中,尽管整个应用采用的微服务架构。

    \n

    \"\"

    \n

    多仓库

    单体仓库和多仓库都是有利有弊的。

    \n

    多仓库的好处是显而易见的:

      \n
    1. 每一个服务都有一个独立的仓库,职责单一。
    2. \n
    3. 代码量和复杂性受控,服务由不同的团队独立维护、边界清晰。
    4. \n
    5. 单个服务也易于自治开发测试部署和扩展,不需要集中管理集中协调。
    6. \n
    \n

    多仓库存在的问题:

      \n
    1. 项目代码不容易规范。每个团队容易各自为政,随意引入依赖,code review 无法集中开展,代码风格各不相同。
    2. \n
    3. 项目集成和部署会比较麻烦。虽然每个项目服务易于集成和部署,但是整个应用集成和部署的时候由于仓库分散就需要集中的管理和协调。
    4. \n
    5. 开发人员缺乏对整个项目的整体认知。开发人员一般只关心自己的服务代码,看不到项目整体,造成缺乏对项目整体架构和业务目标整体性的理解。
    6. \n
    7. 项目间冗余代码多。每个服务一个服务一个仓库,势必造成团队在开发的时候走捷径,不断地重复造轮子而不是去优先重用其他团队开发的代码。
    8. \n
    \n

    单体仓库

    单体仓库可以解决部分上边提到的问题。

    \n

    单体仓库的好处:

      \n
    1. 易于规范代码。所有的代码在一个仓库当中就可以标准化依赖管理,集中开展 code review,规范化代码的风格。
    2. \n
    3. 易于集成和部署。所有的代码在一个仓库里面,配合自动化构建工具,可以做到一键构建、一键部署,一般不需要特别的集中管理和协调。
    4. \n
    5. 易于理解项目整体。开发人员可以把整个项目加载到本地的 IDE 当中,进行 code review,也可以直接在本地部署调试,方便开发人员把握整体的技术架构和业务目标。
    6. \n
    7. 易于重用。所有的代码都在一个仓库中,开发人员开发的时候比较容易发现和重用已有的代码,而不是去重复造轮子,开发人员(通过 IDE 的支持)容易对现有代码进行重构,可以抽取出一些公共的功能进一步提升代码的质量和复用度。
    8. \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

    我把图片保存到手机上,时不时看一下自己的牌有没有和这些规则对应上。因为之前玩过炸金花,所以不一会就了解规则了,但是各种套路和黑话还是不太懂,包括什么时候可以看牌什么时候可以跳过也需要问下其他人,有时候还需要让其他人帮忙算一下钱之类的。

    \n

    这次学习德州扑克是实打实的用钱学习的,大家初金都是 200 块钱,没想到我在新手光环的照耀下不仅没有输钱,最后还赚了 150 多。

    \n

    陆地冲浪板

    因为从家到地铁站、下地铁后再到公司,这两段路程都比较远,从家到地铁可以骑自己的自行车,但是公司那边地铁站下车后通常骑不上车,所以就萌生了用滑板当代步工具的想法。

    \n

    之前网上查了一些资料,一心想学习双翘,找了个家附近的滑板俱乐部,预约了今天的体验课,老师问我的学习目的,我说简单、能代步就可以。教练给我推荐陆地冲浪板(简称陆冲)。对于我不了解的领域,我是很相信专业和权威的,所以听了教练的话体验了一节陆冲课。

    \n

    陆冲适合平时代步,它的轮子比较大,更适合在马路上使用,而且相对来说比较容易入门,学会后还能体验到冲浪的乐趣(这也是它名字的由来)。

    \n

    一节课体验课结束后,感觉很有意思,而且这东西确实看起来容易,到自己滑的时候相当困难,加上陆冲板还可以来回扭动,刚开始上板平衡都很难掌握。为了更深入地学习,我报了 10 节一对一课程,一节 399,还买一块属于自己的板子。板子的话我也一步到位,买的应该是陆冲板中最好的牌子 Caver,虽然价格并不是最高的,但这块板子颜值深得我意,如下图:

    \n



    \n

    这块板长度是 30.5 寸,大概是 6 斤多重,比起其他款式稍微宽一点,所以看起来很大气,售价 2200 软妹币。我还差一套护具没买,训练的时候用的店里的,准备自己从网上买一套,加上头盔大概又是 700 左右的花销。好不容易能有个爱好,该省省该花花吧。

    \n

    肯定有朋友会说我这钱花的不值,陆冲板这么简单靠自学就够了,但我的想法是随着年龄越来越大,我们应该尽可能用更高的效率去学习,不能再花太多时间自己琢磨了,请个教练可以少走弯路,自己跟着视频练习的话根本不知道哪个姿势不对、哪里应该注意,也不知道应该加强练习哪些内容,而且容易受伤。这和软件开发时常用的空间换时间一个道理,我这算是拿钱换时间了,我几年前学习游泳也是报了 10 节私教课,学完后可以用基本标准的姿势蛙泳了,虽然很久没有游泳了,但那些注意事项我还是记得,再游的话很快就能找到感觉。

    \n

    10 节课周六日各上一节,估计一个多月就能学完,再加上平时的练习,到时候我应该就能达到刷街的级别了。

    \n

    作为报课的优惠,教练赠送了我一节课,所以买完板后我又上了正式的第一节课,今天一共学了两个小时,最后,再欣赏一下我的滑板初体验的视频吧。

    \n\n\n\n

    这个视频是教练一手打着绷带,另一手拿着手机,脚下踩着滑板拍出来的,完整视频比较长我剪了其中十来秒出来,可以看出镜头运的很好。我准备最后一节课让教练帮我拍一组酷酷的视频作为结果汇报的材料。

    \n

    P.S. 昨天去检查甲功,今天结果出来指标正常了,喝个酒庆祝下,干杯🍻

    \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

    本指南理论上适用于 IntelliJ 家的所有产品。

    \n
    \n

    首先来介绍下我自己定义的一些快捷键

    左右分屏 (Extend Selection): Alt + w

    写代码时,有时候需要同时打开多份文件,在 IDEA 中有两种分屏方式,一种是上下分,一种是左右分,我觉得上下的方式基本上看不了几行代码,所以我都是使用左右分。默认的快捷键需要用到方向键,但在 HHKB 中使用方向键还需要其他按键组合

    \n

    \"\"

    \n

    代码补全 (Basic): Control + ,

    默认的代码补全快捷键为 Control + Space,但是这个组合被我的 iTerm2 的弹出栏占用了,所以我改为了:Control + ,

    \n

    合并 Git 的修改 (Merge Changes): Alt + m

    Fetch 代码 (Fetch): Alt + f

    git pull (Pull): Shift + Conmmand + p

    因为 push 默认快捷键为 Shift + Command + k,但是 pull 没有默认快捷键,所以我用了这样的组合。

    \n

    然后就是我的一些设置

    关闭代码拖拽功能

    代码拖拽是我非常不喜欢的功能,经常不小心误操作,如下图,去掉勾即可。

    \n

    \"\"

    \n

    代码提示不区分大小写

    默认的代码补全提示是会区分大小写的,比如我们在 Java 文件中输入 stringBuffer IDEA 是不会帮我们提示的,我们需要输入 StringBuffer 才行。

    \n

    如图所示,将选项改为 None 即可。

    \n

    \"\"

    \n

    显示内存使用情况

    对于我这种 8G 内存的 Mac 用户来说,打开这个功能很有必要性,而且点击内存信息展示的那个条可以进行部分的内存回收

    \n

    \"\"

    \n

    优化 Java 注释

    使用 Command + / 快捷键可以对代码进行注释,IDEA 对 Java 代码的单行注释是把斜杠放在本行最开头,这种注释方式非常丑,所以我修改为将斜杠放在代码之前,并且加一个空格。

    \n

    \"\"

    \n

    \"\"

    \n

    小技巧

    点击右下角戴帽子的小人,可以选择不同的检查等级,在编辑大文件的时候,可以暂时将等级改为 None,提高流畅性

    \"\"

    \n
      \n
    • Inspections 为最高等级检查,可以检查单词拼写,语法错误,变量使用,方法之间调用等
    • \n
    • Syntax 可以检查单词拼写,简单语法错误
    • \n
    • None 不设置检查
    • \n
    \n

    点击右下角戴帽子的小人,可以看到有一种叫做 Pover Save Mode(省电模式),开启这个模式后 IDEA 会关掉代码检查和提示等功能,可以将这种模式作为一种「阅读模式」来使用。

    在展示代码的包时,默认会将一些空的包进行折叠,如果更习惯属性结构的话,可以更具下图来修改的方式进行调整:

    \"\"

    \n

    调整后

    \n

    \"\"

    \n

    IDEA 默认情况下会把只有一行的代码进行折叠,我不喜欢这样,所以会关掉这个特性:

    \"\"

    \n"},{"title":"我理想的工作环境","url":"/2021/my-ideal-work-env/","content":"
    \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
    1. 不要内卷,有事做事没事早点下班,晚上下班时间不能晚于8点。
    2. \n
    3. 下午6点后不要拉会。
    4. \n
    5. 没有大小周。
    6. \n
    7. 晚上、周末不要经常性搞聚餐团建之类的(包括团队组织的和个人组织的),一个月不要超过1次。
    8. \n
    9. 没有酒桌文化,不搞日报、日站会等幺蛾子的事情。
    10. \n
    \n

    以上只是我个人站在对生活的态度上列出来的几点,其他更通用一些的考量肯定还包括团队氛围、领导风格、业务方向、公司战略、薪资不能低于业界水平等。

    \n

    目前看来我当前所在的公司以上几条都是满足条件的,据我所知目前市面上绝大多数大厂都无法满足这些点。

    \n

    不知道没进过大厂算不算是一种遗憾,有时候我甚至会因为没有进过大厂而沮丧和焦虑,觉得自己是个 Loser,如果有符合这些条件的大厂能有幸进去体验一下也是可以的。——但是话又说回来,谁规定进过大厂的人生才是完整的?哪里不是围城呢?

    \n

    我也想进一家小而美的公司,最好是 B、C 轮之后,和一群聪明的人在一个细分领域去做一些有挑战的事。

    \n

    大部分人进大厂是因为大厂给的太多了,但我觉得薪资是一个不应该作为决定性决策,我们上班寻求的应该是整个职业生涯利益的最大化,而不仅仅是最近这一份工作利益的最大化,拿上3、5年的高出业界50%的收入也不能解决太多的问题,更何况还搭上了健康和生活乐趣。我说不能解决问题是出于以下考虑:

    \n
      \n
    • 工作的前5-10年,积累技术财富才是更重要的事,而不是为了更多的现金收入。
    • \n
    • 普通程序员很难通过跳槽进入一家大厂来实现财富自由了,大厂所派发的期权和股权暂且不谈价值多少,想拿满就要先卷个三五年。
    • \n
    • 薪资只是工作的附属,工作的真正报酬是成长
    • \n
    \n

    很多人想进大厂的另一个目的是「镀金」,但是所谓的镀金不也是为了未来能进另一个大厂么,但你现在已经到了大厂,为了镀金开始忍耐着在一群人中卷起来,之后跳到另一家继续卷,还是走不出那个圈子。想要走出这个圈子还是要改变自己的想法,用智慧而不是蛮力去解决问题。

    \n
    \n

    如果你所在的大厂没有内卷,比如 Apple、AWS 这种外企大厂,就另当别论了。

    \n
    \n

    当然,我的以上观点可能纯属吃不着葡萄说葡萄酸,没有进过大厂也却是有我自己的原因,因为我从小就讨厌应试教育,在我看来现在的面试也接近应试教育:「面试官清楚自己在问八股文,你也清楚自己在背八股文,你们心照不宣的完成了面试。」我从最开始工作到现在,在面试前没有刷过题(我看面试题的唯一的目的就是用在面试别人上),没有做过刻意的准备,都是看缘分,人家看得上我我就去,看不上就是缘分没到,所以可能也是因为这个原因,通常我在入职后的表现都会超出一些预期,具体表现有这几个:

    \n
      \n
    • 我毕业时去的第一家公司,第二个月就给我加了薪;
    • \n
    • 在上家公司每年的绩效都是 A 或 S;
    • \n
    • 在探探今年上半年也拿到了 S 绩效。
    • \n
    \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 的任务管理工具。我现在差不多每天的工作事项都是靠 Things3 驱动,说的通俗点是靠 Todo 驱动,这有点像我们常说的 deadline 是第一生产力。

    \n

    「君子生非异也,善假於物也。」

    \n
    \n

    下文提到的 Things 皆为 Things3,为了简化称呼我将去掉 3 这个数字。

    \n
    \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 上的价格有些感人。

    \n

    时间目录

    \n

    Things 根据事项要完成的时间分了这些目录:

    \n
      \n
    • 收件箱,临时存放或不用分类的琐碎事项;
    • \n
    • 今天,时间聚焦到今天;
    • \n
    • 计划,也可以理解成日程安排,有具体时间点的事项。
    • \n
    • 随时,随时可抽时间完成的事项以及本周临近截至时间(Deadline)需要优先考虑的事项;
    • \n
    • 某天,选择了「某天」时间标签的事项,可以用来归集不需要时间点约束的未来待办事项;
    • \n
    • 日志簿(完成事项的历史记录)。
    • \n
    \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

    \n

    因为每周三需要和组内的同事一起开周会,这之前需要写好周报,会后我需要合大家的周报,所以我给「提交周报、合周报」也建了一个每周三的循环事件,同时在里边把每个人周报的地址进行了记录:

    \n

    \n

    回顾过往

    职场中我们不免要写周报、月报、年度总结等等,如果自己平时有记录的习惯那还好,如果没有,到写总结的时候一定是大脑空白,仿佛自己失忆了,忘记了自己这段时间忙忙活活干了点啥。

    \n

    如果我们用 Things 将工作事项管理起来,在需要的时候通过「日志簿」来回顾就会很方便。比如现在你问我在一月份做了些什么,我只需要翻到一月份部分,看下我当时的记录知道了。

    \n

    这些内容有些描述过于简单,其他人也许看不懂,但因为每件事情都是我亲自做的,我看下提示就能明白这说的是什么事,由此可见我们并不需要为每个事项做特别详细的说明,能让自己能看明白就够了。人脑存储能力很强,检索能力很弱,需要借助些外力来补足检索能力。

    \n

    \n

    管理我的一切

    不止待办事项,实际上我用 Things 管理了我的方方面面。

    \n
      \n
    • 待学、待读、待听、待看、待买
    • \n
    • 想去的地方
    • \n
    • 突发奇想的点子
    • \n
    • 想写的博客内容
    • \n
    • 阅读收获的金句
    • \n
    • 项目中的可优化项
    • \n
    • 交代给其他人的任务
    • \n
    • 想要尝试的行动
    • \n
    • ……
    • \n
    \n

    Things 支持将事项进行分类管理,上边那些事项我没有计划做的时间点,有些只是为了保存起来,这时候我们可以不设置时间,将它收在我们对应的分类中就可以了。

    \n

    这里借助的是 Things 提供的创建区域和在区域中创建项目的能力。实际上 Things 的这个功能是用来做更大一些的目标管理的,不过我个人将它作为了一个分类功能来使用。

    \n

    \n

    比如,我给自己分了个人和公司两大区域,每个区域中建了一些属于这个区域的分类(也就是项目):

    \n

    \n

    把所有事情都记录下来还有个好处是,在我无所事事时可以看下这些事项中哪个是我现在有心情可以做的,或者读完一本书后下一本要读什么。

    \n

    有待完善的地方

    由于我会有「红点焦虑」的情况,所以想要一个在某个时间点后再出现事项的功能,有些事必须过了某个时间点才能去做。不过我觉得 Things 没有支持这个功能也有它的考虑,「今天」就应该把计划在今天做的事情都列出来,让我们可以提前规划。

    \n

    Things 提供了一个退而求其次的方案,将晚上做的事情通过分割线放到下方,在事项上点击右键就可以操作或者使用快捷键 Command + E

    \n

    \n

    \n

    配合日历订阅使用

    我们再看一眼上边那张图,在「今天」下方显示今日天气状况和今天的节日,这是我在系统的日历中订阅了两个事件,Things 可以将系统日历中的事件列在我们自己事项的上边,就可以实现这样的效果。

    \n

    中国节假日事件:

    \n

    https://weather-in-calendar.com/cal/weather-cal.php?city=%E5%8C%97%E4%BA%AC%E5%B8%82&units=metric&temperature=day

    \n

    北京天气事件:

    \n

    https://weather-in-calendar.com/cal/weather-cal.php?city=%E5%8C%97%E4%BA%AC%E5%B8%82&units=metric&temperature=day

    \n

    天气事件大家可以根据自己所在城市,在这个网站生成订阅链接。

    \n

    最后

    Things 还有一些功能我自己也没有深入探索过,比如标签、截止日期,大家可以自行探索。

    \n

    通过这篇文章我梳理了一下自己使用 Things 的习惯,如果大家觉得可行、适用,可以尝试将自己的事项管理起来,做一个「靠谱」的人。

    \n

    如果你发现文章中有观点介绍有误或者不明确的地方欢迎留言讨论。

    \n"},{"title":"mycli fzf thefuck","url":"/2017/mycli-fzf-thefuck/","content":"

    今天装了3个命令行下的神器,分别是 mycli fzf thefuck,都是通过 Homebrew 装的。

    \n

    thefuck 装完后在 .zshrc 的 plugin 中配上了插件,这样的话用起来就更方便了,当输错命令或者需要 root 权限却没加 sudo 时,只需要双击 esc 就可以了。

    \n

    \"\"

    \n

    mycli 是一个支持语法高亮和命令补全 mysql 客户端,类似于 ipython。安装过程比较长,主要是中间安装 Pyhton 2.7.13 占用了很长时间。装完后直接 mycli -uroot 就进入数据库的交互状态了。有一个地方不太习惯,没执行一个命令出来结果后,需要再按一下 q 才能返回交互状态。

    \n

    \"\"

    \n

    fzf 是命令行下模糊搜索工具。通过 brew install fzf 安装完后,还需要执行 /usr/local/opt/fzf/install 安装 shell 扩展,之后 Control+r 时出来的就不是之前那种很简单的历史命令搜索结果了,而是交互性很棒的结果。输入 kill -9 + <TAB> 能通过模糊搜索的方式搜到需要杀掉的进程,再也不用先 ps -ef | grep xxx 找到对应的进程然后在执行 kill 或者 通过管道 + xargs 的方式来杀进程了。

    \n

    \"\"

    \n

    fzf 还有几种用法我感觉没多少用:

    \n
    cd **<TAB>
    vim **<TAB>
    ssh **<TAB>
    telnet **<TAB>
    unset **<TAB>
    export **<TAB>
    unalias **<TAB>
    \n"},{"title":"Neo4j 统计关系数量时的技巧","url":"/2018/neo4j-count-relationships-skill/","content":"

    今天产品经理给了我一个需求,让我统计下我们数据中的某些类型关系的数量,因为涉及到保密,我就假设他让我统计的关系名称为 FRIENDFATHER,其中 FRIEND 在创建时没有指定方向,FATHER 创建时有方向。

    \n

    当统计这两种关系的数量时,我的建议是 Cypher 语句中不要带有节点标签,或者只在一端带标签,因为在我的测试中,两端都带有节点标签时,查询会超时(因为我们的数据量确实比较大):

    \n

    不建议的写法:

    MATCH (:Person)-[r:FATHER]->(:Person) return COUNT(r);
    \n

    建议的写法:

    MATCH ()-[r:FATHER]->() return COUNT(r);
    or
    MATCH (:Person)-[r:FATHER]->() return COUNT(r);
    \n

    在查询 FRIEND 这种在创建时没有指定方向的关系是,也需要用带方向的查询语句,因为 Neo4j 实际存储时是带有方向的,详情见:http://blog.csdn.net/hwz2311245/article/details/54602706),在不指定方向的情况下,我这里的查询也是超时:

    \n

    不建议的写法:

    MATCH ()-[r:FRIEND]-() return COUNT(r);
    \n

    建议的写法

    MATCH ()-[r:FRIEND]->() return COUNT(r);
    \n","tags":["neo4j"]},{"title":"Neo4j 导入动态类型关系","url":"/2018/neo4j-import-dynamic-relationship-type/","content":"

    今天在构建一批新的关系时,需要用 LOAD CSV 批量导入一些关系数据进去,但是这次的关系类型并不是固定的,而是在文件中指定的,CSV 文件格式如下:

    \n
    AWI9o1sbC5n_pY7tOUdL|AWI96tqyetLN4cQh5GnC|EMERGENCY|紧急联络人
    AWI85duqetLN4cQhYTVc|AWI-DASVC5n_pY7tH2i5|EMERGENCY|紧急联络人
    AWI85duqetLN4cQhYTVc|AWI-DASVC5n_pY7tH2i5|RELATIVE|Spouse
    AWI9-Vm7etLN4cQhC6gp|AWI9lGsNetLN4cQhK_8U|EMERGENCY|紧急联络人
    AWI8pkY9etLN4cQhqdrQ|AWI8dIx9C5n_pY7tBKBJ|EMERGENCY|紧急联络人
    AWI8pkY9etLN4cQhqdrQ|AWI8dIx9C5n_pY7tBKBJ|OTHER|Other
    AWI-ZirYC5n_pY7tIn7b|AWI-d3TaC5n_pY7tUjVr|EMERGENCY|紧急联络人
    AWI-ZirYC5n_pY7tIn7b|AWI-d3TaC5n_pY7tUjVr|RELATIVE|Cousin
    AWI8UPZ3etLN4cQhsFoT|AWI9ngEwetLN4cQhS4hI|EMERGENCY|紧急联络人
    AWI-CJ7jetLN4cQhNwgt|AWI-UkO6etLN4cQhDYhR|EMERGENCY|紧急联络人
    \n

    第一列和第二列分别两个 Person 节点的 UUID,第三列是关系类型(type),第四列是关系的名称(name)。

    \n

    我刚开始这样写的导入语句:

    \n
    USING PERIODIC COMMIT 10000
    LOAD CSV FROM 'file:////relation_all.csv' AS line FIELDTERMINATOR '|'
    MATCH (p1:Person {uuid: line[0]})
    MATCH (p2:Person {uuid: line[1]})
    MERGE (p1)-[:line[2] {name: line[3]}]->(p2)
    \n

    但是发现无法执行,由错误信息可知(下图所示),Neo4j 原始的 LOAD CSV 语句是不支持动态创建关系类型的。

    \n

    \"\"

    \n

    我之前在介绍 Neo4j 冷启动预热缓存 时介绍过一个插件:APOC,这个插件功能非常强大,比如提供了很多好用的路径算法和强大的函数,之后有机会的话会慢慢介绍,今天介绍一下他的动态创建关系的函数 apoc.create.relationship,函数说明如下:

    \n

    apoc.create.relationship(person1,'KNOWS',{key:value,…​}, person2) create relationship with dynamic rel-type

    \n

    所以我的导入语句只需要改成:

    \n
    USING PERIODIC COMMIT 10000
    LOAD CSV FROM 'file:////relation_all.csv' AS line FIELDTERMINATOR '|'
    MATCH (p1:Person {uuid: line[0]})
    MATCH (p2:Person {uuid: line[1]})
    WITH p1, p2, line
    CALL apoc.create.relationship(p1, line[2], {name: line[3]}, p2) YIELD rel
    RETURN rel
    \n

    就完事大吉了。

    \n

    但是考虑到这个 CSV 文件中的关系可能存在重复,所以我通过文档找到了另一个函数:

    \n

    apoc.merge.relationship(startNode, relType, {key:value, …​}, {key:value, …​}, endNode) - merge relationship with dynamic type

    \n

    这个函数中需要传两组 key-value,第一组是用来判断关系是否重复的,第二组是一些其他属性。

    \n

    最终的导入语句如下:

    \n
    USING PERIODIC COMMIT 10000
    LOAD CSV FROM 'file:////relation_all.csv' AS line FIELDTERMINATOR '|'
    MATCH (p1:Person {uuid: line[0]})
    MATCH (p2:Person {uuid: line[1]})
    WITH p1, p2, line
    CALL apoc.merge.relationship(p1, line[2], {name: line[3]}, {}, p2) YIELD rel
    RETURN rel
    \n","tags":["neo4j"]},{"title":"Neo4j 入门教程 - 关于 Neo4j","url":"/2018/neo4j-tutorial-about/","content":"
    \n

    Neo4j 是世界上最流行的图数据库管理系统(DBMS),同样是最流行的 NoSQL 数据库之一。

    \n
    \n

    \"\"

    \n

    Neo4j 是什么

    Neo4j 以图的方式存储和展示数据,数据由节点和节点间的关系来表示。

    \n

    Neo4j 数据库(和任何图数据库一样)与关系型数据库(如:MS Access、SQL Server、MySQL)有很大不同。关系数据库使用表、行和列来存储数据,他们以表格的形式展示数据。

    \n

    Neo4j 不使用表、行或者列存储或展示数据。

    \n

    Neo4j 用来做什么

    Neo4j 非常适合存储有很多关联关系的数据。这是图数据库可以发挥巨大作用的地方。实际上,像 Neo4j 这样的图数据库在处理关系数据方面要优于关系型数据库。

    \n

    图模型通常不需要预定义结构,你不需要在加载数据前创建数据库结构(就像在关系型数据库中那样)。在 Neo4j 中,数据就是结构。Neo4j 是一个 「结构可选」的 DBMS。

    \n

    然而 Neo4j 能更好地处理关系数据的主要原因在于它允许你创建关系。Neo4j 是围绕关系而建立的。它不需要设置主键/外键约束来预先确定哪些字段或者哪些数据间有关系。在 Neo4j 中,只要在你需要时添加任何点之间的关系就行了。

    \n

    所以,这使得 Neo4j 非常合适社交网络应用,比如 Facebook、Twitter 等。同时,Neo4j 还可以应用于很多其他领域。

    \n

    以下是一些 Neo4j 主要应用领域:

    \n
      \n
    • 社交网络
    • \n
    • 实时产品推荐
    • \n
    • 网络架构图
    • \n
    • 欺诈识别
    • \n
    • 访问管理
    • \n
    • 基于图的数字资产搜索
    • \n
    • 主数据管理
    • \n
    \n","tags":["neo4j"]},{"title":"Neo4j 入门教程 - Neo4j 浏览器","url":"/2018/neo4j-tutorial-browser/","content":"
    \n

    Neo4j 浏览器是一个可以通过 Web 浏览器运行的图形用户界面(GUI)

    \n
    \n
    \n

    Neo4j 浏览器可以用来添加数据、运行查询语句、创建关系等等。它还提供了一种简单的方式来可视化数据库中的数据。

    \n

    概览

    下图是 Neo4j 浏览器概览

    \n

    \"Neo4j

    \n

    编辑器

    这里是你输入查询语句和命令的地方,比如创建或检索数据。你可以随时通过输入 :help 并按下回车键(或者点击编辑器右侧的「运行」箭头)来获取帮助。

    \n

    这里是展示查询结果的地方,每个结果有自己的框架,新的结果框会出现在前一个结果框的上边。所以如果需要的话,你可以向下滚动并查看之前的查询结果。你可以随时使用 :clear 命令清空这个

    \n

    标签、节点、关系

    这些代表了数据库中的数据。点击顶部的任意图标都会在框架的底部显示可选信息。

    \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
    • 指定该属性必须包含唯一值(比如两个 Artist 节点不允许有相同值的 name 属性。)
    • \n
    \n

    属性存在约束

    \n
      \n
    • 确保具有特定标签的节点或具有特定类型的关系都存在某个属性(属性存在约束只在 Neo4j 企业版中可用)
    • \n
    \n

    创建唯一约束

    在 Neo4j 中创建唯一约束需要使用 CREATE CONSTRAINT ON 语句,像下边这样:

    \n
    CREATE CONSTRAINT ON (a:Artist) ASSERT a.name IS UNIQUE
    \n

    在上边的例子中,我们为 Artist 标签的所有节点的 name 属性创建了唯一约束。

    \n

    我们的语句执行成功后,展示如下信息:

    \n

    \"\"

    \n
    \n

    当你创建一个唯一约束时,Neo4j 将同时创建一个索引。Cypher 将使用该索引进行查询,就像使用其他索引一样。
    因此不需要单独创建索引了,如果你尝试在已经有索引的情况下创建约束,你将会收到一个错误。

    \n
    \n

    查看约束

    约束(和索引)成为数据库模式的一部分。

    \n

    我们可以通过使用 :schema 名来来查看我们刚刚创建的约束,就像下边这样:

    \n
    :schems
    \n

    你将会看到新创建的约束以及使用它创建的索引,也可以看到我们之前创建的索引:

    \n

    \"\"

    \n

    测试约束

    我们可以通过尝试创建两个相同的艺术家来测试这个约束是否起作用。

    \n

    执行下边的语句两次:

    \n
    CREATE (a:Artist {name: "周杰伦"}) 
    RETURN a
    \n

    第一次运行这条语句时,节点将会被创建。第二次运行时,你应该会收到以下错误信息:

    \n

    \"\"

    \n

    属性存在约束

    属性存在约束能够确保具有特定标签的所有节点具有特定的属性。比如你可以指定 Artist 标签的所有节点都必须包含 name 属性。

    \n

    使用 ASSERT exists(variable.propertyName) 语法来创建属性存在约束。像下边这样:

    \n
    CREATE CONSTRAINT ON (a:Artist) ASSERT exists(a.name)
    \n
    \n

    请注意,属性存在约束只能在 Neo4j 企业版中使用。

    \n
    \n","tags":["neo4j"]},{"title":"Neo4j 入门教程 - 使用 Cypher 创建索引","url":"/2018/neo4j-tutorial-create-index/","content":"
    \n

    索引是一种数据结构,可以提高数据库数据检索的速度。

    \n
    \n

    在 Neo4j 中,你可以给有标签的点的任何属性创建索引。一旦你创建了一个索引,Neo4j 将会管理它,在数据更新时保持最新的索引。

    \n

    使用 CREATE INDEX ON 语句创建索引,像下边这样:

    \n
    CREATE INDEX ON :Album(name)
    \n

    在上边的例子中,我们为所有标签为 Album 的点的 name 属性创建了一个索引。

    \n

    语句执行成功后,将展示如下信息:

    \n

    \"\"

    \n
    \n

    当你创建一个索引时,Neo4j 会在后台进行操作。如果你的数据库很大,可能需要一段时间。只有当 Neo4j 完成索引创建后,这个索引才会被上线并用于查询。

    \n
    \n

    查看索引

    索引(约束)成为了数据库模式的一部分。

    \n

    在 Neo4j 浏览器中,你可以使用 :schema 命令查看所有索引和约束。

    \n

    来试一试吧:

    \n
    :schema
    \n

    你可以看到一个索引和约束的列表:

    \n

    \"\"

    \n

    索引提示

    索引创建完成后,当你在执行查询时会自动使用。

    \n

    然而 Neo4j 也允许你强制提示一个或多个索引,你可以在你的查询语句中使用 USING INDEX ... 创建一个索引提示。

    \n

    所以上边的示例可以像下边这样强制索引:

    \n
    MATCH (a:Album {name: "猛龙过江"}) 
    USING INDEX a:Album(name)
    RETURN a
    \n

    我们也可以使用多个提示,为每个想强制的索引添加一个新的 USING INDEX 即可。

    \n

    是否有必要索引?

    当 Neo4j 创建索引时,它会在数据库中创建冗余的副本,因此使用索引会占用更多的硬盘空间并减慢写入速度。

    \n

    因此在决定索引哪些数据时你需要进行一些权衡。

    \n

    一般来说当你知道某些节点数量很多时,创建索引是个不错的主意。或者你发现查询时间太长可以尝试通过添加索引来解决。

    \n","tags":["neo4j"]},{"title":"Neo4j 入门教程 - 使用 Cypher 创建节点","url":"/2018/neo4j-tutorial-create-node/","content":"
    \n

    要用 Cypher 创建节点和关系,请使用 CREATE 语句

    \n
    \n
    \n

    这个语句由 CREATE 组成,后边跟上你要创建的点或关系。

    \n

    举例

    我们来创建一个包含乐队名和他们专辑的音乐数据库。

    \n

    第一个乐队被称为筷子兄弟,我们创建一个艺术家节点,并称之为筷子兄弟

    \n

    我们第一个点看起来像下边这样

    \n

    \"\"

    \n

    下边是创建筷子兄弟节点的 Cypher CREATE 语句:

    \n
    CREATE (a:Artist { name : "筷子兄弟" })
    \n

    这个 Cypher 语句创建一个带有 Artist 标签的节点,节点有一个 name 属性,该属性的值是筷子兄弟

    \n

    a 前缀是我们提供的变量名,我们可以使用任意变量名。如果我们需要在后边的语句中用到这个点(上边的情况中我们没有用到),这个变量就会是很有用的。注意,变量仅限于在单条语句中使用。

    \n

    到 Neo4j 浏览器中执行上边的语句,该语句将创建一个节点。

    \n

    一旦 Neo4j 创建完节点,你将看到这样的消息:

    \n

    \"\"

    \n

    展示节点

    CREATE 语句创建节点但是不展示节点。为了展示节点我们需要在它后边跟上 RETURN 语句。

    \n

    我们来创建另一个节点,这次我们创建一个专辑,与之前不同的是这次我们在后边跟上 RETURN 语句

    \n
    CREATE (b:Album { name : "猛龙过江", released : "2014" })
    RETURN b
    \n

    上边语句创建了一个带有 Album 标签的节点,它有两个属性:namereleased

    \n

    注意,我们通过使用它的变量名(本例中是 b)返回了这个节点。

    \n

    创建多个节点

    我们可以通过用逗号分隔来一次性创建多个节点:

    \n
    CREATE (a:Album { name: "我们是太阳"}), (b:Album { name: "小水果"}) 
    RETURN a,b
    \n

    或者可以使用多个 CREATE 语句:

    \n
    CREATE (a:Album { name: "我们是太阳"}) 
    CREATE (b:Album { name: "小水果"})
    RETURN a,b
    \n

    接下来,我们将在节点间建立关系。

    \n","tags":["neo4j"]},{"title":"Neo4j 入门教程 - 使用 Cypher 创建关系","url":"/2018/neo4j-tutorial-create-repationship/","content":"
    \n

    就像在 Neo4j 中创建节点一样,可以用 CREATE 来创建节点间的关系。

    \n
    \n
    \n

    创建关系的语句由 CREATE 组成,后边跟着要创建的关系详情。

    \n

    举例

    我们在先前创建的点之间创建一个关系,首先创建一个乐队和专辑之间的关系。

    \n

    我们将创建如下关系

    \n

    \"\"

    \n

    这是 Cypher 的 CREATE 创建上边关系的语句:

    \n
    MATCH (a:Artist),(b:Album)
    WHERE a.name = "筷子兄弟" AND b.name = "猛龙过江"
    CREATE (a)-[r:RELEASED]->(b)
    RETURN r
    \n

    上边代码的解释

    首先我们使用 MATCH 语句查找我们要创建关系的两个点。

    \n

    可能有很多节点带有 ArtistAlbum 标签,所以我们需要找到我们感兴趣的节点。在这个例子中,我们使用属性值来过滤它:使用之前赋值给每个节点的 name 属性。

    \n

    接下来是用来创建关系的 CREATE 语句,在这个例子中,它通过我们在第一行中给出的变量名称(ab)来引用两个节点,关系是通过字符画模式,用箭头指示关系方向来建立的:(a)-[r:RELEASED]->(b)

    \n

    我们给这个关系一个变量名 r 并且给了一个 RELEASE 类型(乐队发行专辑)。关系类型和节点的标签概念类似。

    \n

    添加更多关系

    上边是一个非常简单的例子,Neo4j 擅长的事情是处理很多相互关联的关系。

    \n

    为了看到继续创建更多节点和它们之间的关系是多么容易,让我们在刚刚的基础上继续构建。我们来创建一个点外加两个关系。

    \n

    我们将要达到下边图效果

    \n

    \"\"

    \n

    这张图展示了王太利在筷子兄弟乐队中参与演奏,在专辑中进行表演并且专辑是由他来创作的

    \n

    我们为王太利创建一个节点:

    \n
    CREATE (p:Person { name: "王太利" })
    \n

    现在来创建关系并返回图:

    \n
    MATCH (a:Artist),(b:Album),(p:Person)
    WHERE a.name = "筷子兄弟" AND b.name = "猛龙过江" AND p.name = "王太利"
    CREATE (p)-[:PRODUCED]->(b), (p)-[:PERFORMED_ON]->(b), (p)-[:PLAYS_IN]->(a)
    RETURN a,b,p
    \n

    执行完成后你应该就可以看到前边截图中的图了。

    \n

    \"\"

    \n","tags":["neo4j"]},{"title":"Neo4j 入门教程 - 查询语言 Cypher","url":"/2018/neo4j-tutorial-cypher/","content":"
    \n

    Neo4j 有自己的查询语言称为 Cypher。Cypher 使用与 SQL(结构化查询语言)类似的语法。

    \n
    \n
    \n

    举例

    下边是一条 Cypher 语句:

    \n
    MATCH (p:Person { name:"Homer Flinstone" })
    RETURN p
    \n

    这条 Cypher 语句返回属性 nameHomer FlinstonePerson 节点。

    \n

    如果通过 SQL 来查询关系型数据库,看起来可能更像这样:

    \n
    SELECT * FROM Person
    WHERE name = "Homer Flinstone";
    \n

    不过请记住,Neo4j 不像关系数据库模型那样将数据存储在表中,Neo4j 的所有数据都在节点和关系中存储。所以上边的 Cypher 语句查询的是节点、节点的标签和节点的属性,而 SQL 查询的是表、行和列。

    \n

    SQL 被设计为适用于关系数据库管理系统(DBMS)。Neo4j 是一个 NoSQL DBMS,所以它不使用关系模型同样也不使用 SQL。

    \n

    Cypher 是专门为 Neo4j 的数据模型而设计,用来查询节点及其相互关系的。

    \n

    字符画语法

    Cypher 使用字符画来表示模式,使得我们在第一次学习这门语言时很容易记住它。如果你忘记了如何编写,只需要想一想图的样子就会对你有所帮助。

    \n
    (a)-[:KNOWS]->(b)
    \n

    主要记住如下几点:

    \n
      \n
    • 节点由圆括号表示,看起来像是圆圈。就像这样:(node)
    • \n
    • 关系用箭头来表示。像这样: ->
    • \n
    • 关系相关的信息可以插入到方括号中。像这样:[:KNOWS]
    • \n
    \n

    定义数据

    在使用 Cypher 时请记住以下几点:

    \n
      \n
    • 节点通常有标签(一个或多个)。比如:”Person”,”User”,”Actor”,”Customer”,”Employee”等
    • \n
    • 节点通常有属性,属性提供有关节点的额外信息。比如:”name”,”age”,”born”等
    • \n
    • 关系也可以有属性
    • \n
    • 关系通常有一个类型(类似于节点的标签)。比如:”KNOWS”,”LIKES”,”WORKS_FOR”,”PURCHASED”等
    • \n
    \n

    让我们再来看一下上边的例子:

    \n
    MATCH (p:Person { name:"Homer Flinstone" })
    RETURN p
    \n

    我们可以看到:

    \n
      \n
    • 节点被圆括号 () 包围
    • \n
    • Person 是节点的标签
    • \n
    • name 是节点的属性
    • \n
    \n","tags":["neo4j"]},{"title":"Neo4j 入门教程 - 使用 Cypher 删除节点","url":"/2018/neo4j-tutorial-delete-node-using-cypher/","content":"
    \n

    要使用 Cpyher 删除节点和关系,可使用 DELETE 子句。

    \n
    \n

    MATCH 语句中使用 DELETE 子句来删除任何匹配的数据。

    \n

    因此 DELETE 子句用在之前例子中的 RETURN 子句的地方。

    \n

    举例

    下边的语句删除标签为 Albumname 属性为 Panmax 的节点:

    \n
    MATCH (a:Album {name: "Panmax"}) DELETE a;
    \n
    \n

    在实际删除前认真检查语句是否删除的是正确的数据是个不错的主意。
    为此可以先使用 RETURN 子句构造语句,然后运行它。这样可以检查要删除的是不是正确的数据。一旦你对匹配的结果数据满意后,只需将 RETURN 子句改为 DELETE 子句即可。

    \n
    \n

    删除多个节点

    你也可以一次性删除多个节点。只需要让你的 MATCH 语句包含所有你想要删除的节点就行了。

    \n
    MATCH (a:Artist {name: "jiapan"}), (b:Album {name: "Panmax"}) 
    DELETE a, b
    \n

    删除所有节点

    你可以通过省略过滤条件来删除数据库中的所有节点,就像我们从数据库中选取所有节点一样,你也可以删除它们。

    \n
    MATCH (n) DELETE n;
    \n

    删除带有关系的节点

    在删除节点时有一个小细节需要注意,就是你只能删除没有连接任何关系的节点。换句话说,在删除节点本身前,必须先删除和它相关的关系。

    \n

    如果你尝试在具有关系的节点上执行上述 DELETE 语句,你将看到如下所示的错误消息:

    \n

    \"\"

    \n

    这个错误消息告诉我们,我们在删除节点前必须先删除它的关系。

    \n

    幸运的是有一种便捷的方式可以做到这一点,我们会在下一课来介绍它。

    \n","tags":["neo4j"]},{"title":"Neo4j 入门教程 - 使用 Cypher 删除关系","url":"/2018/neo4j-tutorial-delete-relationship-using-cypher/","content":"
    \n

    你可以像删除节点一样删除关系 - 通过匹配你想要删除的关系。

    \n
    \n

    你可以一次性删除一个或多个关系,甚至可以删除数据库中的所有关系。

    \n

    首先,作为复习,以下是我们之前创建的关系:

    \n

    \"\"

    \n

    我们来删除类型为 RELEASED 的关系。

    \n

    有几种方法可以解决这个问题,我们来看其中的三种。

    \n

    下边的语句范围非常广 - 它将删除所有类型为 RELEASED 的关系。

    \n
    MATCH ()-[r:RELEASED]-() 
    DELETE r
    \n

    你也可以写的更具体一些,就像这样:

    \n
    MATCH (:Artist)-[r:RELEASED]-(:Album) 
    DELETE r
    \n

    上边的语句将匹配所有的 Artist 节点和 Album 节点间具有 RELEASED 的关系。

    \n

    你甚至可以更具体一些,就像这样:

    \n
    MATCH (:Artist {name: "筷子兄弟"})-[r:RELEASED]-(:Album {name: "猛龙过江"}) 
    DELETE r
    \n

    上边的任意一条语句都可以将 RELEASED 关系删掉,图将看起来是这样的:

    \n

    \"\"

    \n

    删除有关联关系的节点

    节点存在关系将不能被删除,如果我们尝试执行下边的语句:

    \n
    MATCH (a:Artist {name: "筷子兄弟"}) DELETE a
    \n

    会看到如下错误:

    \n

    \"\"

    \n

    这是因为节点上有连接的关系。

    \n

    一种选择是删除所有的关系,然后再删除节点。

    \n

    另一种选择是使用 DETACH DELETE 子句。DETACH DELETE 子句允许你删除一个节点的同时删除与其相连的所有关系。

    \n

    所以我们可以将上面的语句改为:

    \n
    MATCH (a:Artist {name: "筷子兄弟"}) DETACH DELETE a
    \n

    执行这条语句将看到下边的成功消息:

    \n

    \"\"

    \n

    删除整个数据库

    你可以进一步使用 DETACH DELETE 并删除整个数据库。

    \n

    只需将过滤条件去掉就可以删除所有的点和关系了。

    \n

    继续来执行下边的语句:

    \n
    MATCH (n) DETACH DELETE n
    \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

    你可以使用 DROP INDEX ON 语句删除索引,这将从数据库中删除索引。

    \n
    \n

    因此要删除我们之前创建的索引,我们可以使用以下语句:

    \n
    DROP INDEX ON :Album(name);
    \n

    语句执行成功后会展示以下消息:

    \n

    \"\"

    \n

    查看模式

    你现在可以使用 :schema 命令来验证对应的索引是否已经从模式中删除。

    \n

    只需输入:

    \n
    :schema
    \n

    可以看到索引已经不在模式中了:

    \n

    \"\"

    \n
    \n

    你可以使用 DROP CONSTRAINT 语句删除约束,这将从数据库中删除约束和相关索引。

    \n
    \n

    那么让我们来删除之前创建的约束(和它关联的索引)吧,我们可以使用下边的语句:

    \n
    DROP CONSTRAINT ON (a:Artist) ASSERT a.name IS UNIQUE
    \n

    语句执行成功后会展示下边的消息:

    \n

    \"\"

    \n

    查看模式

    你现在可以使用 :schema 命令来验证对应的索引(和相关联的约束)是否已经从模式中删除。

    \n

    只需输入:

    \n
    :schema
    \n

    可以看到约束已经不在模式中了:

    \n

    \"\"

    \n","tags":["neo4j"]},{"title":"Neo4j 入门教程 - 使用 Cypher 导入来自 CSV 文件的数据","url":"/2018/neo4j-tutorial-import-data-from-csv-file-using-cypher/","content":"
    \n

    你可以将 CSV 文件中的数据导入到 Neo4j 数据库中,为此我们来学习下 Cypher 中的 LOAD CSV 语句。

    \n
    \n

    将 CSV 文件导入到 Neo4j 的能力,可以实现从其他类型的数据库来导入数据(比如关系型数据库)。

    \n

    在 Neo4j 中,你可以通过本地或远端 URL 来加载 CSV 文件。

    \n

    要访问本地(在数据库服务器上)文件,使用 file:/// 路径。除此之外,可以使用任何 HTTPS,HTTP 和 FTP 协议。

    \n

    读取 CSV 文件

    我们使用 HTTP 协议加载一个名为 genres.csv 的 CSV 文件。它不是一个大文件,这个列表里包含了 115 个音乐流派,所以它将创建 115 个节点(和 230 个属性)。

    \n

    这个文件上传到了开放的网络中,所以你可以在你的 Neo4j 浏览器中运行下边的代码,它可以直接导入到你的数据库中。

    \n
    LOAD CSV FROM 'https://jpanj.com/2018/neo4j-tutorial-import-data-from-csv-file-using-cypher/genres.csv' AS line
    CREATE (:Genre {genreId: line[0], name: line[1]})
    \n
    \n

    你也可以忽略 CSV 文件中的某些字段,比如,如果你不希望将第一个字段导入到数据库中,可以从上边的代码中省略 genreId: line[0],

    \n
    \n

    运行上边的 Cypher 语句会产生以下成功消息:

    \n

    \"\"

    \n

    你可以通过以下查询来查看刚刚新创建的节点:

    \n
    MATCH (n:Genre) RETURN n
    \n

    下边是通过数据可视化界面看到的节点结果:

    \n

    \"\"

    \n

    导入包含标题的 CSV 文件

    之前的 CSV 文件不包含任何标题,如果 CSV 文件包含标题,可以使用 WITH HEADERS

    \n

    使用这个方法还允许你通过它的列名(标题名)来引用每个字段。

    \n

    我们有另一个带标题的 CSV 文件,该文件包含专辑曲目列表。

    \n

    同样,这个文件不大,列表中包含了 32 个专辑,所以它将创建 32 个节点(和 96 个属性)。

    \n

    这个文件也上传到了开放的网络中,所以你可以在你的 Neo4j 浏览器中运行下边的代码,它可以直接导入到你的数据库中。

    \n
    LOAD CSV WITH HEADERS FROM 'https://jpanj.com/2018/neo4j-tutorial-import-data-from-csv-file-using-cypher/tracks.csv' AS line
    CREATE (:Track { trackId: line.Id, name: line.Track, length: line.Length})
    \n

    这将产生下边的成功消息:

    \n

    \"\"

    \n

    下边的查询语句可以查看新创建的节点:

    \n
    MATCH (n:Track) RETURN n
    \n

    同样我们通过可视化框架看到的节点的结果。

    \n

    点击 Table 图标可以看到每个点和它的三个属性值:

    \n

    \"\"

    \n

    自定义分隔符

    如果需要的话你可以指定自定义字段分隔符,假如 CSV 文件中的分隔符是分号的话,你可以指定使用分号作为分隔符而不是逗号。

    \n

    只需将 FIELDTERMINATOR 子句添加到语句中就可以做到了,像下边这样:

    \n
    LOAD CSV WITH HEADERS FROM 'https://jpanj.com/2018/neo4j-tutorial-import-data-from-csv-file-using-cypher/tracks.csv' AS line
    FIELDTERMINATOR ';'
    CREATE (:Track { trackId: line.Id, name: line.Track, length: line.Length})
    \n

    导入大文件

    如果你需要导入包含大量数据的文件,可以使用 PERODIC COMMIT 来处理。

    \n

    在 Neo4j 中使用定期提交功能可以在导入一定数量的行之后提交一次数据,这减少了事务状态的内存开销。

    \n

    默认是 1000 行,所以数据会每 1000 行提交一次。

    \n

    要使用定期提交,只需在语句开头插入 USING PERIODIC COMMIT (在 LOAD CSV 之前)。

    \n

    下边有个例子:

    \n
    USING PERIODIC COMMIT
    LOAD CSV WITH HEADERS FROM 'https://jpanj.com/2018/neo4j-tutorial-import-data-from-csv-file-using-cypher/tracks.csv' AS line
    CREATE (:Track { trackId: line.Id, name: line.Track, length: line.Length})
    \n

    设置定期提交频率

    你还可以将 1000 行的默认值更改为另一个数字,只需将数字加在 USING PERIODIC COMMIT 后边就行了,就像这样:

    \n
    USING PERIODIC COMMIT 800
    LOAD CSV WITH HEADERS FROM 'https://jpanj.com/2018/neo4j-tutorial-import-data-from-csv-file-using-cypher/tracks.csv' AS line
    CREATE (:Track { trackId: line.Id, name: line.Track, length: line.Length})
    \n

    CSV 的格式要求

    以下是使用 LOAD CSV 时应该如何格式化 CSV 文件的一些要求:

    \n
      \n
    • 字符编码必须是 UTF-8
    • \n
    • 行终止标识和系统有关,例如在 Unix 中是 \\n,在 Windows 上是 \\r\\n
    • \n
    • 分隔符必须是逗号,除非用通过 FIELDTERMINATOR 特殊指定
    • \n
    • 如果字符串是用双引号引起来的,数据读入后会将双引号去掉
    • \n
    • 任何需要转义的字符都可以通过反斜线 \\ 来转义
    • \n
    \n","tags":["neo4j"]},{"title":"Neo4j 入门教程 - 安装","url":"/2018/neo4j-tutorial-installation/","content":"

    本篇来简单介绍下如何下载并安装 Neo4j,篇目很短,因为真的很简单。

    \n

    下载 Neo4j

    首先在 https://neo4j.com/download/ 下载 Neo4j。你可以选择企业体验版或者免费的社区版,这里我是用的社区版。点击 Download 按钮即可开始下载。

    \n

    网站会自动下载适合您操作系统的文件,如果你不想要这个,可以选择通过 这个链接 选择另一个操作系统的版本。

    \n

    安装 Neo4j

    当文件下载下来后,就可以安装 Neo4j 了。下载页面包含了将 Neo4j 安装到你的操作系统的一步步指导说明,我在这里介绍下 Mac、Windows 和 Linux 的安装。这里列出的说明,是为了让你快速了解安装 Neo4j 所涉及的步骤,实际步骤可能会随着未来的版本而变化,所以请务必按照下载时网站上的说明来进行安装。当你下载 Neo4j 时,Neo4j 会在感谢页面展示这些说明。

    \n

    Mac (dmg)

    这个安装程序包含了运行 Neo4j 所需要的 Java 版本。

    \n
      \n
    1. 打开你刚刚下载好的 dmg 文件
    2. \n
    3. 将 Neo4j 的图标拖拽到你的应用目录中
    4. \n
    5. 在应用目录中打开 Neo4j,你可能会被系统询问是否是你从互联网上下载的这个程序,不要担心,确认即可
    6. \n
    7. 点击 Start 按钮来启动 Neo4j 的服务
    8. \n
    9. 在你的浏览器中打开程序提供给你的 URL
    10. \n
    11. neo4j 账户修改密码
    12. \n
    \n

    Linux/Unix (tar/tar.gz)

      \n
    1. 打开你的终端
    2. \n
    3. 使用 tar -xvf <file> 来提取存档的内容。比如 tar -xvf neo4j-community-3.2.8-unix.tar,如果你下载的是 tar.gz 的压缩包,那么使用 tar -zxvf 来进行解压
    4. \n
    5. 使用 $NEO4J_HOME/bin/neo4j console 来运行 Neo4j,或者用 $NEO4J_HOME/bin/neo4j start 让服务进程在后台运行
    6. \n
    7. 在你的本机浏览器访问 http://localhost:7474
    8. \n
    9. neo4j 账户修改密码
    10. \n
    \n

    Windows (exe)

    这个安装程序包含了运行 Neo4j 所需要的 Java 版本。

    \n
      \n
    1. 运行你刚刚下载的安装程序,你可能需要给这个程序的安装权限来授权
    2. \n
    3. 按照提示选择运行 Neo4j 的选项
    4. \n
    5. 点击 Start 按钮来启动 Neo4j 服务器
    6. \n
    7. 在浏览器中打开程序提供给你的 URL
    8. \n
    9. neo4j 账户修改密码
    10. \n
    \n

    Windows (zip)

      \n
    1. 首先安装 JDK8
    2. \n
    3. 找到压缩包,点击右键进行解压
    4. \n
    5. 把解压出的文件放到服务器的主目录中,顶级目录称为 NEO4J_HOME,比如 D:\\neo4j\\
    6. \n
    7. 使用 zip 包中提供的 Windows PowerShell 来启动和管理 Neo4j
    8. \n
    9. 在浏览器中访问 http://localhost:7474
    10. \n
    11. neo4j 账户修改密码
    12. \n
    \n

    启动并连接到 Neo4j 服务

    1. 启动服务

    这里是一个已经启动起来的 Neo4j 服务,启动方法取决于你的操作系统,我这里用 Mac 来举例,在应用目录中点击 Neo4j Community Edition 3.2.6,点击打开窗口中 Start 按钮即可启动 Neo4j 服务。

    \n

    \"\"

    \n

    服务启动后,在浏览器中打开 http://localhost:7474 然后按照提示进行操作。

    \n

    下图是我第一次进入的界面(未来版本可能会看到不同的界面)

    \n

    \"\"

    \n

    2. 登录

    使用界面上提供的用户名和密码来登录,默认的密码是 neo4j

    \n

    第一次登录时,系统会提示你修改密码

    \n

    3. 结果

    密码修改完成后这个界面将会被展示

    \n

    \"\"

    \n

    在这里,你可以使用当前界面提供的链接来学习更多关于 Neo4j 的知识以及如何创建数据库和运行查询语句

    \n","tags":["neo4j"]},{"title":"Neo4j 入门教程 - 使用 Cypher 的 MATCH 语句选取数据","url":"/2018/neo4j-tutorial-select-data-with-match-using-cypher/","content":"
    \n

    Cypher 的 MATCH 语句允许你查询符合条件的数据。你可以使用 MATCH 来返回数据或对这些数据执行一些其他操作。

    \n
    \n

    MATCH 语句用于匹配给定的条件,但实际上它并不返回数据。为了从 MATCH 语句返回数据,我们仍然需要使用 RETURN 子句。

    \n

    检索一个节点

    这里有个使用 MATCH 语句检索一个节点的的例子。

    \n
    MATCH (p:Person)
    WHERE p.name = "王太利"
    RETURN p
    \n

    WHERE 子句与 SQL 的 WHERE 子句工作方式相同,它允许你提供额外的条件来缩小查询范围。

    \n

    同时你可以在不使用 WHERE 子句的情况下获得相同的结果。你可以通过使用像创建节点那样的符号来查询节点。

    \n

    下边的代码提供和上边语句相同的结果:

    \n
    MATCH (p:Person {name: "王太利"})
    RETURN p
    \n

    运行上边任意一条查询语句将会看到如下的节点被展示出来:

    \n

    \"\"

    \n

    你可能已经注意到,点击一个节点会展开一个分成三部分的外部圆,每个部分有不同的选项:

    \n

    \"\"

    \n

    点击底部选项将展开节点的关系:

    \n

    \"\"

    \n

    关系

    你还可以通过 MATCH 语句遍历关系。事实上,这才是 Neoj4 真正擅长的事情之一。

    \n

    举个栗子,如果我们想找出哪个乐队发布了名为「猛龙过江」的专辑,可以使用如下查询语句:

    \n
    MATCH (a:Artist)-[:RELEASED]->(b:Album)
    WHERE b.name = "猛龙过江"
    RETURN a
    \n

    这将返回以下节点:

    \n

    \"\"

    \n

    可以看到我们在 MATCH 中使用的模式几乎是不言自明的,它匹配了所有发布过名为 猛龙过江 专辑的乐队。

    \n

    我们使用了变量(a,b)以便在稍后的查询中引用他们。我们没有为关系提供任何变量,因为我们不需要在之后的查询中引用关系。

    \n

    你可能还会注意到第一行使用的是我们在创建关系时相同的模式,这突出了 Cypher 语言的简单性,我们可以在不同的上下文中使用相同的模式(比如创建数据和遍历数据)。

    \n

    返回全部节点

    你可以通过省略过滤条件来返回数据库中所有的节点。因此以下查询将返回数据库中的所有节点:

    \n
    MATCH (n) return n;
    \n

    我们所有的节点将被返回:

    \n

    \"\"

    \n

    你还可以点击侧面的 Table 图标用表格来展示数据:

    \n

    \"\"

    \n
    \n

    返回所有节点时要小心,在大型数据库中执行这个操作可能会产生很大的性能影响。通常建议限制结果以避免意想不到的问题。

    \n
    \n

    限制结果

    使用 LIMIT 来限制输出记录的数量,当你不确定结果集有多大时,使用 LIMIT 是个好主意。

    \n

    因此我们可以简单的将 LIMIT 5 追加到前边的语句上来将输出限制为5条记录:

    \n
    MATCH (n) RETURN n 
    LIMIT 5
    \n","tags":["neo4j"]},{"title":"Neo4j 冷启动预热缓存","url":"/2017/neo4j-warms-up/","content":"

    你可能发现有些查询在第二次运行时非常的快,这是因为在冷启动时服务节点中没有任何缓存,需要到硬盘中查找所有的记录。每当部分或全部记录被缓存,你将发现有了很大的性能提升。

    \n

    一种被广泛使用的技术是「缓存预热」,借助这个技术,我们运行一个查询语句来触发图中所有的点和关系。假设内存可以容纳这些数据,整个图会被缓存起来。否则将会缓存尽可能多的数据。尝试一下它是如何给你带来帮助的吧!

    \n

    Cypher(Server,Shell)

    \n
    MATCH (n)
    OPTIONAL MATCH (n)-[r]->()
    RETURN count(n.prop) + count(r.prop);
    \n

    上边的例子用到了 count(n.prop) + count(r.prop) ,来强制让优化器在点或关系中搜索名为 prop 的属性。用 count(*) 替代它将不够充分,因为这样不会加载所有的点和关系属性。

    \n

    内嵌方式(Java):

    \n
    @GET @Path("/warmup")
    public String warmUp(@Context GraphDatabaseService db) {
    try ( Transaction tx = db.beginTx()) {
    for ( Node n : GlobalGraphOperations.at(db).getAllNodes()) {
    n.getPropertyKeys();
    for ( Relationship relationship : n.getRelationships()) {
    relationship.getPropertyKeys();
    relationship.getStartNode();
    }
    }
    }
    return "Warmed up and ready to go!";
    }
    \n

    在 3.0 之后的版本并且使用了 APOC 插件的话,可以运行如下存储过程来完成缓存预热

    \n

    CALL apoc.warmup.run()

    \n
    \n

    property record loading for warmup, apoc.warmup.run(true)

    \n
    \n

    CALL apoc.warmup.run() 默认不读取属性记录,更加建议使用 call apoc.warmup.run(true),这个是 3.2.0 以上版本插件的新功能。

    \n

    \"\"

    \n

    这样做除了纯粹的提升性能外还可以提供更多方面的帮助,如果你使用的是 Neo4j集群的话,还可以帮助缓解由于查询滞后而导致的上游问题。例如,如果节点繁忙并且负载均衡超时时间很短,图中没有任何数据在内存中,很可能会显示该集群最初不可用。如果缓存处于预热状态,那么冷启动应该就不会有短暂超时的问题了。

    \n

    APOC 安装

      \n
    1. 将最新的插件 jar 包下载后放进 neo4j 的 plugins 目录中
    2. \n
    3. 修改配置文件加入 dbms.security.procedures.unrestricted=apoc.*
    4. \n
    5. 重启 neo4j
    6. \n
    \n","tags":["neo4j"]},{"title":"Neo4j 关系不支持多种类型","url":"/2018/neo4j-with-multiple-types/","content":"

    Neo4j 中创建节点时,可以指定多个标签:

    \n
    CREATE (n:Person:China)
    \n

    但是在创建关系时,只能指定一种类型,其实官方通过这两个不同词汇(标签 和 类型)也能体现出节点和关系关于分类方面的不同。

    \n

    如下图所示,在尝试用多种类型创建关系时,会报错:

    \n

    \"\"

    \n

    \"\"

    \n

    一个节点可以有多个标签,一个关系只能有一种类型。

    \n

    GitHub issues 中也有一个简短的解释:

    \n
    \n

    This is unfortunately out of scope right now the property-graph model fared well with a single relationship-type so far.

    \n
    \n
    \n

    I’d suggest you create multiple relationships between your two nodes that is the way to go.

    \n
    \n

    属性图模型在单一关系类型时表现更好,如果需要在两个节点间表示多个关系,直接创建多条关系就可以了。

    \n","tags":["neo4j"]},{"title":"又入手了一个 HHKB","url":"/2023/new-hhkb/","content":"

    15年我刚工作不久,当时有个 App 叫网易海淘,我通过这个平台在日本亚马逊买到了自己的第一个机械键盘 HHKB,记得当时的价格是1550左右。大学时候好几个舍友都因为玩游戏买了机械键盘,但我一直用的都是罗技很便宜的那款。

    \n

    这个 HHKB 键盘到现在陪伴了我8年多年,电脑换了好几个、工作换了好几份,没换的一直是这个键盘。有点像换马不换鞍的感觉,HHKB 一直作为我最亲密的战友陪伴着我,我在公司写代码、调戏妹子、和其他人对喷都离不开它,我在公司拍工位照片时也都有它的身影。

    \n

    17年:

    \n

    \n

    18年:

    \n

    \n

    22年:

    \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

    如果你的男朋友是程序员,相信我,送他这款键盘准没错👨🏻‍💻

    \n
    \n

    BTW,开车回家的路上还在下雨,我在一个十字路口亲眼目睹了一场车祸,两个车都赶在变黄灯前加速,一个左转一个执行,我眼看着两个车就想游乐场的碰碰车一样撞在了一起,听到 duang 一声、地面颤动了一下,事情太突然,当时的感觉不太真实,车上的人应该都没有大碍,过十字路口还是要注意安全,黄灯能不抢还是不抢。

    \n

    \n"},{"title":"nginx 配置反向代理 + ssl 模板","url":"/2019/nginx-reverse-proxy-and-ssl-template/","content":"
    server {
    listen 80;
    server_name abc.com;

    location ^~ /.well-known/acme-challenge/ {
    alias /xxx/xxx/;
    try_files $uri =404;
    }

    location / { // 强制 https 重定向
    rewrite ^(.*)$ https://$host$1 permanent;
    }


    }

    server {
    listen 443 ssl;
    server_name abc.com;

    location / {
    proxy_pass http://127.0.0.1:8000;
    proxy_set_header Host $host;
    proxy_set_header X-Real-IP $remote_addr;
    proxy_set_header REMOTE-HOST $remote_addr;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    }

    ssl on;
    ssl_certificate /root/ssl/chained.pem;
    ssl_certificate_key /root/ssl/domain.key;
    }

    // 静态网站
    server {
    listen 443 ssl;
    server_name xxx.com;

    root /www/xxx;
    index index.html;
    error_page 404 /404.html;

    ssl on;
    ssl_certificate /root/ssl/chained.pem;
    ssl_certificate_key /root/ssl/domain.key;
    }
    \n"},{"title":"解决 CentOS 7 下 Nginx 报 Too many open files","url":"/2020/nginx-too-many-open-files/","content":"

    前几天在对开发环境中的服务进行压测时 Nginx 出现 Too many open files 的错误,这里记录下解决方法。

    \n

    \"1.jpg\"

    \n

    检查文件句柄

    先来通过两个命令检查下 master 进程 和 worker 进程的文件句柄限制。

    \n

    在 Nginx 运行时,检查当前 master 进程的限制:

    \n
    cat /proc/$(cat /var/run/nginx.pid)/limits|grep open.files

    Max open files 1024 4096 files
    \n

    检查 worker 进程:

    \n
    ps --ppid $(cat /var/run/nginx.pid) -o %p|sed '1d'|xargs -I{} cat /proc/{}/limits|grep open.files

    Max open files 1024 4096 files
    Max open files 1024 4096 files
    Max open files 1024 4096 files
    Max open files 1024 4096 files
    \n

    上边返回结果的第二列和第三列分别为软限制(soft limit)和硬限制(hard limit),下边我们来对其进行调整。

    \n

    调整限制

      \n
    1. /etc/sysctl.conf 中加上 fs.file-max = 70000
    2. \n
    3. /etc/security/limits.conf 中加上 nginx soft nofile 10000nginx hard nofile 30000
    4. \n
    5. 执行 sysctl -p 使配置生效
    6. \n
    7. /etc/nginx/nginx.conf 中加上 worker_rlimit_nofile 30000;
    8. \n
    \n

    虽然 Nginx 可以通过 nginx -s reload 使配置生效,但这种方式并不会让全部进程都应用上新的配置,如果你在多核机器下,可以实验下:在执行这个操作后,通过检查 worker 进程句柄限制(方法见上文),还是有部分进程的句柄被限制为 S1024/H4096,即使试用 nginx -s quit 也不管用。解决方法是用 kill 命令杀掉 Nginx 后重新启动,这样所有的 Nginx 进程就都有了 S10000/H30000 的文件句柄限制。

    \n
    pkill -9 nginx
    systemctl start nginx
    \n

    再次验证 worker 进程

    \n
    ps --ppid $(cat /var/run/nginx.pid) -o %p|sed '1d'|xargs -I{} cat /proc/{}/limits|grep open.files

    Max open files 30000 30000 files
    Max open files 30000 30000 files
    Max open files 30000 30000 files
    Max open files 30000 30000 files
    \n

    可以看到配置已在全部 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 比他小,另一个说他的 Leader 是95后之类的。

    \n

    一个好的直属 Leader 对工作体验太重要了,在工作中伴随我们最久对我们的影响最大的人就是直属 Leader。我不太相信一个工作两三年的人有特别好的管理能力。对管理的认识虽然可以靠书本学习一些驭人之术来提升,但更多的是靠人生阅历,前者是 PUA,后者是真正的管理。但要做到后者是需要时间的,就像我们不可能找10个孕妇来一个月内生出一个宝宝一样。

    \n

    我那个去了字节的领导第三天就要求去参加季度规划会,之前他的话语权很重,大家都会听他的,但他在字节的第三天,就在会上比被自己小的产品经理diss,问他是不是不了解背景,质疑他的能力。这也是我前边说过的温情,一个稍微成熟点的,有点社会阅历的成年人不会对一个刚入职3天的人讲出那种话。我的自尊心很重、心眼很小,承受不了职场PUA…

    \n

    有人生阅历的 Leader 更加善而坚定,更加有管理上的温情,这样的领导能站在员工的角度理解员工,照顾员工的感受,真正为员工着想。

    \n

    另外大厂里还会有各种「嫡系」文化,在有裁员指标时,通常裁的不是能力不行的,而是非嫡系的。在有晋升指标时最先安排的也是嫡系里的“自己人”。

    \n

    鸡头与凤尾

    我深知人外有人天外有天,我可以在小公司里混的如鱼得水,但放到大厂的人才荟聚的地方也许就是一颗再普通不过的螺丝钉。

    \n

    我不想在一个默默无闻的岗位工作,这种地方不会让我感受到成就感,很容易失去工作的动力。而大公司就是这么一个地方,大公司会使得每个员工的贡献平均化,这是一个问题。我觉得,大公司最大的困扰就是无法准确测量每个员工的贡献。

    \n

    做宽与做专

    我也许更擅长把一件事情从0做到80分,但从80做到100甚至120分不是我擅长的,而这是在大厂里需要具备的精益求精的能力。我更喜欢做宽而不是做专,喜欢做个八面手而不是一颗螺丝钉,由此也可以看出小公司更适合我一些。

    \n
    \n

    我不想离开现在的公司的最主要原因还是工作时长方面,虽然现阶段的我需要钱,去大公司确实可以用时间换钱,但综合考虑各种因素,对于这个年龄和家庭情况的我已经不再合适。留给那些还年轻、还有梦想的年轻人们去闯一闯吧,未来属于他们。

    \n"},{"title":"由日本排放核废水引发的思考","url":"/2023/nuclear-wastewater/","content":"

    首先声明,我不是精日,只是想就事论事反思一下最近看到的新闻。

    \n

    日本放出要排放核废水的消息后,国内声讨的新闻铺天盖地,官方对这件事大肆渲染,带百姓们的节奏。官方宣传这件事有他自己的目的,其中之一是最近出现的很多人民内部矛盾已经不可协调,需要一些外部事件来转移和宣泄人民的情绪。

    \n

    官方的做法无可厚非,毕竟是一种维稳的政治手段,但百姓们的各种行为就让人大跌眼镜了。超市的盐被大妈们抢购一空,女士和小学生边撕心裂肺的哭边骂街,年轻人上街打砸日本车。各个年龄段的人民都在用自己的方式来宣泄情绪。

    \n

    对比较理性的人来讲,他们通常不问做错事是否有理由,而是先确定当前是否做错了事。

    \n

    在日本排放核废水这件事上,人们不判断日本的做法到底符不符合规范,只要是日本做的事,我们的人民从来都不判断对错,直接默认就是对方的错,上来就是破口大骂。在其他很多事情上也是类似。

    \n

    实际上日本排放核废水是经过国际原子能机构同意的,已经达到了安全标准。同时韩国和中国也都参与了监管。

    \n

    美国之音的原文如下:

    \n
    \n

    日本政府決定將核廢水排入海洋的作法已獲得國際機構的背書。自2021年開始評估約兩年後,聯合國核監督機構國際原子能機構(IAEA)上個月初批准了日本排放的計畫,得出的結論是將核廢水排入海洋,對人類和環境的輻射影響“可以忽略不計”。

    \n
    \n

    另外,日本排放核废水受影响最大的是哪个国家?肯定是日本自己本国,只能说我们的国民咸吃萝卜淡操心,自己过的不好还要去操心别人。

    \n

    我们根本不需要成为所有领域的专家,只要有一点点批判性思维,先问有没有再问为什么,就能够避免大部分常识性错误。

    \n

    由此可以看出,我们国家国民的思想进步还有很长的一段路要走。

    \n"},{"title":"Obsidian换成Notion","url":"/2023/obsidian-to-notion/","content":"

    我之前经常给别人吹嘘Obsidian的强大,甚至在公司的内部分享中也给大家推荐过Obsidian。

    \n

    我现在最常用的是Notion,很早前就放弃了Obsidian。

    \n

    我放弃Obsidian的几个主要原因是有:

    \n
      \n
    • 手机端做的很糟糕。Obsidian应该就是直接把电脑端那套东西搬到了手机端,没有做太多适配,导致手机端体验非常不好,插件多一点就会卡顿。
    • \n
    • 数据同步不及时。我一开始使用的同步方案是基于iCloud,完全是佛系同步。后来改用AWS,但每次使用前后都要自己手动执行同步,不够便利,配置也较为复杂。
    • \n
    • 无法开箱即用。需要安装很多插件后才能用起来,配色方案还要自己选了又选,每个配色和风格都有自己不满足的地方。
    • \n
    • 复杂的插件系统。插件有两种安装方式,可以通过软件内直接安装,也可以通过源码安装。不同的插件安装后配置的方式也有区别,有的插件是改配置文件,有的可以直接在界面上配置,和前边遇到的问题一样,插件配置的多终端同步也就很不方便。也因为存在插件系统,我永远不知道自己没有用上什么功能,就会沉浸在每天浏览插件的焦虑中。
    • \n
    \n

    这些缺点在Notion上都得到了解决,Notion本身就是基于Web的,数据自始至终都在云端,不存在数据同步问题。我一开始吹嘘Obsidian时用到的一个理由是数据属于我自己,不信任任何第三方,第三方跑路后你的数据就再也找不回来了。现在想想真是既可笑又狂妄,况且Notion支持数据导出,导出后的数据就是纯文本Markdown格式,很容易迁移。

    \n

    Notion在手机端做了大量优化,弱网环境下也可以使用离线数据,离线编辑,有网后自动进行同步和合并,界面也极其流畅。

    \n

    Notion完全可以开箱即用,你可以使用它的Web端,也可以使用它的客户端,即使一个全新的用户也能很快上手用起来。

    \n

    Notion的数据库系统和模板库也很强大,我用数据库系统记录Twitter上感兴趣的推文,还用它来维护我在Github上star的Repo。这两个用到数据库的功能都是使用别人开源的代码实现的。模板系统我用的不多,公司项目有专门的项目管理工具,我的待办事项使用Things。在数据库系统和模板系统方面,我在今后有精力了还需要深入学习一下。

    \n

    有一说一,Obsidian由双向链接构成的知识图谱确实非常强大,猛一看也很唬人,但一般用户很难维护起自己的网络结构。Obsidian的插件生态也很好,有各种强大的插件可以使用,但还是有一定的上手难度。

    \n

    我觉得Obsidian和Notion这两个阵营很像手机操作系统中的安卓和苹果。

    \n

    我在大学使用安卓手机的时候,最喜欢折腾的事情就是刷机、装插件、改主题、改字体等等。后来改用iPhone后一开始也喜欢折腾越狱之类的,后来随着苹果的生态越来越好,加上自己精力有限也就不折腾了。在折腾iPhone越狱期间,我发现我每次改完一个地方,过段时间就会刷回原生操作系统,比如改了个字体、加了个图标,过段时间腻了就又会刷回去,到头来还是觉得自带的顺眼、舒服,自己整的花里胡哨的一点用都没有。

    \n

    想想也是,苹果这么大的公司,由那么多专业的设计师设计出来的界面、选择出的字体,一定是符合绝大多数用户的最佳方案。我一个非专业人事居然会认为自己改的风格会超过专业的设计师。

    \n
    \n

    专业人士和业余爱好者的一个差别在于,是否了解极限的存在。

    \n
    \n

    Notion也是一样,Notion内部一定有非常专业的产品经理和设计团队去考虑如何更好的为用户服务,让用户有更好的使用体验。我们作为普通用户,享受他们的服务就可以了,业余的水平再高也是业余的,专业的事就交给专业的人去做就好了。

    \n

    既然专业的人做专业的事,同理专业的事应该交给专业的工具,所以我不会用 Notion 作为任务管理工具,因为他在这个领域并不专业。

    \n

    Notion 的产品完整度很高,每个功能都进过了精细的打磨,整个产品用起来有很扎实稳重的感觉,而 Obsidian 给我一种轻飘飘的感觉。

    \n

    苹果和Notion虽然一直在听取市场上用户们的需求,但他们的每次改动都是经过深思熟虑的。让用户满意并不等于迎合用户的一切要求。用户不了解所有可能的选择,也经常弄错自己真正想要的东西。

    \n

    做一个好产品就像做一个好中医一样,不能头痛医头,脚痛医脚。病人告诉你症状,你必须找出他生病的真正原因,然后针对病因进行治疗。

    \n"},{"title":"一行 Python 代码能做什么","url":"/2021/one-line-python/","content":"

    大学中虽然教授过 C、C++、Java,但我当时选择自学了 Python,并且在工作的前几年也是用的 Python,我非常喜欢这门语言。

    \n

    虽然我在平时开发时,不喜欢为了让代码精简些、酷炫些而写出匪夷所思的代码,那些代码大部分杂乱无章、可读性差,第二次再读自己的代码就不一定能读懂。但 Python在这方面做得非常好,这也是为什么它经常成为编码挑战、面试手写代码的首选。

    \n

    下面我对 Python 中用一行代码就能解决的问题做了下整理,通过这些代码片段和技巧也能看出这门语言设计的精妙和优雅:

    \n

    一个数字的位数之和

    这个单行代码对于计算一个数字的位数之和非常有用:

    \n
    sum_of_digit = lambda x: sum(map(int, str(x)))
    output = sum_of_digit(123)
    print("Sum of the digits is: ", output)

    Output:
    Sum of the digits is: 6
    \n

    单一的if-else条件

    在其他语言中,条件式有时看起来有点笨重,如:

    \n
    x = 10
    y = 5
    if x > y:
    print("x is greater than y")
    else:
    print("y is greater than x")
    \n

    用Python简化它:

    \n
    x = 10
    y = 5
    print("x is greater than y" if x > y else "y is greater than x")
    \n

    读起来和正常的英语有些像。

    \n

    你可以用以下结构在一行代码内形成一个if语句:

    \n

    <条件-真> if 条件 else <条件-假>

    \n

    多个if-else条件

    我们有时会使用大量的 if-else 语句,我们使用 elif 关键字,它是其他语言 else if 关键字组合的缩写,这对于转换为单行的python代码来说比较困难,看一个在代码里面使用 elif 的例子:

    \n
    x = 200
    if x < 20:
    print("x is less than 20")
    elif x == 200:
    print("x is equal to 200")
    else:
    print("x is above 20")
    \n

    这段代码将打印第二条语句,即 x is equal to 200

    \n

    现在我们把这段代码转换成一行代码:

    \n
    x = 200
    print("x is less than 20") if x < 20 else print("x is equal to 200") if x == 200 else print("x is above 20")
    \n

    这里使用的依然是上一个技巧,只是将其扩展为了多个条件,不过我不建议这样写,它也许很快就会变得难以阅读和维护。你需要权衡好什么时候使用这个技巧。

    \n

    字符串反转

    使用字符串切片操作,在一行代码中反转字符串:

    \n
    input_string = "Namaste World!"
    reversed_string = input_string[::-1]
    print("Reversed string is: ", reversed_string)

    Output:
    Reversed string is: !dlroW etsamaN
    \n

    分配多个变量

    在一行中为每个变量分配不同的值,甚至不同的数据类型:

    \n
    name, age, single = ‘jc’, 35, False
    \n

    列表推导

    列表推导是一种简单而优雅的方法,它可以从现有的列表中定义和生成新的列表:

    \n

    举个例子,生成填充了数字 0 到 4 的列表:

    \n
    scores = []
    for x in range (5):
    scores.append(x)
    print(scores)
    \n

    同样的结果我们可以用列表推导法来实现:

    \n
    scores = [x for x in range(5)]
    print(scores)
    \n

    这是 Python 最伟大的功能之一。

    \n

    列表推导中的条件式

    继续扩展上一个技巧,如果我们想根据一个条件来跳过一些项呢?

    \n

    例如,如果我们只想要奇数:

    \n
    scores = []
    for x in range (20):
    if x % 2 == 1:
    scores.append(x)
    print(scores)
    \n

    在列表推导中使用条件语句同样可以实现:

    \n
    scores = [x for x in range(20) if x % 2 == 1]
    print(scores)
    \n

    优点:列表推理不仅更清晰,而且在大部分情况下其性能也比单次循环好得多。

    \n

    斐波那契数列

    斐波那契数列是一组数字的集合,其中每个数字都是它前面两个数字之和。

    \n

    在一行代码中,我们使用列表推理和 for 循环生成一个斐波那契数列:

    \n
    n=10
    fib = [0,1]
    [fib.append(fib[-2]+fib[-1]) for _ in range(n)]
    print(fib)

    Output:
    [0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89]
    \n

    合并两个字典

    我们可以使用**操作符在一行代码中合并多个字典。

    \n

    我们只需要将字典和**操作符一起传给{},它就会为我们合并字典:

    \n
    dictionary1 = {"name": "Joy", "age": 25}
    dictionary2 = {"name": "Joy", "city": "New York"}
    merged_dict = {**dictionary1, **dictionary2}
    print("Merged dictionary:", merged_dict)

    Output:
    Merged dictionary: {'name': 'Joy', 'age': 25, 'city': 'New York'}
    \n

    交换一个字典中的键和值

    dict = {'Name': 'Joy', 'Age': 25, 'Language':'Python'}
    result = {v:k for k, v in dict.items()}
    print(result)
    Output:
    {'Joy': 'Name', 25: 'Age', 'Python': 'Language'}
    \n

    这个交换键值对的代码非常实用。

    \n

    交换变量

    在其他语言中,交换两个变量需要借助第三个变量(一个临时变量)来实现:

    \n
    tmp = var1
    var1 = var2
    var2 = tmp
    \n

    在Python中,可以直接在一条语句中完成:

    \n
    var1, var2 = var2, var1
    \n

    甚至更进一步,可以使用相同的技巧来交换数组中的元素

    \n
    colors = ['red', 'green', 'blue']
    colors[0], colors[1] = colors[1], colors[0]
    print(colors)

    Output:
    ['green', 'red', 'blue']
    \n

    列表推理中的嵌套循环

    列表推理还可以用在矩阵(多维数组)上:

    \n
    my_list = [(x, y) for x in [3, 4, 6] for y in [3, 4, 7] if x != y]
    print(my_list)

    Output:
    [(3, 4), (3, 7), (4, 3), (4, 7), (6, 3), (6, 4), (6, 7)]
    \n

    字典推理

    与列表推理的概念相同,举个例子,我们需要一个键/值对,其中值是键的平方:

    \n
    square_dict = dict()
    for num in range(1, 11):
    square_dict[num] = num * num
    print(square_dict)
    \n

    下面使用字典推导:

    \n
    square_dict = { num: num * num for num in range(1, 11) }
    print(square_dict)
    \n

    拍平一个列表

    数据工程师经常与列表和多维数据打交道,有时他们需要将多维列表转换成一维的。他们经常使用 numpy 之类的包来做这件事。

    \n

    下面的例子展示了如何使用纯 Python 的单行代码来完成同样的工作:

    \n
    my_list = [[1,2], [4, 6], [8, 10]]
    flattened = [i for j in my_list for i in j]
    print(flattened)

    Output:
    [1, 2, 4, 6, 8, 10]
    \n

    是的,这依然时列表推导的一种应用。

    \n

    从列表中解构变量

    假设你有一个列表,你想把它前边的几个值捕捉到变量中,其余值都放进另一个列表。这在处理参数的时候会很有用。

    \n

    让我们看一个例子:

    \n
    x, y, *z = [1, 2, 3, 4, 5]
    print(x, y, z)

    Output:
    1 2 [3, 4, 5]
    \n

    将文件加载到一个列表中

    脚本最常用的一个场景是处理文本文件,特别是将文件的每一行读入到列表中,这样我们就可以对数据进行我们需要的操作了。

    \n

    在 Python 中,我们可以用强大的列表推导法将文件所有行读入一个列表:

    \n
    my_list = [line.strip() for line in open('countries.txt', 'r')]
    print(my_list)
    \n

    总结

    Python 是一门神奇的语言。今天我展示了几个强大的 Python 技巧,它们将帮助我们开发更优雅、更简单、更高效的代码。

    \n"},{"title":"对配置中心进行优化","url":"/2017/optimization-config-server/","content":"

    现在我们的配置中心使用 Spring Cloud BusSpring Cloud Config 的整合,并以 RabbitMQ 作为消息代理,实现了应用配置的动态更新。

    \n

    架构如下图所示:

    \n

    \"\"

    \n

    但是,现在的架构有个很大的缺陷,就是每次在修改配置文件后,需要手动地触发下应用的 /bus/refresh 接口,才能完成更新操作。假如我们后端有上百个不同的服务在运行的话,手动去更新简直就是灾难,更新某一个应用时,需要先查到他的 IP + 端口号。而且如果同时修改了很多服务的配置的话,一个一个去发更新请求就有些太痛苦了。

    \n

    解决这个问题的办法是借助 GitLab 的 Webhook 机制,让 GitLab 帮我们去发这个请求。Webhook 用于当 GitLab 上的项目有变化的时侯以 HTTP 接口的形式通知第三方。

    \n

    进入我们 GitLab 的 config-repo/app-a 仓库,在 Settings - Integrations 中可以对 Webhook 进行设置:URL 填写我们刷新配置的地址,触发器选择 Push events 就够了,然后直接保存。

    \n

    \"\"

    \n

    现在可以测试一下,修改配置后,不需要再手动访问 /bus/refresh 也能完成更新操作了。

    \n

    初步优化到这里就结束了,已经可以省去很多人力成本,简单来说就是服务的配置更新需要 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 那台机器刚好出了问题,这个时候其他的实例也就得不到更新了。

    \n

    其实这个时候依赖哪个节点都不合适,谁也不知道哪个节点在什么时候会挂掉,可能有人会想到,我可以给所有节点都发一遍请求,我来分析一下这样做的缺点:

    \n
      \n
    • 第一点是节点很多的时候,你需要配很多 Webhook
    • \n
    • 第二点是当每个实例收到消息后,都会通过 RabbitMQ 通知其他所有实例,这样做非常浪费资源而且消息总线的意义就不存在了
    • \n
    • 第三点是我们指定的实例会在收到更新请求的时候立刻更新配置,并通过异步的方式来通知其他实例,这时会导致我们节点间存在不对等性,从而增加集群内部的复杂度
    • \n
    • 第四如果我们需要对服务实例进行迁移,那么我们还要修改 Webhook 中的配置
    • \n
    \n

    所以我们需要做一些调整,让服务集群中的各个节点是对等的:我们在 Config Server 中也引入 Spring Cloud Bus,将配置服务端也加入到消息总线中来。/bus/refresh 请求不再发送到具体实例上,而是发送给 Config Server,并通过 destination 参数来指定需要更新配置的服务或实例。

    \n

    Config Server 项目的 build.gradle 中加入消息总线的依赖:

    \n

    compile('org.springframework.cloud:spring-cloud-starter-bus-amqp')

    \n

    然后修改 application.yml,加入

    \n
    management:
    security:
    enabled: false

    spring:
    ...
    rabbitmq:
    host: 172.24.8.100
    port: 5672
    username: admin
    password: admin
    \n

    然后在 app-a 的 GitLab 仓库中修改我们刚才设置的 Webhook,将地址改为:http://172.24.8.100:7020/bus/refresh?destination=app-a

    \n

    注意端口号已经变了,对应的是配置中心的端口,destination 的值是要刷新的服务名称,这样的话配置其他服务 Webhook 的时候,只需要修改这个名称就可以了。

    \n

    \"\"

    \n

    通过上面的改动,我们的服务实例就不需要再承担触发配置更新的职责。同时,对于Git的触发等配置都只需要针对 Config Server 即可,从而简化了集群上的一些维护工作。

    \n

    保存 Webhook 后再次修改 GitLab 上 app-a 的配置文件,提交修改后刷新页面看到结果已经变为最新的配置了。

    \n"},{"title":"《哈佛幸福课》让我受益的点","url":"/2023/positive-psychology/","content":"

    今天终于把哈佛幸福课的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

    允许自己为“人” (permission to be human)

    允许自己有缺点、犯错误,允许自己做人而不是神。在合理合法的范围内,对自己宽容一点。

    \n

    当经历感情创伤时,你会看着它说“我只是普通人,我很难过,真希望事情不是这样,但我接受它,就像接受重力定律一样,因为重力定律是一种物理本质,就像感情创伤是一种人性本质,允许为人。”

    \n

    好东西太多有时也不是好事

    过犹不及,多则劣,少则精。

    \n

    两首好歌同时放,就是噪音。

    \n

    留下自己真正想要的,扔掉并没有很想要的,就算它很珍贵。比物质充裕更能带来幸福的是时间充裕。

    \n

    少做点事,可以完成得更多。时间充裕的人,往往更容易获得幸福感。

    \n

    宁缺毋滥,简化与效率是以曲线形式存在。

    \n

    果断坚决,在适当的时候学会说”不”,弄清楚你究竟真正想做的东西,然后去做。

    \n

    你现在和未来所经营的一段亲密关系比世界任何事都重要

    甚至比问问题更重要,比考试更重要,比我们有多成功,多被人景仰更重要得多。

    \n

    最能给人幸福感的东西,是良好的人际关系。亲密关系比很多事情都重要,它会给人带去有治愈能力的爱和温暖。

    \n

    最成功恋情的四个特点:

    \n
      \n
    1. 经营爱情需要付出努力
    2. \n
    3. 我们需要被了解而不是被认可
    4. \n
    5. 爱情中冲突不可避免
        \n
      • 冲突要针对行为而不是针对人。
      • \n
      • 避免针对人身,认可本人,尽量赞赏对方,仅对其行为或是其想法观念不苟同。
      • \n
      • 尽量在私下才争吵。
      • \n
      • 可以有争执,但要将其保持在认知行为上而非情感的,感情的,蔑视的层面。
      • \n
      \n
    6. \n
    7. 积极认知,做优点感知者,多赞赏对方
    8. \n
    \n

    性在长久美好恋情中很重要。爱情,准确地说,性的至高点使爱具体化,使爱具体化。

    \n

    每周锻炼4次,每次30分钟

    基因决定的基准幸福水平,当我们不锻炼时,就像打了镇静剂。

    \n

    运动是一项对现在和未来的投资。

    \n

    我们必须和本性抗争,和本性抗争是很难的,提升我们幸福的水平是很难的,而同时要和本性抗争则是难以想象的困难。

    \n

    锻炼的好处:

      \n
    • 心理层面:增强自信自尊、减轻焦虑和压力、有助于临床精神疾病的辅助治疗、提高认知功能。
    • \n
    • 身体层面:减轻或保持体重、减少慢性病、更强大的免疫系统、更美妙的性生活。
    • \n
    \n

    其他提高幸福感的灵丹妙药还有:

      \n
    • 冥想、深呼吸、瑜伽
    • \n
    • 良好的睡眠
    • \n
    • 触摸、拥抱
        \n
      • 触摸有助于伤口愈合,有助于身体健康,增强免疫系统,改善性生活。
      • \n
      \n
    • \n
    \n

    被了解而非被证明

    从希望被认可变成希望被了解。一个人很多时候不是因为完美而被喜欢,是因为真实而被喜欢。因为真实而被喜欢,才是持久、轻松、可持续发展的。

    \n

    并不是说要完全去除我们依赖别人的自尊,而是明白更重要的是被了解;去表达自己,而非给他人留下印象。这样人生会变得更轻松,更简单。

    \n

    休息的重要性

    那些成功人士,一是他们有习惯,二是他们有恢复,有休息

    \n

    我们要转变对生活的理解:

    \n
      \n
    • 从马拉松运动员 变为 短跑运动员;
    • \n
    • 从不停地跑跑跑 变为 短跑,恢复,短跑,恢复。
    • \n
    \n

    心怀感激

    应心怀感激,不要等到不幸发生时才意识到。

    \n

    有很多好事值得我们感激,但我们都把它们习以为常,认为理所当然。例如我们把父母、朋友对我们的好视为理所当然。

    \n

    把感激培养成一种生活习惯,对身体的好处,包括心率变异性,它能预测我们是否能长寿,预测我们是否健康。当我们感激时,副交感神经系统功能增强,使我们变平静,从而加强免疫系统,当感激成为我们的性格。还有很多好处,所以感激不只上一种心情,也是一种性格。

    \n

    表达感激时我们感觉很好,对方也会感觉很好,他们的获益良多,于是你创造了一个双赢的局面,一个上升的螺旋。

    \n

    怎样培养感激?

    \n
      \n
    • 通过一次又一次的感激来培养
    • \n
    • 每天睡前写下 5 件让自己满意的事
    • \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

    我在喝怡宝这个水时确实喝出了甘冽都口感,这种口感会让我心情愉悦起来。我现在每天早上到公司后,会先用公司的咖啡机接一份意式浓缩(之前是直接接美式),然后兑上怡宝的纯净水,口感会好很多,也会好喝很多。

    \n


    \n

    P.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

    看了一下介绍,和我的想法相同,同时提供了便于扩展的参数,而且提供了各个平台编译好的可执行文件。

    \n

    安装

    Windowns 安装

    scoop install serve
    \n

    Mac 安装

    brew install philippgille/tap/serve
    \n

    Linux 安装

    wget https://download.jpanj.com/serve_v0.3.2_Linux_x64.zip
    unzip serve_v0.3.2_Linux_x64.zip .
    \n

    运行

    检查 serve 版本

    $ serve -v
    serve version: v0.3.2
    \n

    服务监听 8100 端口并将当前目录作为静态文件根目录

    $ serve -p 8100

    Serving "." on all network interfaces (0.0.0.0) on HTTP port: 8100

    Local network interfaces and their IP addresses so you can pass one to your colleagues:
    Interface | IPv4 Address | IPv6 Address
    ---------------------|-----------------|----------------------------------------
    lo | 127.0.0.1 | ::1
    eth0 | xxx.xxx.xx.xxx | fe80::a8aa:ff:fe11:fb4b
    docker0 | 172.17.0.1 | fe80::42:1dff:fe8b:c5fe
    br-556c0122836a | 172.24.0.1 | fe80::42:92ff:fe47:2965
    vethf6d94b9 | | fe80::dcdf:59ff:fe90:c6e3
    vethf64d185 | | fe80::286e:3eff:fe12:eaf9
    veth7259f52 | | fe80::34d3:83ff:fe97:612c
    veth0e57d69 | | fe80::c007:bff:fe1b:2b7b

    You probably want to share:
    http://xxx.xxx.xx.xxx:8100
    \n

    -d 指定文件根目录

    $ serve -p 8100 -d "/opt"
    \n

    -a 启用 Basic 认证

    $ serve -p 8100 -d "/opt" -a "test:test"
    \n
    \"\"
    \n\n

    -s 生成一个自签名证书(7天有效期),开启 https 协议

    $ serve -p 8100 -d "/opt" -a "test:test" -s
    \n

    -b 指定监听的网络接口,默认为 0.0.0.0

    $ serve -p 8100 -d "/opt" -a "test:test" -b "0.0.0.0"
    \n

    -h 查看帮助文档

    $ serve -h
    Usage of serve:
    -a string
    \tRequire basic authentication with the given credentials (e.g. -a "alice:secret")
    -b string
    \tBind to (listen on) a specific interface. "0.0.0.0" is for ALL interfaces. "localhost" disables access from other devices. (default "0.0.0.0")
    -d string
    \tThe directory of static files to host (default ".")
    -h\tPrint the usage
    -p string
    \tPort to serve on. 8080 by default for HTTP, 8443 for HTTPS (when using the -s flag) (default "8080")
    -s\tServe via HTTPS instead of HTTP. Creates a temporary self-signed certificate for localhost, 127.0.0.1, <hostname>.local, <hostname>.lan, <hostname>.home and the determined LAN IP address
    -t\tTest / dry run (just prints the interface table)
    -v\tPrint the version
    \n"},{"title":"RAID 介绍,为什么不推荐为 HDFS 配置 RAID?","url":"/2019/raid-and-hdfs/","content":"

    RAID

    开始前先来思考一个问题,如果一个文件的大小超过了一块磁盘的大小,该如何存储?

    \n

    独立硬盘冗余阵列(RAID, Redundant Array of Independent Disks),简称磁盘阵列,利用虚拟化存储技术把多个磁盘组合起来,成为一个或多个磁盘阵列组,目的为提升性能或数据冗余,或是两者同时提升。

    \n

    简单来说,RAID 把多个磁盘组合成为一个逻辑磁盘,因此,操作系统只会把它当作一个实体磁盘。

    \n

    常见 RAID 等级

    RAID 0

    \"\"

    \n

    假设服务器有 N 块磁盘,RAID 0 是数据在从内存缓冲区写入磁盘时,根据磁盘数量将数据分成 N 份,这些数据同时并发写入 N 块磁盘,使得数据整体写入速度是一块磁盘的 N 倍;读取的时候也一样,所以在所有的级别中,RAID 0 的速度是最快的

    \n

    但是 RAID 0 不做数据备份,N 块磁盘中只要有一块损坏,数据完整性就被破坏,其他磁盘的数据也都无法使用了。

    \n

    RAID 1

    \"\"

    \n

    RAID 1 是数据在写入磁盘时,将一份数据同时写入两块磁盘,这样任何一块磁盘损坏都不会导致数据丢失,插入一块新磁盘就可以通过复制数据的方式自动修复,具有极高的可靠性,RAID 1 的数据安全性在所有的 RAID 级别上来说是最好的。但无论用多少磁盘做 RAID 1,仅算一个磁盘的容量,是所有 RAID 中磁盘利用率最低的一个级别。

    \n

    RAID 1 在一些多线程操作系统中能有很好的读取速度,理论上读取速度等于磁盘数量的倍数,与 RAID 0 相同。写入速度有微小的降低。

    \n

    RAID 10

    \"\"

    \n

    结合 RAID 0RAID 1 两种方案构成了 RAID 10,它是将所有磁盘 N 平均分成两份,数据同时在两份磁盘写入,相当于 RAID 1;但是平分成两份,在每一份磁盘(也就是 N/2 块磁盘)里面,利用 RAID 0 技术并发读写,这样既提高可靠性又改善性能。不过 RAID 10 的磁盘利用率较低,有一半的磁盘用来写备份数据。

    \n

    RAID 3

    \"\"

    \n

    RAID 3 可以在数据写入磁盘的时候,将数据分成 N-1 份,并发写入 N-1 块磁盘,并在第 N 块磁盘记录校验数据,这样任何一块磁盘损坏(包括校验数据磁盘),都可以利用其他 N-1 块磁盘的数据修复。

    \n

    由于数据内的比特分散在不同的磁盘上,因此就算要读取一小段数据资料都可能需要所有的磁盘进行工作,所以这种规格比较适于读取大量数据时使用。

    \n

    在数据修改较多的场景中,任何磁盘数据的修改,都会导致第 N 块磁盘重写校验数据。频繁写入的后果是第 N 块磁盘比其他磁盘更容易损坏,需要频繁更换,所以 RAID 3 很少在实践中使用。

    \n

    RAID 5

    \"\"

    \n

    相比 RAID 3RAID 5 是使用更多的方案。RAID 5RAID 3 很相似,但是校验数据不是写入第 N 块磁盘,而是螺旋式地写入所有磁盘中。这样校验数据的修改也被平均到所有磁盘上,避免 RAID 3 频繁写坏一块磁盘的情况。

    \n

    RAID 5 至少需要三块磁盘,RAID 5 不是对存储的数据进行备份,而是把数据和相对应的奇偶校验信息存储到组成 RAID 5 的各个磁盘上,并且奇偶校验信息和相对应的数据分别存储于不同的磁盘上。当 RAID 5 的一个磁盘数据发生损坏后,可以利用剩下的数据和相应的奇偶校验信息去恢复被损坏的数据。RAID 5 可以理解为是 RAID 0RAID 1 的折衷方案。

    \n

    RAID 6

    \"\"

    \n

    如果数据需要很高的可靠性,在出现同时损坏两块磁盘的情况下(或者运维管理水平比较落后,坏了一块磁盘但是迟迟没有更换,导致又坏了一块磁盘),仍然需要修复数据,这时候可以使用 RAID 6

    \n

    RAID 5 相比 RAID 6 增加第二个独立的奇偶校验信息块。两个独立的奇偶系统使用不同的算法,数据的可靠性非常高,任意两块磁盘同时失效时不会影响数据完整性。

    \n

    各种 RAID 技术比较

    \n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n
    RAID类型访问速度数据可靠性磁盘利用率目的
    RAID 0很快很低100%追求最大容量、速度
    RAID 1很慢很高50%追求最大安全性
    RAID 10中等很高50%总和 RAID 0/1 优点,理论速度较快
    RAID 5较快很高(N-1)/N追求最大容量、最小预算
    RAID 6较快较 RAID 5 高(N-2)/N同 RAID 5,但更安全
    \n

    HDFS

    RAID 可以看作是一种垂直伸缩,一台计算机集成更多的磁盘实现数据更大规模、更安全可靠的存储以及更快的访问速度。而 HDFS 则是水平伸缩,通过添加更多的服务器实现数据更大、更快、更安全存储与访问。

    \n

    Hadoop 分布式文件系统 HDFS 的设计目标是管理数以千计的服务器、数以万计的磁盘,将这么大规模的服务器计算资源当作一个单一的存储系统进行管理,对应用程序提供数以 PB 计的存储容量,让应用程序像使用普通文件系统一样存储大规模的文件数据。

    \n

    为什么不推荐为 HDFS 配置 RAID?

    HDFS 已经为同一个文件保留了多个副本,如果磁盘发生故障 HDFS 可以将其恢复。HDFS 同样可以一次从多个节点(DataNode)读取数据,如果使用 RAID 1,将浪费更多的存储空间,如果使用 RAID 0,产生失败的可能性会提升 N 倍(N = 磁盘数量)。使用 RAID 5/6 的话,读写速度将受到影响,也更昂贵。

    \n

    不过由于 NameNodeHDFS 中容易出现单点故障,因此需要更可靠的硬件配置,所以建议在 NameNode 上使用 RAID

    \n

    其他说明

    在一些 Master 处理节点上,如:Hive 的 MetaStore,也推荐使用 RAID。同时建议为所有的系统盘配置 RAID,你不会希望仅仅因为系统盘故障而导致节点故障。

    \n

    对于 ElasticSearch,其本身也提供了很好的 HA 机制,同样无需使用 RAID

    \n

    最后

    回到开头的那个问题,我的回答是:

    \n
      \n
    • 单机时代:RAID
    • \n
    • 分布式时代:分布式文件系统
    • \n
    \n"},{"title":"《人间值得》摘抄","url":"/2022/read-notes-life-is-worth-living/","content":"

    \n

    第 1 章:工作是为了什么

    从本质上来说,人就是为了生活而工作。

    \n

    在我看来,为了钱而工作,并不是可耻的事情,这是理所当然的事,我认为是非常了不起的。

    \n

    赚多少钱倒没那么重要,如果能够支撑自己和家人的日常生活,这就足够了。人生,就是这样活着而已。

    \n

    至于“人生价值”“自我成长”之类,是要等自己立足安稳后,在闲暇之余慢慢思考的问题。人生很长,慢慢思考就好了。

    \n

    有些人痛苦地向我倾诉,说自己“在现在的公司没有发展”,或者“失去了工作的目标”。我认为,这些可能都是想得太多的缘故。

    \n

    如果被权力、地位、名誉之类的东西紧紧束缚住,在工作中一味地在意别人的眼光,很快就会疲于应对。要是这般勉强度过几十年,迟早会被工作击垮。

    \n

    我不关心头衔和职位,这些都如过眼云烟。如果自己和家人健康、精力充沛,有几个知己可以交谈,还有什么其他奢望呢?

    \n

    相反,如果你拼命工作,身体状况因此变得糟糕,自己和家人关系疏远,即使你挣了很多钱,那又有什么幸福可言?

    \n

    不要把自我价值全部建立在工作上,带着“为身边的人略尽绵力”的想法去工作,或许会更好。

    \n

    工作中的人际关系比工作内容重要得多。从我的经验来看,不喜欢工作的大多数是人际关系出了问题。对有些人而言,不管他们做什么工作,他们都讨厌工作,这也许是与人交往上出现了问题。

    \n

    过多的“空闲”,有时会带来负面影响,“适当忙碌”的状态反而更好。

    \n

    如果工作让你一直做出巨大的牺牲,那一定要果断离开,毫不犹豫。

    \n

    我并不提倡过度工作,甚至过劳死。公司不过是“别人赚钱的工具”,如果这个工具紧紧地束缚住了自己的宝贵生命或家人的幸福,那么逃离也无妨。一旦决定“逃离”,你应该自信地离开。

    \n

    习惯遇事不抱怨,依靠自己解决,无论发生什么事情,你都能想办法解决。

    \n

    如果工作让你一直做出巨大的牺牲,那一定要果断离开,毫不犹豫。

    \n

    第 2 章:不要期待过多,对生活中的小事心存感激

    过于强调“应该如此”而拼命努力,多半是因为欲望过高。此外,欲望过高的本质,或是“想被人称赞自己努力上进真厉害”

    \n

    在人生中,很多事情不会按照你的想法发生,这会让我们感觉很痛苦。

    \n

    人生不可思议之处在于,即使去了新环境,也会遇到讨厌的人、合不来的人,尽管程度不同,但或多或少都会出现。

    \n

    不要试图通过改变他人来获得快乐,而是想“自己如何做才会快乐”或“怎么努力让自己在这里心情愉快地度过”,我觉得这才是应该考虑的关键。

    \n

    人生的本质就是一个人活着。

    \n

    人际关系是无法预测的。人与人之间可能因为一些小事而结缘,也会因一些小事分离。人会快速地向着有利于自己的方向前行,由于时间或距离的原因不能见面,缘分也会渐渐变浅。这就是人际关系。

    \n

    “情”这个东西看起来是一件好事,但从另一方面来说,它会让你对别人产生期待、执着,让你在关系中变得“自私”。

    \n

    不要有太多的期望。

    \n

    只要是别人给予的东西,自己就应该感谢对方

    \n

    在一生中,任何人都会遇到几次大的转折点,也就是人生的十字路口。

    \n

    总想着得失,那么就会觉得勉强自己,甚至产生心结。与其如此,还不如率性而为,跟随心的决定。

    \n

    第 3 章:恰到好处的人际关系

    即使你不能给出建议,没有提供令人豁然开朗的方法,就是简简单单设身处地地倾听,对方也会轻松许多。

    \n

    如果被人说了不好的话,就不妨想想“那家伙在家里有什么惹他烦心的事吗?”,也许心情就会好转。

    \n

    无论是费劲地想要主动交往,还是试图引起对方的关注,都显得不自然、不正常。

    \n

    当你想到“自己这么努力,为什么没有得到回报”,也许对待别人就会变得苛刻。

    \n

    第 4 章:让心归于平静

    人为什么会感到不安?大多数情况下,这种不安是因为对未来考虑太多。

    \n

    我认为,只要想清楚今天一天的事情就可以了。

    \n

    任何事物都有两面性,痛苦的经历可以扩展人的本性,就像肌肉可以锻炼、拉伸一样。

    \n

    如果总也不顺利,那么你就要意识到,“人生本来就是这样”。

    \n

    保持心平气和的另一个有效方法,就是“工作时间以外,不考虑工作上的事”

    \n

    在非工作时间,尽量不要考虑工作上的事。

    \n

    自信绝非一成不变的,我们只能在某一段时间或某一领域经历它。

    \n

    总而言之,不被负面情绪影响的最大秘诀就是好好生活

    \n

    痛苦与伤心,其实也是与生俱来的东西。人活着,肯定会经历苦难。

    \n

    不要事事都想咬紧牙关挺过去,只要抱着“今天这样做基本就可以了”的态度,日复一日地坚持积累。

    \n

    第 5 章:生活和工作的平衡之道

    在我看来,与其追求完美而挫折不断,不如以笨拙的方式坚持下去。

    \n

    如果母亲的情绪不稳定,孩子的精神状态就会受到影响。

    \n

    父母的心情会扰乱孩子的内心,孩子的波动反过来又会反弹给父母。

    \n

    有多种选择的时候我们往往左右瞻顾,当“只有一个选择”的时候,反而会意外地突破现状。

    \n

    生活如果没有目标,就会变得懒散。一旦决定“今天这样做”,生活一下子就会张弛有度。

    \n

    家庭问题能忍耐就忍耐,工作方面能放松就放松。

    \n

    我非常推荐大家和同事一起出去玩,你可以发现同事在工作之外的真性情,也可以与趣味相投的人成为好朋友。

    \n

    提醒别人的事情,自己如果做不到,更加不好。即使是孩子,也会看穿大人的一言一行。因此,要想改变孩子,首先得改变自己。这样,通过育儿,也会注意到自己一些为人处事的方式。

    \n

    育儿基本的原则是,对待大人和孩子一视同仁。

    \n

    孩子成长的每个过程,比任何一部动漫或电影都令人感动。

    \n

    最关键的是,父母应该真心为孩子的幸福考虑,并付诸行动。这样做,才能将爱传递给孩子

    \n

    父母和孩子的人生车轮虽然驶向不同的方向,由于桥梁的存在,你们可以随时往来。

    \n

    担心死亡来临、提前做好计划终究无济于事。把最基本的要求告诉家人,其余的事情顺其自然就可以。

    \n

    第 6 章:简单生活每一天

    在追求的过程中,一定要分清自己是自己,他人在实践他人的人生,我们不需要追寻别人的脚步。

    \n

    越是对别人讨厌、反感,这些情绪就越容易在自己的表情和态度上反映出来,进而传达给对方。

    \n

    由于过于害怕孤独,就会迎合别人,或者对别人妥协,从而使自己痛苦不已。

    \n

    若想人际关系变好,就更应该珍惜一个人的时光。也许,这才是最根本、最重要的事情。

    \n

    “对患者来说,能接待他们的医生很多;但对孩子而言,母亲只有一个

    \n

    和周围的人交往要保持适当的距离,这是维系和谐关系的关键。我们也是有感情的人,在不知不觉中平衡就会被打破。
    或许是对别人期待太多,或许是对自己太过严苛,总之与他人交往总会有感觉不舒服的时候。

    \n

    人生的满足感并非由别人决定,也绝不应该追求和别人同样的生活。

    \n"},{"title":"【阅读笔记】微服务架构设计模式—第1章","url":"/2022/read-notes-microservices-patterns-1/","content":"

    软件架构对功能性需求的影响

    软件架构对功能性需求影响并不大,架构的重要性在于它影响了应用的非功能性需求,

    \n

    扩展立方体

    \"20220701140535.png\"
    之前只听过垂直扩容和水平扩容,本书中将微服务比喻成一个立方体,X、Y、Z 三个轴表示对应用扩展的 3 种方式:

    \n
      \n
    • X 轴扩展和我们之前了解的水平扩容意思相同
        \n
      • \"20220701140557.png\"
      • \n
      \n
    • \n
    • Z 轴扩展表示根据请求属性(如 userId)进行路由,作者称之为流量分区
        \n
      • \"20220701140610.png\"
      • \n
      \n
    • \n
    • Y 轴表示根据功能将请求路由到不同的服务,如订单服务、商品服务
        \n
      • \"20220701140624.png\"
      • \n
      \n
    • \n
    \n

    作者对微服务的定义

    把应用程序功能性分解为一组服务的架构风格。

    \n

    「两个披萨」原则

    「两个披萨」原则是指某个事情的参与人数不能多到两个披萨饼还不够他们吃饱的地步。亚马逊CEO贝索斯认为事实上并非参与人数越多越好,他认为人数多不利于决策的形成,并会提高沟通的成本,这被称为「两个披萨」原则。

    \n

    微服务好处

      \n
    • 持续交付、持续部署
    • \n
    • 容易维护
    • \n
    • 独立部署
    • \n
    • 独立扩展
    • \n
    • 团队自治
    • \n
    • 新技术
    • \n
    • 容错性
    • \n
    \n

    微服务弊端

      \n
    • 服务的拆分和定义
    • \n
    • 开发、测试和部署更困难
    • \n
    • 部署时需要协调更多开发团队
    • \n
    • 考虑什么阶段使用微服务架构
    • \n
    \n

    马拉法拉利比喻

    采用微服务架构以后,如果仍旧沿用瀑布式开发流程,那就跟用一匹马来拉法拉利跑车没什么区别。如果你希望通过微服务架构来完成一个应用程序的开发,那么采用类似 Scrum 或 Kanban 这类敏捷开发和部署实践就是必不可少的。

    \n

    人们对变化做出情绪化反应的三个阶段

      \n
    1. 结束、失落和放弃:当人们被告知某种变化,这类变化会把他们从舒适区中拉出,这类情绪开始滋生和蔓延。人们会念叨失去之前的种种好处。
    2. \n
    3. 中立区:处理新旧任务工作方式交替过程中,人们普遍会对新的工作方式无所适从。人们开始纠结并必须学习处理新工作的方式。
    4. \n
    5. 新的开始:最终阶段,人们开始发自内心地热情拥抱新的工作方式,并且开始体验到新工作方式所带来的种种好处。
    6. \n
    \n","tags":["读书笔记","微服务架构设计模式"]},{"title":"【阅读笔记】微服务架构设计模式—第2章","url":"/2022/read-notes-microservices-patterns-2/","content":"

    软件架构定义

    抽象定义:计算机系统的软件架构是构建这个系统所需要的一组结构,包括软件元素、它们之间的关系以及两者的属性。

    \n

    更容易理解的定义:应用程序的架构是将软件分解为元素(element)和这些元素之间的关系(relation)。

    \n

    4+1 视图模型

    应用程序的架构可以从多个视角来看:

    \n
      \n
    • 逻辑视图:开发人员创建的软件元素。
    • \n
    • 实现视图:构建编译系统的输出。
    • \n
    • 进程视图:运行时的组件。
    • \n
    • 部署视图:进程如何映射到机器。
    • \n
    • +1 是指场景,它负责把视图串联在一起。
    • \n
    \n

    \"20220712181316.png\"

    \n

    应用程序两个层面的需求

    功能性需求:决定一个应用程序做什么
    非功能性需求:决定一个应用程序在运行时的质量

    \n

    六边形架构风格

    六边形架构风格选择以业务逻辑为中心的方式组织逻辑视图。

    \n

    \"20220712181309.png\"

    \n

    出入站端口

      \n
    • 入站端口的一个实例是服务接口,它定义服务的公共方法。
    • \n
    • 出站端口是业务逻辑调用外部系统的方式。
    • \n
    \n

    出入站适配器

      \n
    • 入站适配器通过调用入站端口来处理来自外部世界的请求。
        \n
      • Spring MVC Controller
      • \n
      • 订阅消息的消息代理客户端
      • \n
      \n
    • \n
    • 出站适配器实现出站端口,并通过调用外部应用程序或服务处理来自业务逻辑的请求。
        \n
      • 访问数据库的操作的数据访问对象(DAO)类
      • \n
      • 调用远程服务的代理类
      • \n
      • 发布事件
      • \n
      \n
    • \n
    \n

    六边形架构风格的一个重要好处是它将业务逻辑与适配器中包含的表示层和数据访问层的逻辑分离开来。业务逻辑不依赖于表示层逻辑或数据访问层逻辑。

    \n

    松耦合

    松耦合服务是改善开发效率、提升可维护性和可测试性的关键。小的、松耦合的服务更容易被理解、修改和测试。

    \n

    保证数据的私有属性是实现松耦合的前提之一。

    \n

    服务的定义

    服务是一个单一的、可独立部署的软件组件,它实现了一些有用的功能。

    \n

    服务的 API 封装了其内部实现。

    \n

    微服务架构模式

    将应用程序构建为松耦合、可独立部署的一组服务。

    \n

    定义应用程序架构的三步式流程:

    \n
      \n
    1. 定义系统操作
        \n
      1. 创建由关键类组成的抽象领域模型
          \n
        • 用户故事中提及的名词
        • \n
        \n
      2. \n
      3. 确定系统操作
          \n
        • 用户故事中提及的动词
        • \n
        \n
      4. \n
      \n
    2. \n
    3. 定义分解服务
        \n
      • 方法 1:采用业务能力进行服务拆分
      • \n
      • 方法 2:根据子域进行服务拆分
      • \n
      \n
    4. \n
    5. 定义服务 API 和写作方式
    6. \n
    \n

    定义应用程序架构过程图

    \"20220712181300.png\"

    \n

    定义系统操作过程图

    \"20220712181240.png\"

    \n

    服务分解后的几个障碍

      \n
    1. 网络延迟
    2. \n
    3. 可用性
    4. \n
    5. 数据一致性
    6. \n
    7. 上帝类
    8. \n
    \n

    两类系统操作

      \n
    • 命令型:创建、更新或删除数据的系统操作。
    • \n
    • 查询型:查询和读取数据的系统操作。
    • \n
    \n

    根据子域进行服务拆分

    领域驱动为每一个子域定义单独的领域模型。

    \n
      \n
    • 子域是领域的一部分
    • \n
    • 领域是 DDD 中用来描述应用程序问题域的一个术语
    • \n
    \n

    DDD 把领域模型的边界称为限界上下文(bounded context)。

    \n

    我们可以通过 DDD 的方式定义子域,并把子域对应为每一个服务,这样就完成了微服务架构的设计工作。

    \n

    微服务架构应遵循单一职责原则和闭包原则:

      \n
    • 单一职责原则:设计小的、内聚的、仅仅含有单一职责的服务。这会缩小服务的大小并提升它的稳定性。
    • \n
    • 闭包原则:把根据同样原因进行变化的服务放在一个组件内。这样做可以控制服务的数量,当需求发生变化时,变更和部署也更加容易。理想情况下,一个变更只会影响一个团队和一个服务。
    • \n
    \n","tags":["读书笔记","微服务架构设计模式"]},{"title":"【阅读笔记】微服务架构设计模式—第3章","url":"/2022/read-notes-microservices-patterns-3/","content":"

    一个理想的微服务架构应该是在内部由松散耦合的若干服务组成,这些服务使用异步消息相互通信。REST 等同步协议主要用于服务与外部其他应用程序的通信。

    \n

    考虑交互方式将有助于你专注于需求,并避免陷人特定进程间通信技术的细节。

    \n

    交互方式的选择会影响应用程序的可用性,交互方式还可以帮助你选择更合适的集成测试策略。

    \n

    有多种客户端与服务的交互方式

    它们可以分为两个维度。

    \n

    第一个维度关注的是一对一和一对多:

      \n
    • 一对一:每个客户端请求由一个服务实例来处理。
    • \n
    • 一对多:每个客户端请求由多个服务实例来处理。
    • \n
    \n

    一对一的交互方式有以下几种类型:

    \n
      \n
    • 请求/响应:一个客户端向服务端发起请求,等待响应;客户端期望服务端很快就会发送响应。
        \n
      • 在一个基于线程的应用中,等待过程可能造成线程阻塞。这样的方式会导致服务的紧耦合
      • \n
      \n
    • \n
    • 异步请求/响应:客户端发送请求到服务端,服务端异步响应请求。
        \n
      • 客户端在等待响应时不会阻塞线程,因为服务端的响应不会马上就返回。
      • \n
      \n
    • \n
    • 单向通知:客户端的请求发送到服务端,但是并不期望服务端做出任何响应。
    • \n
    \n

    一对多的交互方式有以下几种类型:

    \n
      \n
    • 发布 / 订阅方式:客户端发布通知消息,被零个或者多个感兴趣的服务订阅。
    • \n
    • 发布 / 异步响应方式:客户端发布请求消息,然后等待从感兴趣的服务发回的响应。
    • \n
    \n

    第二个维度关注的是同步和异步:

      \n
    • 同步模式:客户端请求需要服务端实时响应,客户端等待响应时可能导致堵塞。
    • \n
    • 异步模式:客户端请求不会阻塞进程,服务端的响应可以是非实时的。
    • \n
    \n

    \"20220906181622.png\"

    \n

    一个设计良好的接口会在暴露有用功能同时隐藏实现的细节。

    \n

    „„你应该努力只进行向后兼容的更改。向后兼容的更改是对 API 的附加更改或功能增强:

    \n
      \n
    • 添加可选属性。
    • \n
    • 向响应添加属性。
    • \n
    • 添加新操作。
    • \n
    \n

    「严以律己,宽以待人」:服务应该为缺少的请求属性提供默认值;客户端应忽略任何额外的响应属性。

    \n
    \n

    进程间通信的本质是交换消息。消息通常包括数据,因此一个重要的设计决策就是这些数据的格式。消息格式的选择会对进程间通信的效率、API 的可用性和可演化性产生影响。

    \n

    消息的格式可以分为两大类:文本和二进制。

    \n
      \n
    • 第一类是 JSON 和 XML 这样的基于文本的格式。
        \n
      • 这类消息格式的好处在于,它们的可读性很高,同时也是自描述的。
          \n
        • JSON 消息是命名属性的集合。
        • \n
        • 相似地,XML 消息也是命名属性的集合。
        • \n
        \n
      • \n
      • 使用基于文本格式消息的弊端主要是消息往往过度冗长,特别是 XML。另外一个弊端是解析文本引入的额外开销,尤其是在消息较大的时候。
      • \n
      \n
    • \n
    • 有几种不同的二进制格式可供选择。常用的包括 Protocol Buffers) 和 Avro
    • \n
    • Protocol Buffers 使用 tagged fields(带标记的字段),
    • \n
    • Avro 的消费者在解析消息之前需要知道它的格式。
    • \n
    • 因此,实行 API 的版本升级演进, Protocol Buffer 要优于 Avro
    • \n
    \n

    REST 是一种使用 HTTP 协议的进程间通信机制

    \n
      \n
    • REST 中的一个关键概念是资源,它通常表示单个业务对象。
    • \n
    • REST 使用 HTTP 动词来操作资源,使用 URL 引用这些资源。
    • \n
    \n

    REST 成熟度模型

      \n
    • Level 0:Level 0 层级服务的客户端只是向服务端点发起 HTTP POST 请求,进行服务调用。每个请求都指明了需要执行的操作、这个操作针对的目标(例如,业务对象) 和必要的参数。
    • \n
    • Level 1: Level 1 层级的服务引入了资源的概念。要执行对资源的操作,客户端需要发出指定要执行的操作和包含任何参数的 POST 请求。
    • \n
    • Level 2:Level 2 层级的服务使用 HTTP 动词来执行操作,譬如 GET 表示获取、 POST 表示创建、PUT 表示更新。请求查询参数和主体(如果有的话)指定操作的参数。这让服务能够借助 Web 基础设施服务,例如通过 CDN 来缓存 GET 请求。
    • \n
    • Level 3: Level 3 层级的服务基于 HATEOAS (Hypertext As The Engine Of Application State)原则设计,基本思想是在由 GET 请求返回的资源信息中包含链接,这些链接能够执行该资源允许的操作。例如,客户端通过订单资源中包含的链接取消某一订单,或者发送 GET 请求去获取该订单,等等。HATEOAS 的优点包括无须在客户端代码中写入硬链接的 URL。此外,由于资源信息中包含可允许操作的链接,客户端无须猜测在资源的当前状态下执行何种操作。
    • \n
    \n

    REST 最初没有 IDL。幸运的是,开发者社区重新发现了 RESTful API 的 IDL 价值。最流行的 REST IDL 是 Open API 规范

    \n
    \n

    REST 好处和弊端

    好处

      \n
    • 它非常简单,并且大家都很熟悉。
    • \n
    • 可以使用浏览器扩展(比如 Postman 插件)或者 curl 之类的命令行(假设使用的是 JSON 或其他文本格式)来测试 HTTP API。
    • \n
    • 直接支持请求 /响应方式的通信。
    • \n
    • HTTP 对防火墙友好。
    • \n
    • 不需要中间代理,简化了系统架构。
    • \n
    \n

    弊端

      \n
    • 它只支持请求 /响应方式的通信。
    • \n
    • 可能导致可用性降低。
        \n
      • 由于客户端和服务直接通信而没有代理来缓冲消息,因此它们必须在 REST API 调用期间都保持在线。
      • \n
      \n
    • \n
    • 客户端必须知道服务实例的位置(URL)。
        \n
      • 这是现代应用程序中的一个重要问题。客户端必须使用所谓的服务发现机制来定位服务实例。
      • \n
      \n
    • \n
    • 在单个请求中获取多个资源具有挑战性。
    • \n
    • 有时很难将多个更新操作映射到 HTTP 动词。
        \n
      • 考虑 gRPC
      • \n
      \n
    • \n
    \n

    gRPC API 由一个或多个服务和请求/响应消息定义组成。服务定义类似于 Java 接口,是强类型方法的集合。除了支持简单的请求 /响应 RPC 之外,gRPC 还支持流式 RPC。

    \n

    gRPC 使用 Protocol Buffers 作为消息格式。

    \n
      \n
    • Protocol Buffers 是一种高效且紧凑的二进制格式。
    • \n
    • 它是一种标记格式:Protocol Buffers 消息的每个字段都有编号,并且有一个类型代码。消息接收方可以提取所需的字段,并跳过它无法识别的字段。因此,gRPC 使 API 能够在保持向后兼容的同时进行变更。
    • \n
    \n

    gRPC 好处和弊端

    好处

      \n
    • 设计具有复杂更新操作的 API 非常简单。
    • \n
    • 它具有高效、紧凑的进程间通信机制,尤其是在交换大量消息时。
    • \n
    • 支持在远程过程调用和消息传递过程中使用双向流式消息方式。
    • \n
    • 它实现了客户端和用各种语言编写的服务端之间的互操作性。
    • \n
    \n

    弊端

      \n
    • 与基于 REST/JSON 的 API 机制相比,JavaScript 客户端使用基于 gRPC 的 API 需要做更多的工作。
    • \n
    • 旧式防火墙可能不支持 HTTP/2。
    • \n
    \n
    \n

    服务保护自己的方法包括以下机制的组合:

    \n
      \n
    • 网络超时:在等待针对请求的响应时,一定不要做成无限阻塞,而是要设定一个超时。使用超时可以保证不会一直在无响应的请求上浪费资源。
    • \n
    • 限制客户端向服务器发出请求的数量:把客户端能够向特定服务发起的请求设置一个上限,如果请求达到了这样的上限,很有可能发起更多的请求也无济于事,这时就应该让请求立刻失败。
    • \n
    • 断路器模式:监控客户端发出请求的成功和失败数量,如果失败的比例超过一定的阈值,就启动断路器,让后续的调用立刻失效。
        \n
      • 如果大量的请求都以失败而告终,这说明被调服务不可用,这样即使发起更多的调用也是无济于事。在经过一定的时间后,客户端应该继续尝试,如果调用成功,则解除断路器。
      • \n
      • 断路器是一个远程过程调用的代理,在连续失败次数超过指定阀值后的一段时间内,这个代理会立即拒绝其他调用。
      • \n
      \n
    • \n
    \n
    \n

    服务发现在概念上非常简单:其关键组件是服务注册表,它是包含服务实例网络位置信息的一个数据库。

    \n

    服务实例启动和停止时,服务发现机制会更新服务注册表。当客户端调用服务时,服务发现机制会查询服务注册表以获取可用服务实例的列表,并将请求路由到其中一个服务实例。

    \n

    实现服务发现有以下两种主要方式:

    \n
      \n
    • 应用层服务发现模式:服务及其客户直接与服务注册表交互。
        \n
      • 这种服务发现方法是两种模式的组合:
          \n
        • 第一种模式是自注册模式
            \n
          • 自注册:服务实例向服务注册表注册自己。
          • \n
          \n
        • \n
        • 第二种模式是客户端发现模式
            \n
          • 客户端发现:客户端从服务注册表检索可用服务实例的列表,并在它们之间进行负载平衡。
          • \n
          \n
        • \n
        \n
      • \n
      • 好处:
          \n
        • 可以处理多平台部署的问题
        • \n
        \n
      • \n
      • 弊端:
          \n
        • 需要为你使用的每种编程语言(可能还有框架)提供服务发现库
        • \n
        • 开发者负责设置和管理服务注册表,这会分散一定的精力
        • \n
        \n
      • \n
      \n
    • \n
    • 平台层服务发现模式:通过部署基础设施来处理服务发现。
        \n
      • 这种方法是以下两种模式的组合:
          \n
        • 第三方注册模式:由第三方负责(称为注册服务器,通常是部署平台的一部分)处理注册,而不是服务本身向服务注册表注册自己。
            \n
          • 第三方注册:服务实例由第三方自动注册到服务注册表
          • \n
          \n
        • \n
        • 服务端发现模式:客户端不再需要查询服务注册表,而是向 DNS 名称发出请求,对该 DNS 名称的请求被解析到路由器,路由器查询服务注册表并对请求进行负载均衡。
            \n
          • 服务端发现:客户端向路由器发出请求,路由器负责服务发现
          • \n
          \n
        • \n
        \n
      • \n
      • 好处:
          \n
        • 服务发现的所有方面都完全由部署平台处理
        • \n
        \n
      • \n
      • 弊端:
          \n
        • 仅限于支持使用该平台部署的服务
        • \n
        \n
      • \n
      \n
    • \n
    \n

    服务发现示例图

    应用层服务发现模式

    \"20220907100739.png\"

    \n

    平台层服务发现模式

    \"20220907100828.png\"

    \n
    \n

    有以下几种不同类型的消息:

    \n
      \n
    • 文档:仅包含数据的通用消息。接收者决定如何解释它。
        \n
      • 对命令式消息的回复是文档消息的一种使用场景。
      • \n
      \n
    • \n
    • 命令:一条等同于 RPC 请求的消息。它指定要调用的操作及其参数。
    • \n
    • 事件:表示发送方这一端发生了重要的事件。
        \n
      • 事件通常是领域事件,表示领域对象 (如 order 或 customer)的状态更改。
      • \n
      \n
    • \n
    \n

    有以下两种类型的消息通道:点对点发布-订阅

    \n
      \n
    • 点对点通道向正在从通道读取的一个消费者传递消息。
        \n
      • 服务使用点对点通道来实现前面描述的一对一交互方式。例如,命令式消息通常通过点对点通道发送。
      • \n
      \n
    • \n
    • 发布-订阅通道将一条消息发给所有订阅的接收方。
        \n
      • 服务使用发布一订阅通道来实现前面描述的一对多交互方式。例如,事件式消息通常通过发布-订阅通道发送。
      • \n
      \n
    • \n
    \n

    无代理消息 vs 基于代理的消息

    无代理消息

    好处:

    \n
      \n
    • 允许更轻的网络流量和更低的延迟,因为消息直接从发送方发送到接收方,而不必从发送方到消息代理,再从代理转发到接收方。
    • \n
    • 消除了消息代理可能成为性能瓶颈或单点故障的可能性。
    • \n
    • 具有较低的操作复杂性,因为不需要设置和维护消息代理。
    • \n
    \n

    弊端:

    \n
      \n
    • 服务需要了解彼此的位置,因此必须使用服务发现机制。
    • \n
    • 会导致可用性降低,因为在交换消息时,消息的发送方和接收方都必须同时在线。
    • \n
    • 在实现例如确保消息能够成功投递这些复杂功能时的挑战性更大。
    • \n
    \n

    基于代理的消息

    消息代理是所有消息的中介节点。

    \n

    好处:

    \n
      \n
    • 松耦合:客户端发起请求时只要发送给特定的通道即可,客户端完全不需要感知服务实例的情况,客户端不需要使用服务发现机制去获得服务实例的网络位置。
    • \n
    • 消息缓存:消息代理可以在消息被处理之前一直缓存消息。
        \n
      • 像 HTTP 这样的同步请求/ 响应协议,在交换数据时,发送方和接收方必须同时在线。然而,在使用消息机制的情况下,消息会在队列中缓存,直到它们被接收方处理。这就意味着,例如,即使订单处理系统暂时离线或不可用,在线商店仍旧能够接受客户的订单。订单消息将会在队列中缓存(并不会丢失)。
      • \n
      \n
    • \n
    • 灵活的通信:消息机制支持前面提到的所有交互方式。
    • \n
    • 明确的进程间通信:基于 RPC 的机制总是企图让远程服务调用跟本地调用看上去没什么区别(在客户端和服务端同时使用远程调用代理)。然而,因为物理定律(如服务器不可预计的硬件失效)和可能的局部故障,远程和本地调用还是大相径庭的。消息机制让这些差异交得很明确,这样程序员不会陷人一种“太平盛世”的错觉。
    • \n
    \n

    弊端:

    \n
      \n
    • 潜在的性能瓶颈:消息代理可能存在性能瓶颈。幸运的是,许多现代消息代理都支持高度的横向扩展。
    • \n
    • 潜在的单点故障:消息代理的高可用性至关重要,否则系统整体的可靠性将受到影响。幸运的是,大多数现代消息代理都是高可用的。
    • \n
    • 额外的操作复杂性:消息系统是一个必须独立安装、配置和运维的系统组件。
    • \n
    \n

    选择消息代理时,你需要考虑以下各种因素:

    \n
      \n
    • 支持的编程语言:你选择的消息代理应该支持尽可能多的编程语言。
    • \n
    • 支持的消息标准:消息代理是否支持多种消息标淮,比如 AMQP 和 STOMP,还是它仅支持专用的消息标准?
    • \n
    • 消息排序:消息代理是否能够保留消息的排序?
    • \n
    • 投递保证:消息代理提供什么样的消息投递保证?
    • \n
    • 持久性:消息是否持久化保存到磁盘并且能够在代理崩溃时恢复?
    • \n
    • 耐久性:如果接收方重新连接到消息代理,它是否会收到断开连接时发送的消息?
    • \n
    • 可扩展性:消息代理的可扩展性如何?
    • \n
    • 延迟:端到端是否有较大延迟?
    • \n
    • 竞争性(并发)接收方:消息代理是否支持竞争性接收方?
    • \n
    \n
    \n

    使用多个线程和服务实例来并发处理消息可以提高应用程序的吞吐量。但同时处理消息的挑战是确保每个消息只被处理一次,并且是按照它们发送的顺序来处理的。

    \n

    现代消息代理(如 Apache Kafka 和 AWS Kinesis) 使用的常见解决方案是使用分片(分区)通道。该解决方案分为三个部分。

    \n
      \n
    1. 分片通道由两个或多个分片组成,每个分片的行为类似于一个通道。
    2. \n
    3. 发送方在消息头部指定分片键,通常是任意字符串或字节序列。消息代理使用分片键将消息分配给特定的分片。例如,它可以通过计算分片键的散列来选择分片。
    4. \n
    5. 消息代理将接收方的多个实例组合在一起,并将它们视为相同的逻辑接收方。例如, Apache Kafka 使用术语消货者组。消息代理将每个分片分配给单个接收器。它在接收方启动和关闭时重新分配分片。
    6. \n
    \n

    \"20220908182530.png\"

    \n
    \n

    处理重复消息有以下两种不同的方法:

    \n
      \n
    • 编写幂等消息处理程序。
    • \n
    • 跟踪消息并丢弃重复项。
    • \n
    \n

    程序的幂等性,是指即使这个应用被相同输入参数多次重复调用时,也不会产生额外的效果。

    \n
      \n
    • 例如,取消一个已经被取消的订单,就是一个幂等性操作。
    • \n
    \n

    跟踪消息并丢弃重复消息方案:

    \n
      \n
    1. 消息接收方使用 message id 跟踪它已处理的消息并丢弃任何重复项。
    2. \n
    3. 在应用程序表,而不是专用表中记录 message id。
    4. \n
    \n
    \n

    服务通常需要在更新数据库的事务中发布消息。

    \n

    确保消息的可靠发送的机制:

    \n
      \n
    • 使用数据库表作为消息队列
    • \n
    • 通过轮询模式发布事件
    • \n
    • 使用事务日志拖尾模式发布事件
    • \n
    \n
    \n

    领域事件是聚合 (业务对象)在创建、更新或删除时触发的事件。服务使用 DomainEventPublisher 接口发布领域事件。

    \n

    如果你想最大化一个系统的可用性,就应该设法最小化系统的同步操作量。

    \n
      \n
    • 即:应该尽可能选择异步通信机制来处理服务之间的调用。
    • \n
    \n

    消除同步交互的方法:

    \n
      \n
    • 使用异步交互模式
    • \n
    • 复制数据
        \n
      • 弊端:
          \n
        • 有时候被复制的数据量巨大,会导致效率低下
        • \n
        • 复制数据并没有从根本上解决服务如何更新其他服务所拥有的数据这个问题
        • \n
        \n
      • \n
      \n
    • \n
    • 先返回响应,再完成处理
        \n
      1. 仅使用本地的数据来完成请求的验证。
      2. \n
      3. 更新数据库,包括向 OUTBOX 表插人消息。
      4. \n
      5. 向客户端返回响应。
      6. \n
      \n
    • \n
    \n","tags":["读书笔记","微服务架构设计模式"]},{"title":"【阅读笔记】微服务架构设计模式—第7章","url":"/2022/read-notes-microservices-patterns-7/","content":"

    两种查询模式

    在微服务架构中实现查询操作有两种不同的模式:

    \n
      \n
    • API 组合模式:这是最简单的方法,应尽可能使用。它的工作原理是让拥有数据的服务的客户端负责调用服务,并组合服务返回的查询结果。
    • \n
    • 命令查询职责隔离(CQRS)模式:它比 API 组合模式更强大,但也更复杂。它维护一个或多个视图数据库,其唯一目的是支持查询。
    • \n
    \n

    API 组合模式

    API 组合模式有两种类型的参与者

      \n
    • API 组合器:它通过查询数据提供方的服务来实现查询操作。
    • \n
    • 数据提供方服务:拥有查询返回的部分数据的服务。
    • \n
    \n

    API 组合模式是否可使用的几个因素:

      \n
    • 数据分区方式
    • \n
    • 拥有数据的服务公开 API 的功能
    • \n
    • 服务使用数据库的功能
    • \n
    \n

    担任 API 组合器的三个选择

      \n
    1. 由服务的客户端扮演 API 组合器的角色
    2. \n
    3. 实现应用程序外部 API 的 API Gateway 来扮演 API 组合器的角色
    4. \n
    5. 将 API 组合器实现为独立的服务
    6. \n
    \n

    API 组合器应尽可能地并行调用提供方服务,最大限度地缩短查询操作的响应时间。

    \n

    API 组合模式好处和弊端

    好处

      \n
    • 简单直观
    • \n
    \n

    弊端

      \n
    • 增加了额外的开销
    • \n
    • 带来可用性降低的风险
        \n
      • 提高可用性方案:返回缓存数据或不完整数据
      • \n
      \n
    • \n
    • 缺乏事务数据一致性
    • \n
    \n

    CQRS 模式

    CQRS 是命令查询职责隔离 (Command Query Responsibility Segregation) 的简称,它涉及隔离或问题的分隔。它将持久化数据模型和使用数据的模块分为两部分:命令端和查询端。

    \n
      \n
    • 命令端模块和数据模型实现创建、更新和删除操作(缩写为 CUD,例如:HTTP POST、PUT 和 DELETE)。
    • \n
    • 查询端模块和数据模型实现查询(例如 HTTP GET)。查询端通过订阅命令端发布的事件,使其数据模型与命令端数据模型保持同步。
    • \n
    \n

    \n

    CQRS 好处与弊端

    好处

      \n
    • 在微服务架构中高效地实现查询。
    • \n
    • 高效地实现多种不同的查询类型。
    • \n
    • 在基于事件湖源技术的应用程序中实现查询。
    • \n
    • 更进一步地实现问题隔离。
    • \n
    \n

    弊端

      \n
    • 更加复杂的架构。
    • \n
    • 处理数据复制导致的延迟。
    • \n
    \n

    CQRS 设计

    CQRS 视图模块包括由一个或多个查询操作组成的 APl。它通过订阅由一个或多个服务发布的事件来更新它的数据库视图,从而实现这些查询操作。

    \n

    \n
      \n
    • 数据访问模块实现数据库访问逻辑。
    • \n
    • 事件处理程序查询 API 模块使用数据访问模块来更新和查询数据库。
        \n
      • 事件处理程序模块订阅事件并更新数据库。
      • \n
      • 查询 API 模块负责实现查询 API。
      • \n
      \n
    • \n
    \n

    NoSQL

    NoSQL 数据库通常具有有限的事务模式和较少的查询功能。在一些情况下, NOSQL 数据库比 SQL 数据库更有优势,包括更灵活的数据模型以及更好的性能和可扩展性。

    \n

    NoSQL 数据库通常是 CQRS 视图的一个很好的选择,CQRS 可以利用它们的优势并忽略其弱点。CQRS 视图受益于 NoSQL 数据库更丰富的数据模型和性能。它不受 NoSQL 数据库事务处理能力的限制,因为 CQRS 只需要使用简单的事务并执行一组固定的查询即可。

    \n

    判断视图未及时更新的一个思路

    命令和查询模块 API 可以使客户端使用以下方法检测不一致性。

    \n
      \n
    1. 命令端操作将包含已发布事件的 ID 标记返回给客户端。
    2. \n
    3. 客户端把这个事件有关的 ID 传递给查询操作,如果该事件尚未更新视图,则返回查询错误。视图模块可以使用重复事件检测机制来实现这样的功能。
    4. \n
    \n

    检测重复事件

    OrderHistoryDaoDynamoDb DAO 可以使用名为 «aggregateType>><<aggregateId>> 的属性跟踪从每个聚合实例接收的事件,其值是接收到的最高事件 ID。如果属性存在且其值小于或等于事件 ID,则事件是重复的。

    \n

    增量式构建 CQRS视图

      \n
    1. 基于其先前的快照和自创建快照以来发生的事件,定期计算每个聚合实例的快照
    2. \n
    3. 使用快照和任何后续事件创建视图
    4. \n
    \n","tags":["读书笔记","微服务架构设计模式"]},{"title":"用一个现实世界中的例子介绍 Go 接口","url":"/2019/real-world-go-interface/","content":"

    \"\"

    \n

    假设我现在要用 Go 编写一个 Web 应用。在这个应用里,我要实现给用户发送消息的功能。我可以通过邮件或短信等方式来发送这条消息,这是一个完美的接口使用场景。

    \n

    在这个虚构的 Web 应用中,先来创建如下 main.go 文件:

    \n
    package main

    import "fmt"

    type User struct {
    \tName string
    \tEmail string
    }

    type UserNotifier interface {
    \tSendMessage(user *User, message string) error
    }

    func main() {
    \tuser := &User{"Panmax", "panmax@email.com"}

    \tfmt.Printf("Welcome %s\\n", user.Name)
    }
    \n

    这里的 User 结构体代表一个用户。

    \n

    可以看到我创建了只有一个函数 SendMessage()UserNotifier 接口。

    \n

    为了实现这个接口,我需要创建一个结构体来实现 SendMessage() 函数。

    \n
    type EmailNotifier struct {
    }

    func (notifier EmailNotifier) SendMessage(user *User, message string) error {
    \t_, err := fmt.Printf("Sending email to %s with content %s\\n", user.Name, message)
    \treturn err
    }
    \n

    正如你看到的,我创建了一个新的 EmailNotifier 结构体。然后我给这个结构体实现了 SendMessage() 方法。在这个例子中,EmailNotifier 只是简单打印了一条消息。在现实世界中你可能需要调用发送邮件的 API,比如 Mailgun

    \n

    到此,UserNotifier 接口已经实现了,就是这么简单。

    \n

    下一步要做的是使用 UserNotifier 接口为用户发送一份邮件。

    \n
    func main() {
    \tuser := User{"Panmax", "panmax@email.com"}
    \tfmt.Printf("Welcome %s\\n", user.Name)

    \tvar userNotifier UserNotifier
    \tuserNotifier = EmailNotifier{}
    \tuserNotifier.SendMessage(&user, "Interfaces all the way!")
    }
    \n

    运行这个程序,EmailNotifierSendMessage 方法被正确调用了。

    \n
    go build -o main main.go
    ./main
    Welcome Panmax
    Sending email to Panmax with content Interfaces all the way!
    \n

    下边我们来实现发送短信的接口。

    \n
    type SmsNotifier struct {
    }

    func (notifier SmsNotifier) SendMessage(user *User, message string) error {
    \t_, err := fmt.Printf("Sending SMS to %s with content %s\\n", user.Name, message)
    \treturn err
    }
    \n

    我们可以把 Notifier 放进用户结构体中,这样每个用户都有一个属于自己的 Notifier,是不是很酷。

    \n
    type User struct {
    \tName string
    \tEmail string
    \tNotifier UserNotifier
    }
    \n

    然后,我们向 User 结构体中添加一个便捷方法 notify(),这个方法使用 UserNotifier 接口给用户发送消息。

    \n
    func (user *User) notify(message string) error {
    \treturn user.Notifier.SendMessage(user, message)
    }
    \n

    最后,我在 main() 函数中创建两个用户,分别调用了它们的 notify() 方法。

    \n
    func main() {
    \tuser1 := User{"Dirk", "dirk@email.com", EmailNotifier{}}
    \tuser2 := User{"Justin", "bieber@email.com", SmsNotifier{}}

    \tuser1.notify("Welcome Email user!")
    \tuser2.notify("Welcome SMS user!")
    }
    \n

    最终结果正是我们所预期的:

    \n
    go build -o main main.go
    ./main
    Sending email to Panmax with content Welcome Email user!
    Sending SMS to Panmax with content Welcome SMS user!
    \n

    结语

    本文介绍了 Go 接口是如何工作的,同时用一个现实中简单的例子进行了演示。

    \n

    希望对你有所帮助。

    \n"},{"title":"最近一个月的「汉堡王」早餐心得分享","url":"/2022/recent-breakfast-burger-king/","content":"

    今天这篇博客和之前的有些不同,主要区别是这篇是我用语音写的。也就是把「飞书妙记」打开录音,之后转成文字整理出来。

    \n

    为什么要拿语音写呢?我之前写博客都是通过电脑编写,写的过程中会浏览一些其他网页,找找资料之类的,而且打字的话很容易打断思绪。所以我这次尝试用说的形式来整理一篇博客出来。

    \n

    用语音写的另一个原因是它会逼着我一直的去往下不停地去说,会尽量的不去中断,尽量的去保持思考状态,往外输出,也许还能提升我的口语表达能力。如果是打字的话,一会儿看看这个一会儿看看那个很容易被中断。

    \n

    使用语音录入完转文字后,再通过电脑把段落顺序做些调整,把口语化表达改为书面表达就可以发布了。我发现整理所花费的时间比说的时间还要长,录了 20 分钟,整理了 1 小时

    \n

    明天就是国庆节了,周围有小一半的同事请了假,我下午也请了半天假,所以利用中午的时间把这篇博客发完也就要下班了,祝大家国庆节快乐。

    \n

    这篇博客想聊一下我近一个月来如何解决上班时间早饭和午饭的问题。

    \n

    我们公司(望京 Soho)附近有很多餐厅、便利店,我看到公司对面有一个店面很大的「汉堡王」,我之前对汉堡王的印象或者说对汉堡的印象,是一个不是特别健康的食品。不过我依然抱着试一试的态度搜了一下他家的菜单,顺便看一看有没有优惠之类的,毕竟打工人还是想找一些经济实惠的东西解决温饱问题。

    \n

    经过我一番调查后发现,「汉堡王」有一个有 3 种会员:

    \n
      \n
    • 19.9 元: 早餐半价
    • \n
    • 9.9 元:每天可以 8 元买一杯咖啡
    • \n
    • 29.9 元:包括了上面两个优惠,还额外多一个每天减免一次是外卖送餐费用。
    • \n
    \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":"

    什么是 painc?

    \n

    Panic 是 Go 语言中一个内置函数,它会中断正常的控制流并开始 panic 流程。当函数 F 调用 panic 时,F 的执行停止,F 中的任何延迟函数(deferred function)都被正常执行,然后 F 返回给它的调用者。对于调用者来说,F 的行为就像对 panic 的调用。这个过程继续在堆栈中进行,直到当前 goroutine 中的所有函数都返回,这时程序就会崩溃。painc 可以通过直接调用 panic 函数来启动,也可以由运行时错误引起,如数组越界。

    \n
    \n

    简单地说,painc 使一个函数不执行其预期的流程,并可能导致整个程序退出。

    \n

    解决方案

    Go 原生提供了一些功能,可以帮助我们从这种情况下恢复。

    \n

    Defer

    Go 的 defer 语句安排了一个函数:这个函数在执行 defer 的函数返回之前立即运行。

    \n

    我们称 defer 调用的函数为:延迟函数

    \n
    // Contents 将文件内容以字符串形式返回。
    func Contents(filename string) (string, error) {
    f, err := os.Open(filename)
    if err != nil {
    return \"\", err
    }
    defer f.Close() // f.Close 将在函数完成后执行

    var result []byte
    buf := make([]byte, 100)
    for {
    n, err := f.Read(buf[0:])
    result = append(result, buf[0:n]...)
    if err != nil {
    if err == io.EOF {
    break
    }
    return \"\", err // 如果我们从这里返回,f 会被安全关闭
    }
    }
    return string(result), nil // 如果我们从这里返回,f 会也会被安全关闭
    }
    \n

    Recover

    panic 被调用时,它立即停止执行当前函数,并沿 goroutine 的堆栈运行所有延迟函数。

    \n

    recover 的调用会终止 panic,并返回传递给 panic 的参数。recover 只在延迟函数中有效,因为 panic 后唯一能够运行的代码在延迟函数中。

    \n
    func server(workChan <-chan *Work) {
    for work := range workChan {
    go safelyDo(work)
    }
    }

    func safelyDo(work *Work) {
    defer func() {
    if err := recover(); err != nil {
    log.Println(\"work failed:\", err)
    }
    }()
    do(work)
    }
    \n

    实现

    让我们来实现一个简单的数学函数,它可以将两个数字相除,如果分母是 0,就会 panic Divide by zero error!

    \n

    下边的函数检查分母的值,如果它是 0,就会 panic。

    \n
    func checkForError(y float64) { 
    if y == 0 {
    panic(\"Divident cannot be 0! Divide by 0 error.\")
    }
    }
    \n

    下边这个函数负责对提供的数字进行除法操作并返回,同时它使用上面定义的函数来检查分母是否为 0。

    \n

    由于 checkForError 会破坏流程,因此这个函数实现了recover()defer,以便在发生 panic 时返回 0。

    \n
    func safeDivision(x, y float64) float64 { 
    var returnValue float64
    defer func() {
    if err := recover(); err != nil {
    fmt.Println(\"Panic occured:\", err)
    fmt.Println(\"Returning safe values\")
    returnValue = 0 }
    }()
    checkForError(y)

    returnValue = x / y

    return returnValue
    }
    \n

    将上边的代码组合起来:

    \n
    package main

    import (
    \t\"fmt\"
    )

    func main() {
    \tfmt.Println(\"Pre panic execution\")
    \tvalue1 := safeDivision(2, 0)
    \tfmt.Println(\"Post panic execution, -> \", value1)
    \tfmt.Println(\"Pre valid execution\")
    \tvalue2 := safeDivision(2, 1)
    \tfmt.Println(\"Post valid execution, value -> \", value2)
    }

    func safeDivision(x, y float64) float64 {
    \tvar returnValue float64
    \tdefer func() {
    \t\tif err := recover(); err != nil {
    \t\t\tfmt.Println(\"Panic occured:\", err)
    \t\t\tfmt.Println(\"Returning safe values\")
    \t\t\treturnValue = 0
    \t\t}
    \t}()
    \tcheckForError(y)

    \treturnValue = x / y

    \treturn returnValue
    }

    func checkForError(y float64) {
    \tif y == 0 {
    \t\tpanic(\"Divident cannot be 0! Divide by 0 error.\")
    \t}
    }
    \n

    输出为:

    \n
    Pre panic execution
    Panic occured: Divident cannot be 0! Divide by 0 error.
    Returning safe values
    Post panic execution, -> 0
    Pre valid execution
    Post valid execution, value -> 2
    \n"},{"title":"Redis 开放端口与设置密码","url":"/2017/redis-open-port-and-set-password/","content":"

    最近自己的 VPS 上部署了几个 Docker 服务,其中一个打算用 Reids 做个计数的功能,因为我的偏好是数据库类的程序不用 Docker 来部署,所以在本地安装了 Redis 服务,但是这样如果不做任何配置的话 Docker 容器中的服务是访问不到宿主机的 Redis 服务的。

    \n

    从网上搜了下解决方法,都挺复杂的,需要配置网桥之类的,所以我就走了个捷径,直接将 Redis 的端口进行开放,然后设置一个密码:

    \n

    修改 /etc/redis/redis.conf:

    \n

    将里边的 bind 127.0.0.1 改为 bind 0.0.0.0,这样的话 Redis 就可以监听外部请求了。

    \n

    接下来为 Redis 配置一个认证密码:

    \n

    找到 #requirepass foobared 将注释去掉,同时将 foobared 改为自己想设置的密码。

    \n

    修改完后,保存退出,然后重启 Redis 服务:sudo /etc/init.d/redis-server restart

    \n

    这样就完成了,在我本地尝试登录服务器的 Redis:

    \n

    redis-cli -h ipaddress 发现登录成功,发送个命令试试看: keys *,这是会得到:

    \n

    (error) NOAUTH Authentication required.

    \n

    这样的结果,告诉我们没有权限,因为我们设置了访问密码。

    \n

    正确的登录姿势是:redis-cli -h ipaddress -a password

    \n

    同时,Python 程序中连接 Redis 的时候也要记得加上 password 参数。

    \n"},{"title":"Redis 性能变慢时的 Checklist","url":"/2021/redis-slow-checklist/","content":"
      \n
    1. 获取 Redis 实例在当前环境下的基线性能。
    2. \n
    3. 是否用了慢查询命令?
        \n
      • 如果是的话,就使用其他命令替代慢查询命令,或者把聚合计算命令放在客户端做。
      • \n
      \n
    4. \n
    5. 是否对过期 key 设置了相同的过期时间?
        \n
      • 对于批量删除的 key,可以在每个 key 的过期时间上加一个随机数,避免同时删除。
      • \n
      \n
    6. \n
    7. 是否存在 bigkey?
        \n
      • 对于 bigkey 的删除操作,如果你的 Redis 是 4.0 及以上的版本,可以直接利用异步线程机制减少主线程阻塞;如果是 Redis 4.0 以前的版本,可以使用 SCAN 命令迭代删除;
      • \n
      • 对于 bigkey 的集合查询和聚合操作,可以使用 SCAN 命令在客户端完成。
      • \n
      \n
    8. \n
    9. Redis AOF 配置级别是什么?业务层面是否的确需要这一可靠性级别?
        \n
      • 如果我们需要高性能,同时也允许数据丢失,可以将配置项 no-appendfsync-on-rewrite 设置为 yes,避免 AOF 重写和 fsync 竞争磁盘 IO 资源,导致 Redis 延迟增加。
      • \n
      • 如果既需要高性能又需要高可靠性,最好使用高速固态盘作为 AOF 日志的写入盘。
      • \n
      \n
    10. \n
    11. Redis 实例的内存使用是否过大?发生 swap 了吗?
        \n
      • 如果是的话,就增加机器内存,或者是使用 Redis 集群,分摊单机 Redis 的键值对数量和内存压力。
      • \n
      • 同时,要避免出现 Redis 和其他内存需求大的应用共享机器的情况。
      • \n
      \n
    12. \n
    13. 在 Redis 实例的运行环境中,是否启用了透明大页机制?
        \n
      • 如果是的话,直接关闭内存大页机制就行了。
      • \n
      \n
    14. \n
    15. 是否运行了 Redis 主从集群?
        \n
      • 如果是的话,把主库实例的数据量大小控制在 2~4GB,以免主从复制时,从库因加载大的 RDB 文件而阻塞。
      • \n
      \n
    16. \n
    17. 是否使用了多核 CPU 或 NUMA 架构的机器运行 Redis 实例?
        \n
      • 使用多核 CPU 时,可以给 Redis 实例绑定物理核
      • \n
      • 使用 NUMA 架构时,注意把 Redis 实例和网络中断处理程序运行在同一个 CPU Socket 上
      • \n
      \n
    18. \n
    \n
    \n

    备注:

    \n

    内存大页机制:Linux 内核从 2.6.38 开始支持内存大页机制,该机制支持 2MB 大小的内存页分配,而常规的内存页分配是按 4KB 的粒度来执行的。

    \n

    NUMA 架构:在主流的服务器上,一个 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) 这个错误。

    \n

    查询后发现这个是 JetBrain 在将 delve 嵌入到 他们的 IDE 时导致的 bug,按照官方的说法是升级 IDE 就可以解决了。详细讨论见这个 issue:https://github.com/go-delve/delve/issues/1710

    \n

    但是我的 ToolBox 在 Check for updates 时没有响应,所以需要通过其他方式进行了解决。

    \n

    更新 dlv,并将 GoLand 中的 dlv 路径指向更新后的路径

    1) go get -u github.com/go-delve/delve/cmd/dlv
    2) 执行以下命令并将打印的路径复制下来:

    \n
    ➜ echo `go env | grep GOPATH | cut -d "\\"" -f 2`/bin/dlv

    # 以下是打印的结果,进行复制
    /Users/jiapan/go/bin/dlv`
    \n

    3) 在 GoLand 中 Help -> Edit Custom Properties(之前没编辑过会提示新建)
    4) 新增一项 dlv.path={你复制的路径},比如我的:

    \n
    dlv.path=/Users/jiapan/go/bin/dlv
    \n

    再次启动服务,问题解决。

    \n
    \n

    delve 是 go 语言的 debug 工具,delve 的意思是:钻研、探索,用这个来命名一个 debug 工具还是非常形象的。

    \n
    \n"},{"title":"解决 ssh 登录慢的问题","url":"/2018/resolve-ssh-slow/","content":"

    OpenSSH 在用户登录的时候会验证 IP,它根据用户的 IP 使用反向 DNS 找到主机名,再使用 DNS 找到 IP 地址,最后匹配一下登录的 IP 是否合法。如果客户机的 IP 没有域名,或者 DNS 服务器很慢或不通,那么登录就会很花时间。

    \n

    解决办法:

    \n

    在目标服务器上修改 sshd 服务器端配置,并重启 sshd。

    \n

    vi /etc/ssh/sshd_config

    \n

    设置 UseDNSno 即可。

    \n

    最后

    \n

    systemctl restart sshd

    \n
    \n

    参考:https://www.cnblogs.com/ggjucheng/p/3348499.html

    \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

    下边用几个我使用过的稍微好一点的物品举例,这些物品价格确实会稍贵一点,但也不是什么奢侈品,限于我目前的人生阅历也只能用这些来说明了。

    \n

    戴森吹风机

    一个戴森吹风机3000多,普通家庭是绝对不会买的。我们家几年前一直在用其他品牌的吹风机,也没感觉有什么问题,后来我们帮一个保险销售介绍客户,完成了很多任务,作为奖励她送了我们一台戴森吹风机,自从用上以后就再也用不惯其他吹风机了。

    \n

    前一阵子搬家,我把戴森拿到了新家使用,因为我爸妈还要在之前的房子住,那边需要一个新吹风机。我看到这两年一个国产的品牌「徕芬」吹风机很火,外形也和戴森很像,就买了一个给他们用。前两天我回家用了一次徕芬,实话实说,如果我之前没有用过戴森,我一定觉得这个吹风机非常好用,但用过了更好的对比之下才知道还有很大差距。

    \n

    室内隐形门锁

    \n

    传统的门锁都会外露一个弹簧的探头,用来在关门时将门卡住。探头上下两个角很尖,不注意时会磕碰到人,家里有小孩的情况下,如果小孩正好跟门锁差不多高,在跑来跑去时会更危险。日常关门时,因为探头要和门框上的凹槽摩擦,还会有很大的噪音。因为探头存在阻力,在关这种门时,通常是用手把门把手转到下边,再去将门关严,或者需要很用力地去关。

    \n

    装修新房时,才知道现在已经有了无形的锁具,门在开启状态时探头是不会外漏的,只有将门关闭后探头才会弹出,避免了磕碰还更静音了。想把门关严时也不用捉着把手去关了,直接推门就可以。我没有研究它的原理,猜测是用了磁铁之类的。

    \n

    花洒 && 零冷水

    另一个和装修有关的是新家里的淋浴设备和零冷水燃气热水器。在我没有用新的花洒之前,没觉得之前用过的花洒有什么问题,用过之后觉得之前花洒水量太小了。前两天再去用之前的就觉得身上的沫子半天才能冲干净,新的淋浴一瞬间就冲完了。

    \n

    还有支持恒温的零冷水燃气热水器,如果没有接触过,我真的不知道洗澡水居然可以不需要等待,每次打开直接出热水,温度也是之前设置好的恒定温度,完全不用担心忽冷忽热的问题。

    \n

    自助餐

    上个周末和家人去吃了一次比格自助,79一位。如果家庭条件一般,自助吃的比较少的话,就会觉得比格很不错了,当然比格在这个价位里也确实不错。但如果吃过更好的,就会知道比格的食材还差很多。

    \n

    其实我也没什么资格评判比格,因为我吃的比较多的也是比格或者比格这个价位的自助,只是在公司团建的时候有幸吃过其他稍微高档一些的,比如第六季、水木锦堂之类的。但次数有限,那些更高级的,上千块的自助还没有体验过。

    \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 该如何补救呢?

    \n
      \n
    1. 首先使用 control + z 让当前进程挂起(Suspend)。
    2. \n
    3. 然后我们使用 jobs 查看它的作业号。
    4. \n
    5. 再用 bg %jobspec 来将它放入后台并继续运行。
    6. \n
    7. 最后使用 disown -h %jobspec 来使这个作业忽略 HUP 信号。
    8. \n
    \n

    这个方法可以用在 scp 的命令中,在没有设置 ssh 无密码登录的情况下,我们不能使用 nohup 来执行 scp 命令,所以只能在开始大文件拷贝后,通过上述流程来让这个作业放置在后台执行。

    \n"},{"title":"告别 git push --set-upstream","url":"/2022/say-bye-to-set-upstream/","content":"

    当我们使用 git 命令将本地新开分支的代码推到远端仓库时,需要先使用 --set-upstream 命令声明要推到远端的哪个分支,比如:

    \n
    git push --set-upstream origin test && git push
    \n

    当我们忘记使用 --set-upstream 时就会报如下错误:

    \n
    git push

    fatal: The current branch test-branch has no upstream branch.
    To push the current branch and set the remote as upstream, use
    git push --set-upstream origin test -branch
    \n

    大多数情况下我们都是将本地同名分支推到远端仓库,那么有没有办法可以让我们在 push 时自动使用本地分支名作为远端的分支呢?当然有!

    \n

    我们可以如下配置 git,将 push 到远端的分支名自动使用当前本地分支名:

    \n
    git config --global --add push.autoSetupRemote true
    \n

    如此配置后,就可以跟 --set-upstream 说再见了。

    \n

    补充: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
      \n
    • Serverless 最初是用来描述那些结合第三方、云托管来管理服务器端逻辑和状态的应用。通常是一些「富客户端」应用,比如单页 web 应用或者手机 APP,它们可以使用第三方提供的庞大生态系统来进行云端存储(比如:国内的 LeanCloud,国外的 Parse、Firbase)。这些类型的服务之前被称为「后端即服务」或「BaaS」。
    • \n
    • Serverless 还可以表示另一种情况,开发人员仍然编写服务器端应用代码,但是与传统架构不同,这个应用运行在无状态的容器中,这些容器是事件触发、短暂(可能只调用一次)并且由第三方来管理的。这种做法通常被理解为「函数即服务」或「FaaS」。AWS Lambda 是目前提供「函数即服务」的实现之一。
    • \n
    \n

    尽管有 Serverless 这个名字,但实际并不是在没有服务器的情况下运行代码。之所以使用「无服务器计算(serverless computing)」这个名称,是因为拥有系统的企业或个人不必为运行后端应用而采购、租用、配置服务器或虚拟机。

    \n

    Serverless 有以下优势:

    \n
      \n
    • 无服务器管理(无需管理任何形式的服务器)
    • \n
    • 按执行付费(不为空闲时间买单)
    • \n
    • 自动伸缩(根据需求伸缩)
    • \n
    • 函数作为应用的逻辑单元
    • \n
    \n

    Serverless 模式鼓励将开发重点放在定义明确的业务逻辑单元上,而无需考虑如何部署、扩容或其它一些过早优化。因此开发的重点也应该是单个功能或模块,而不是一个具有大范围功能的服务。Serverless 将开发人员从部署的麻烦中解放出来,使得他们能够专注于按照逻辑封装应用。

    \n

    一个典型的例子是将图片上传到文件存储,此事件调用一个 Serverless 函数,这个函数创建图片的缩略图然后把该缩略图存入文件存储中,并将缩略图位置记录在 NoSQL 数据库中。数据写入 NoSQL 数据库的事件可能还会触发其他函数。这个缩略图创建函数只需按需运行,唯一的成本是调用该函数的次数。

    \n

    和其他技术一样,Serverless 并不完美。它的缺点是应用监控和调试将会变得困难,只能依靠于服务产生的日志记录。同时,在有服务间调用事件时,可能会出现供应商锁定。并且现有的 IDE 对 Serverless 函数支持也不够友好。

    \n

    简单 HTTP 服务示例

    \n

    Serverless 框架 —— 可以构建由微服务组成的应用,这些微服务在响应事件时运行,并且可以自动扩容、只在运行期间收费。

    \n
    \n

    下边的例子将演示如何实现一个简单的 HTTP GET 端点,调用它时会返回当前的时间。内部函数名为 currentTime,HTTP 端点为 ping

    \n

    快速上手 Serverless

      \n
    • 通过 npm 安装 serverless 程序
    • \n
    \n
    npm install -g serverless
    \n\n

    我们需要新建 handler.jsserverless.yml 文件来描述和部署我们的 severless 函数。

    \n
    // handler.js

    'use strict';

    module.exports.endpoint = (event, context, callback) => {
    const response = {
    statusCode: 200,
    body: JSON.stringify({
    message: `Hello, the current time is ${new Date().toTimeString()}.`,
    }),
    };

    callback(null, response);
    };
    \n
    // serverless.yml

    service: serverless-simple-http-endpoint

    frameworkVersion: ">=1.1.0 <2.0.0"

    provider:
    name: aws
    runtime: nodejs8.10

    functions:
    currentTime:
    handler: handler.endpoint
    events:
    - http:
    path: ping
    method: get
    \n

    本地函数调用

    在命令行中执行

    \n
    serverless invoke local --function currentTime
    \n

    返回结果如下:

    \n
    {
    "statusCode": 200,
    "body": "{\\"message\\":\\"Hello, the current time is 21:46:18 GMT+0800 (CST).\\"}"
    }
    \n

    部署

    部署应用只需执行

    \n
    serverless deploy
    \n

    在安全凭证配置正确的情况下会看到类似下边的结果:

    \n
    Serverless: Packaging service...
    Serverless: Excluding development dependencies...
    Serverless: Uploading CloudFormation file to S3...
    Serverless: Uploading artifacts...
    Serverless: Uploading service serverless-simple-http-endpoint.zip file to S3 (331 B)...
    Serverless: Validating template...
    Serverless: Updating Stack...
    Serverless: Checking Stack update progress...
    ..............................
    Serverless: Stack update finished...
    Service Information
    service: serverless-simple-http-endpoint
    stage: dev
    region: us-east-1
    stack: serverless-simple-http-endpoint-dev
    resources: 11
    api keys:
    None
    endpoints:
    GET - https://qnye7m4dwf.execute-api.us-east-1.amazonaws.com/dev/ping
    functions:
    currentTime: serverless-simple-http-endpoint-dev-currentTime
    layers:
    None
    \n

    使用

    现在,我们可以直接调用 AWS Lambda 服务,并且可以同时获取执行日志:

    \n
    serverless invoke --function currentTime --log

    {
    "statusCode": 200,
    "body": "{\\"message\\":\\"Hello, the current time is 14:03:57 GMT+0000 (UTC).\\"}"
    }
    --------------------------------------------------------------------
    START RequestId: 002cbcce-fda6-4d84-98a2-2fb19d325812 Version: $LATEST
    END RequestId: 002cbcce-fda6-4d84-98a2-2fb19d325812
    REPORT RequestId: 002cbcce-fda6-4d84-98a2-2fb19d325812\tDuration: 0.61 ms\tBilled Duration: 100 ms\tMemory Size: 1024 MB\tMax Memory Used: 59 MB
    \n

    或者使用如 curl 等工具发送一个 HTTP 请求来查看结果:

    \n
    curl https://qnye7m4dwf.execute-api.us-east-1.amazonaws.com/dev/ping

    {"message":"Hello, the current time is 14:03:49 GMT+0000 (UTC)."}
    \n

    甚至可以直接用浏览器访问:

    \n

    \"\"

    \n"},{"title":"山丘 - 李宗盛","url":"/2018/shangqiu-video/","content":"\n\n
    \n

    想说却还没说的 还很多
    攒着是因为想写成歌
    让人轻轻地唱着 淡淡地记着
    就算终于忘了 也值了
    说不定我一生涓滴意念
    侥幸汇成河
    然后我俩各自一端
    望着大河弯弯 终于敢放胆
    嘻皮笑脸 面对 人生的难
    也许我们从未成熟
    还没能晓得 就快要老了
    尽管心里活着的还是那个
    年轻人
    因为不安而频频回首
    无知地索求 羞耻于求救
    不知疲倦地翻越 每一个山丘
    越过山丘 虽然已白了头
    喋喋不休 时不我予的哀愁
    还未如愿见着不朽
    就把自己先搞丢
    越过山丘 才发现无人等候
    喋喋不休 再也唤不回温柔
    为何记不得上一次是谁给的拥抱
    在什么时候
    我没有刻意隐藏 也无意让你感伤
    多少次我们无醉不欢
    咒骂人生太短 唏嘘相见恨晚
    让女人把妆哭花了 也不管
    遗憾我们从未成熟
    还没能晓得 就已经老了
    尽力却仍不明白
    身边的年轻人
    给自己随便找个理由
    向情爱的挑逗 命运的左右
    不自量力地还手 直至死方休
    越过山丘 虽然已白了头
    喋喋不休 时不我予的哀愁
    还未如愿见着不朽
    就把自己先搞丢
    越过山丘 才发现无人等候
    喋喋不休 再也唤不回了温柔
    为何记不得上一次是谁给的拥抱
    在什么时候
    越过山丘 虽然已白了头
    喋喋不休 时不我予的哀愁
    还未如愿见着不朽
    就把自己先搞丢
    越过山丘 才发现无人等候
    喋喋不休 再也唤不回了温柔
    为何记不得上一次是谁给的拥抱
    在什么时候
    喋喋不休 时不我予的哀愁
    向情爱的挑逗 命运的左右
    不自量力地还手 直至死方休
    为何记不得上一次是谁给的拥抱
    在什么时候

    \n
    \n"},{"title":"Java 单例模式完整指南","url":"/2019/singleton-design-pattern-in-java/","content":"
    \n

    设计模式一直流行于程序员之间,本文讨论被许多人认为最简单但最有争议的设计模式 —— 单例模式

    \n
    \n

    \"\"

    \n

    设计模式概述

    在软件工程中,设计模式描述了如何解决重复出现的设计问题,以设计出灵活、可复用的面向对象的应用程序。设计模式一共有 23 种,可以将它们分为三个不同的类别 —— 创建型、结构型和行为型。

    \n

    创建型设计模式

    创建型设计模式是处理对象创建机制的模式,试图以适合的方式创建对象。

    \n

    对象创建的基本形式可能会导致设计问题或增加设计的复杂性。创建型设计模式通过某种方式控制对象的创建来解决此问题。

    \n

    结构型设计模式

    结构型设计模式处理类和对象的组成。这类模式使我们将对象和类组装为更大的结构,同时保持结构的高效和灵活。

    \n

    行为型设计模式

    行为型设计模式讨论对象的通信以及它们之间如何交互。

    \n

    单例设计模式

    我们对设计模式和其类型进行了概述,接下来我们重点介绍单例设计模式。

    \n

    单例模式提供了控制程序中允许创建的实例数量的能力,同时确保程序中有一个单例的全局访问点。

    \n

    优点

      \n
    • 对单个实例的访问控制
    • \n
    • 减少对象数量
    • \n
    • 允许完善操作和表示
    • \n
    \n

    缺点

      \n
    • 被很多程序员视为反模式
    • \n
    • 在可能不需要单例的情况下被误用
    • \n
    \n

    实现

    单例设计模式可以通过多种方式实现。每一种都有其自身的优点和局限性,我们可以通过以下几种方式实现单例模式:

    \n
      \n
    • 预先初始化(Eager Initialization)
    • \n
    • 静态块预初始化
    • \n
    • 延迟初始化(Lazy Initialization)
    • \n
    • 线程安全的延迟初始化
    • \n
    • 双重检查的延迟初始化
    • \n
    • 单个实例的枚举
    • \n
    \n

    实现单例

    本节我们将讨论实现单例模式的各种方法。

    \n

    预先初始化(Eager Initialization)

      \n
    • 用预先初始化方法,对象在创建之前就已被初始化
    • \n
    • 由于预先初始化,所以可能会出现程序并不需要的情况下初始化
    • \n
    • 如果单例类很简单并且不需要太多资源,这个方法会很有用。
    • \n
    \n
    public class EagerInitialization {

    private static EagerInitialization INSTANCE = new EagerInitialization();

    private EagerInitialization() {
    }

    public static EagerInitialization getInstance() {
    return INSTANCE;
    }

    }
    \n

    静态块预初始化

      \n
    • 在前面讨论的预先初始化方法中,没有提供任何异常处理逻辑
    • \n
    • 在此实现中,对象是在静态块中创建的,因此在对象初始化时可以进行异常处理
    • \n
    • 这种方法和预先初始化有同样的问题:即使程序可能不使用对象,对象也会被提前创建出来
    • \n
    \n

    延迟初始化(Lazy Initialization)

      \n
    • 按需创建对象
    • \n
    • 与预先初始化不同,延迟初始化会在需要时创建对象
    • \n
    • 此实现不是线程安全的
    • \n
    \n
    import java.util.Objects;

    public class LazyInit {

    private static LazyInit INSTANCE = null;

    private LazyInit() {
    }

    public static LazyInit getInstance() {
    if (null == INSTANCE) {
    synchronized (LazyInit.class) {
    INSTANCE = new LazyInit();
    }
    }
    return INSTANCE;
    }
    }
    \n

    线程安全的延迟初始化

      \n
    • 添加了用以处理多线程的同步方案
    • \n
    • 因为每次调用都需要进行方法级同步,而过度的同步会降低性能
    • \n
    \n
    import java.util.Objects;

    public class LazyInitialization {

    private static LazyInitialization INSTANCE = null;

    private LazyInitialization() {
    }

    public synchronized static LazyInitialization getInstance() {
    if (null == INSTANCE) {
    INSTANCE = new LazyInitialization();
    }
    return INSTANCE;
    }
    }
    \n

    双重检查的延迟初始化

      \n
    • 解决方法级同步的问题
    • \n
    • 为对象可为空性执行双重检查
    • \n
    • 尽管这种方法似乎可以完美的工作,但是在多线程场景下不太适用。
    • \n
    \n
    import java.util.Objects;

    public class DoubleCheckSingleton {

    private static DoubleCheckSingleton INSTANCE;

    private DoubleCheckSingleton(){}

    public static DoubleCheckSingleton getInstance() {
    if(null == INSTANCE){
    synchronized (DoubleCheckSingleton.class) {
    if(null == INSTANCE){
    INSTANCE = new DoubleCheckSingleton();
    }
    }
    }
    return INSTANCE;
    }

    }
    \n

    说明一下为什么这种方法在多线程常见下可能存在问题:

    \n

    INSTANCE = new DoubleCheckSingleton(); 这句代码,实际上可以分解成以下三个步骤:

    \n
      \n
    1. 分配内存空间
    2. \n
    3. 初始化对象
    4. \n
    5. 将对象指向刚分配的内存空间
    6. \n
    \n

    但是有些编译器为了性能的原因,可能会将第二步和第三步进行重排序,顺序就成了:

    \n
      \n
    1. 分配内存空间
    2. \n
    3. 将对象指向刚分配的内存空间
    4. \n
    5. 初始化对象
    6. \n
    \n

    在多线程中就会出现第二个线程判断对象不为空,但此时对象还未初始化的情况。

    \n

    正确的双重检查

    import java.util.Objects;

    public class DoubleCheckSingleton {

    private volatile static DoubleCheckSingleton INSTANCE;

    private DoubleCheckSingleton(){}

    public static DoubleCheckSingleton getInstance() {
    if(null == INSTANCE){
    synchronized (DoubleCheckSingleton.class) {
    if(null == INSTANCE){
    INSTANCE = new DoubleCheckSingleton();
    }
    }
    }
    return INSTANCE;
    }

    }
    \n

    为了解决上述问题,需要加入关键字 volatile。使用了 volatile 关键字后,重排序被禁止,所有的写(write)操作都将发生在读(read)操作之前。

    \n

    单个实例的枚举

      \n
    • 使用 Java 枚举类型创建单例
    • \n
    • 此方法为处理反射、序列化和多线程场景提供了本地支持
    • \n
    • 枚举类型有些不灵活
    • \n
    \n
    public enum Singleton {
    INSTANCE;
    }
    \n

    保护单例

    使单例不受反射访问的影响

    在所有的单例实现中(枚举方法除外),我们通过提供私有构造函数来确保单例。但是,可以通过反射来访问私有构造函数,反射是在运行时检查或修改类的运行时行为的过程。
    \u0005
    让我们演示如何通过反射访问单例:

    \n
    import java.lang.reflect.Constructor;
    import java.lang.reflect.InvocationTargetException;

    public class SingletonWithReflection {


    public static void main(String[] args) {

    EagerInitializedSingleton firstSingletonInstance = EagerInitializedSingleton.getInstance();
    EagerInitializedSingleton secondSingletonInstance = null;

    try{
    Class<EagerInitializedSingleton> clazz = EagerInitializedSingleton.class;
    Constructor<EagerInitializedSingleton> constructor = clazz.getDeclaredConstructor();
    constructor.setAccessible(true);
    secondSingletonInstance = constructor.newInstance();
    }
    catch(NoSuchMethodException | InvocationTargetException | IllegalAccessException | InstantiationException e){
    e.printStackTrace();
    }

    System.out.println("Instance 1 hashcode: "+firstSingletonInstance.hashCode());
    System.out.println("Instance 2 hashcode: "+secondSingletonInstance.hashCode());

    }
    }
    \n

    上边代码输出如下:

    \n
    Instance 1 hashcode: 21049288
    Instance 2 hashcode: 24354066
    \n

    解决:

    如果单例对象已经初始化,则可以通过禁止对构造函数的访问来防止通过反射访问单例类。如果在对象初始化之后调用构造函数,可以通过抛出异常的方式来实现。

    \n
    public final class EagerInitializedSingleton {

    private static final EagerInitializedSingleton INSTANCE = new EagerInitializedSingleton();

    private EagerInitializedSingleton(){
    if(Objects.nonNull(INSTANCE)){
    throw new RuntimeException("This class can only be access through getInstance()");
    }
    }

    public static EagerInitializedSingleton getInstance(){
    return INSTANCE;
    }
    }
    \n

    使单例在序列化时安全

    在分布式应用程序中,有时我们会序列化一个对象,以将对象状态保存在持久化存储中,并用以之后的检索。保存对象状态的过程称为序列化,而检索操作称为反序列化

    \n

    如果单例没有被正确实现,那么可能出现一个单例对象有两个实例的情况。

    \n

    让我们看看如何出现这种情况:

    \n
    import java.io.FileInputStream;
    import java.io.FileOutputStream;
    import java.io.ObjectInputStream;
    import java.io.ObjectOutputStream;

    public class SingletonWithSerialization {

    public static void main(String[] args) {

    EagerInitializedSingleton firstSingletonInstance = EagerInitializedSingleton.getInstance();
    EagerInitializedSingleton secondSingletonInstance = null;

    ObjectOutputStream outputStream = null;
    ObjectInputStream inputStream = null;
    try{
    // 将对象状态保存到文件中
    outputStream = new ObjectOutputStream(new FileOutputStream("FirstSingletonInstance.ser"));
    outputStream.writeObject(firstSingletonInstance);
    outputStream.close();

    // 从文件中检索对象状态
    inputStream = new ObjectInputStream(new FileInputStream("FirstSingletonInstance.ser"));
    secondSingletonInstance = (EagerInitializedSingleton) inputStream.readObject();
    inputStream.close();
    }
    catch(Exception e){
    e.printStackTrace();
    }

    System.out.println("FirstSingletonInstance hashcode: "+firstSingletonInstance.hashCode());
    System.out.println("SecondSingletonInstance hashcode: "+secondSingletonInstance.hashCode());
    }
    }
    \n

    以上代码输出如下:

    \n
    FirstSingletonInstance hashcode: 23090923
    SecondSingletonInstance hashcode: 19586392
    \n

    这说明现在有两个单例实例。

    \n
    \n

    注意,单例类必须实现 Serializable 接口才能序列化实例。

    \n
    \n

    为了避免序列化产生多个实例,我们可以在单例类中实现 readResolve() 方法。这个方法将会替换从流中读取的对象。实现代码如下:

    \n
    import java.io.Serializable;
    import java.util.Objects;

    public class EagerInitializedSingleton implements Serializable {

    private static final long serialVersionUID = 1L;

    private static final EagerInitializedSingleton INSTANCE = new EagerInitializedSingleton();

    private EagerInitializedSingleton(){
    if(Objects.nonNull(INSTANCE)){
    throw new RuntimeException("This class can only be access through getInstance()");
    }
    }

    public static EagerInitializedSingleton getInstance(){
    return INSTANCE;
    }

    protected Object readResolve(){
    return getInstance();
    }
    }
    \n

    再次执行 SingletonWithSerialization 输出如下:

    \n
    FirstSingletonInstance hashcode: 24336889
    SecondSingletonInstance hashcode: 24336889
    \n

    Java API 中的单例示例

    Java API 中有很多类是用单例设计模式设计的:

    \n
    java.lang.Runtime#getRuntime()
    java.awt.Desktop#getDesktop()
    java.lang.System#getSecurityManager()
    \n

    结论

    单例模式是最重要和最常用的设计模式之一。尽管很多人批评它是一种反模式,并且有很多实现时的注意事项,但在实际生活中有很多使用这种模式的示例。本文尝试介绍了常见的单例设计和与之相关的缺陷。

    \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

    \n

    6mm 的价格是69.9,8mm 的价格是130,从京东购入。

    \n

    强烈建议再从拼多多买羽毛球拍防汗带把手柄缠上,这样手感非常好。

    \n

    运动监控

    我用 AppleWatch 采集心率,通过 YaoYao 这个跳绳专用 App 在运动期间查看心率并进行间歇训练计数。

    \n

    我会将心率心率控制在135左右,中间会有几十秒努力将心率提升至极限150+,有效训练心肺功能。

    \n

    \n

    \n

    AirPods Pro2 耳机

    因为是在室内,摇绳的声音就比较大,再加上升级成半斤重的跳绳后,甩绳子的声音整个楼层都能听到,带上 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":"

    \"1.jpg\"

    \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
    • 出现饥饿感是正常的,并不一定意味着你没吃饱。永远不饿只能说明我们吃得太多了。
    • \n
    • 出现困倦感也是正常的,并不意味着你睡眠不足。从不犯困意味着我们睡得太多了。
    • \n
    \n

    作者甚至认为睡得太多更容易患抑郁症,现在确实在医疗界会用限制睡眠来缓解抑郁症,《我们为什么睡觉》这本书里也有介绍。

    \n

    还有一个与我们之前认知向背的观点:作者认为记忆力的巩固并不需要睡眠,这点我也有体会,比如我前一天背了英语、Anki,在失眠一晚后的第二天再次复习那些内容时依然可以背下来,没有任何影响。

    \n

    很多人(包括我自己)认为睡眠不足会影响第二天的情绪,但我觉得心理因素的影响比身体因素影响要大的多。我低落的原因大多是因为前一晚翻来覆去睡不着而恼火影响了第二天的情绪,回想一下初高中时候逃课去网吧通宵,第二天也没觉得情绪有啥影响。

    \n

    说到逃课去网吧这里跑个题,我的初恋是高中同学,我俩确定关系是有次我们学校搞活动,搞到晚上 10 点多,她是走读生,我是住宿生,我知道她家离学校很远,坐公交要 1 小时,想要做次护花使者送她回家。我记得当时我们打了个车把她送到她家附近,这时候回学校已经进不了宿舍了,学校大门也进不了,索性我就没回去(也多亏了那个活动结束后宿舍比较混乱,没有查寝),我在她家附近找了个网吧玩了一晚上,当然我的另一个目的也是第二天早上能和她一起乘公交去学校,第二天早上我们在公交车上汇合,她给我带了一包牛奶,那一天是我的高光时刻,觉得世界上其他事情都不重要了,虽然前一晚没睡觉,但心情反而乐到极点,课上也睡得死去活来。

    \n

    我在睡眠少的时候更容易亢奋,更容易在这一天发朋友圈或者写博客,我觉得这也符合我们祖先的特征:缺乏睡眠大多是因为周围有危险情况导致的,他们为了活下来需要更警觉。作者在文章中也补充了几件发生在其他人身上的轶事,其中有一个叫 Brian Timar 的提到:自己在本科阶段每次重大考试前一晚都不睡觉,第二天会超级兴奋和敏锐。

    \n
    \n

    sleep 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
    • miserable, then
    • \n
    • absurd and in a good humor, weirdly elated, then
    • \n
    • Super PumpedTM, and
      really sharp when the test (or whatever) actually started.
    • \n
    \n
    \n

    很多人表达过睡觉多的孩子学习更好,我们是不是可以从另一个角度去解释这个现象:学生们睡觉多了学习时间就少了,他们需要集中精力去学习,这样的话效率自然就高了;那些睡觉少的学生因为时间多,所以做事总是磨磨蹭蹭,效率不高,效率不高更不容易学会,就不愿意学,这样就形成了负向螺旋下降。如果那些睡眠少的学生把大量时间都用在学习上我相信不会比其他学生差吧。

    \n

    我大部分失眠情况可能都是因为担心失眠而失眠了,担心自己睡不好影响第二天的表现,读完这篇文章我觉得自己以后可以不这么紧张了,我要告诉自己:睡眠少可以让我更亢奋,发挥的可以更好。

    \n"},{"title":"Spark 操作 Elasticsearch 示例","url":"/2017/spark-operator-elasticsearch-demo/","content":"

    上周五调研了下如何用 Spark 读写 Elasticsearch(下文简称 es),中间被官方提供的 jar 包卡了很久,所以本来想周末记录一下,结果一发懒就没做,就蹭到周一晚上来写一下了,最近调研的东西很多,有很多要记得东西,一点一点来吧。

    \n

    不废话,直接 Show you the code:

    \n
    import org.apache.spark.{SparkConf, SparkContext}
    import org.elasticsearch.spark._


    object ElasticSparkHelloWorld {
    def main(args: Array[String]) {


    val conf = new SparkConf().setAppName(ElasticSparkHelloWorld.getClass.getName)
    conf.setMaster("local")
    conf.set("es.nodes", "localhost")
    conf.set("es.port", "9200")
    conf.set("es.index.auto.create", "true")
    conf.set("es.nodes.wan.only", "true")
    conf.set("es.query", "?q=*")
    conf.set("es.resource", "spark/docs")

    val sc = new SparkContext(conf)
    val numbers = Map("one" -> 1, "two" -> 2, "three" -> 3, "four" -> 4)
    val airports = Map("OTP" -> "Otopeni", "SFO" -> "San Fran")

    sc.makeRDD(Seq(numbers, airports)).saveToEs("spark/docs")

    println(sc.esRDD().count())

    }
    }
    \n

    其实上边这些代码从网上一搜一大堆,重点是下边 sbt 部分的配置:

    \n
    name := "spark-es-demo"

    version := "1.0"

    scalaVersion := "2.11.11"

    //scalacOptions += "-Ylog-classpath"

    libraryDependencies += "org.apache.spark" %% "spark-core" % "1.6.2"
    libraryDependencies += "org.elasticsearch" % "elasticsearch-spark-13_2.11" % "5.5.2"
    \n

    需要注意 scala 版本,spark 版本还有 es 版本一定要对应,否则无法运行

    \n

    比如

    \n
      \n
    • scalaVersion 版本是 2.11.11
    • \n
    • spark 版本是 1.6.2
    • \n
    • es 版本是 5.5.2
    • \n
    \n

    依赖需要写成下边这样:

    \n
    libraryDependencies += "org.apache.spark" %% "spark-core" % "1.6.2"  // 这里指定 spark-core 的版本
    libraryDependencies += "org.elasticsearch" % "elasticsearch-spark-13_2.11" % "5.5.2"
    \n

    解释一下 "elasticsearch-spark-13_2.11" % "5.5.2" 这部分

    \n

    -13 是给 Spark1.3-1.6 提供的
    -20 是给 Spark2.0 提供的

    \n

    _2.11scalaVersion 的前边两位

    \n

    5.5.2elasticsearch 的版本号

    \n

    官方文档中提到

    \n
    \n

    The Spark connector framework is the most sensitive to version incompatibilities.

    \n
    \n
    \n

    Spark 连接器框架是对版本号非常敏感并且不兼容的。

    \n
    \n

    另外一个坑是,elasticsearch-spark-13_2.11 这个 jar 包所依赖的包无法在 maven 官方源中找到,需要添加另一个源:conjars: http://conjars.org/repo

    \n

    ~/.sbt 下新建 repositories 文件,我的 repositories 内容如下:

    \n
    [repositories]
    local
    aliyun: http://maven.aliyun.com/nexus/content/groups/public/
    conjars: http://conjars.org/repo
    central: http://repo1.maven.org/maven2/
    \n

    将阿里源放在上边,可以让官方依赖下载更快。

    \n

    完整代码见:https://github.com/Panmax/spark-es-demo

    \n","tags":["BigData"]},{"title":"Spring Boot 与 Docker 结合","url":"/2017/spring-boot-docker/","content":"
    \n

    Docker 是一个具有社交倾向的 Linux 容器管理工具包,允许用户发布容器镜像,其他用户可以使用这些镜像。Docker 镜像是运行容器化进程的基础,本文将介绍如何编译一个简单的 Spring Boot 应用的镜像。

    \n
    \n

    Docker 的安装和基本使用不在本文中介绍,之后可以单独拿出来写一写。

    \n

    本文将使用 Gradle 作为编译工具,基础项目工程直接使用 IDEA 的 Spring Initializr 生成,如下图:

    \n

    \"\"

    \n

    直接下一步,注意这里的 Type 修改为 Gradle Project,然再下一步

    \n

    \"\"

    \n

    然后只需要勾选 Web 即可

    \n

    \"\"

    \n

    我们来简单调整一下 build.gradle,新增:

    jar {
    baseName = 'my-spring-boot-docker'
    version = '0.1.0'
    }
    \n

    它的作用是让编译出来的 jar 包文件名为:my-spring-boot-docker-0.1.0.jar

    \n

    此时 build.gradle 如下:

    buildscript {
    ext {
    springBootVersion = '1.5.4.RELEASE'
    }
    repositories {
    mavenCentral()
    }
    dependencies {
    classpath("org.springframework.boot:spring-boot-gradle-plugin:${springBootVersion}")
    }
    }

    apply plugin: 'java'
    apply plugin: 'eclipse'
    apply plugin: 'org.springframework.boot'


    jar {
    baseName = 'my-spring-boot-docker'
    version = '0.1.0'
    }


    version = '0.0.1-SNAPSHOT'
    sourceCompatibility = 1.8

    repositories {
    mavenCentral()
    }


    dependencies {
    compile('org.springframework.boot:spring-boot-starter-web')
    testCompile('org.springframework.boot:spring-boot-starter-test')
    }
    \n

    然后我们写一个最简单的 Controller,为了方便直接写在 main 方法的类中:

    \n
    package com.jpanj;

    import org.springframework.boot.SpringApplication;
    import org.springframework.boot.autoconfigure.SpringBootApplication;
    import org.springframework.web.bind.annotation.RequestMapping;
    import org.springframework.web.bind.annotation.RestController;

    @SpringBootApplication
    @RestController
    public class SpringBootDockerApplication {

    \t@RequestMapping("/")
    \tpublic String home() {
    \t\treturn "Hello Docker World";
    \t}

    \tpublic static void main(String[] args) {
    \t\tSpringApplication.run(SpringBootDockerApplication.class, args);
    \t}
    }
    \n

    现在我们在不使用 Docker 容器 的情况下运行这个应用:

    ./gradlew build
    java -jar build/libs/gs-spring-boot-docker-0.1.0.jar
    \n

    然后访问 localhost:8080 可以看到 Hello Docker World 的返回结果。

    \n

    接下来让我们把它容器化吧

    Docker 有一个简单的 Dockerfile 文件格式用来指定生成镜像的层次,所以我们在 Spring Boot 项目中创建一个 Dockerfile,将这个文件放在项目根目录下即可,现在这个项目结构是这个样的:

    \n

    \"\"

    \n

    Dockerfile 内容如下:

    FROM frolvlad/alpine-oraclejdk8:slim
    VOLUME /tmp
    ADD target/my-spring-boot-docker-0.1.0.jar app.jar
    ENV JAVA_OPTS=""
    ENTRYPOINT [ "sh", "-c", "java $JAVA_OPTS -Djava.security.egd=file:/dev/./urandom -jar /app.jar" ]
    \n

    这个 Dockerfile 非常简单,不过这就是你运行一个 Spring Boot 应用的全部了,只需要 JavaJAR 文件就够了。这个项目 JAR 文件被作为 app.jar 加入到容器中,然后通过 ENTRYPOINT 来执行它。

    \n

    Docker容器 运行时应该尽量保持容器存储层不发生写操作,在这里我们添加一个 VOLUME 指向 /tmp 是因为 Spring Boot 应用在默认情况下会为 Tomcat 创建工作目录。这里的 /tmp 目录就会在运行时自动挂载为匿名卷,任何向 /tmp 中写入的信息都不会记录进容器存储层,从而保证了容器存储层的无状态化。

    \n

    当然,也可以在运行时可以覆盖这个挂载设置

    \n

    docker run -d -v mytmp:/tmp xxxx

    \n

    在这行命令中,就使用了 mytmp 这个命名卷挂载到了 /tmp 这个位置,替代了 Dockerfile 中定义的匿名卷的挂载配置。

    \n

    不过此步骤在这个简单的应用中是可选的,但是在其他会写入文件系统的 Spring Boot 应用中是必须的。

    \n

    接下来就是把这个项目编译成一个可以到处运行的 Docker 镜像了,在 build.gradle 中我们添加一些新的插件:

    \n
    buildscript {
    ...
    dependencies {
    ...
    classpath('se.transmode.gradle:gradle-docker:1.2')
    }
    }


    ...
    apply plugin: 'docker'

    task buildDocker(type: Docker, dependsOn: build) {
    applicationName = jar.baseName
    dockerfile = file('Dockerfile')
    doFirst {
    copy {
    from jar
    into "${stageDir}/target"
    }
    }
    }
    \n

    上边的配置做了这 3 件事:

      \n
    • 镜像的名字被设置为 jar 配置的 baseName 属性
    • \n
    • 确定 Dockerfile 的位置
    • \n
    • jar 文件从编译目录复制到 docker 编译目录的 target 目录下,这就是我们在 Dockerfile 中看到的 ADD target/my-spring-boot-docker-0.1.0.jar app.jar 为什么会生效的原因。
    • \n
    \n

    此时完整的 build.gradle 如下:

    buildscript {
    ext {
    springBootVersion = '1.5.4.RELEASE'
    }
    repositories {
    mavenCentral()
    }
    dependencies {
    classpath("org.springframework.boot:spring-boot-gradle-plugin:${springBootVersion}")
    classpath('se.transmode.gradle:gradle-docker:1.2')
    }
    }

    apply plugin: 'java'
    apply plugin: 'eclipse'
    apply plugin: 'org.springframework.boot'
    apply plugin: 'docker'


    jar {
    baseName = 'my-spring-boot-docker'
    version = '0.1.0'
    }


    task buildDocker(type: Docker, dependsOn: build) {
    applicationName = jar.baseName
    dockerfile = file('Dockerfile')
    doFirst {
    copy {
    from jar
    into "${stageDir}/target"
    }
    }
    }


    version = '0.0.1-SNAPSHOT'
    sourceCompatibility = 1.8

    repositories {
    mavenCentral()
    }


    dependencies {
    compile('org.springframework.boot:spring-boot-starter-web')
    testCompile('org.springframework.boot:spring-boot-starter-test')
    }
    \n

    现在你可以使用下边的命令编译这个 docker 镜像,然后将它推送到远端仓库中分享给其他用户使用(本教程不介绍推送远端仓库的方法)。

    \n

    ./gradlew build buildDocker

    \n

    执行上边命令后,可以在本地的 docker 镜像仓库中看到,已经有了我们自己编译出来的 my-spring-boot-docker 镜像:

    \n

    \"\"

    \n

    然后你就可以像这样来运行它了:

    docker run -p 8080:8080 -t my-spring-boot-docker:0.0.1-SNAPSHOT

    \n
    \n

    这里说明一下 -p 的用途, -p 8080:8080 的意思是将本地的 8080 端口映射到容器的 8080 端口,因为容器相对于主机来说是完全隔离的,所以必须要有此设置,不然外部是无法访问到 8080 端口的。

    \n
    \n

    现在再次访问 localhost:8080 就可以看到 Hello Docker World 啦。

    \n

    当容器运行时你可以通过 docker ps 的命令看到正在运行的容器列表:

    \n

    \"\"

    \n

    而且可以通过 docker stop 加上这个容器的 ID 来停掉它:

    \n

    \"\"

    \n

    如果你想删掉这个容器,可以使用 docker rm + 容器 ID

    \n

    到此你已经为 Spring Boot 应用创建了一个 docker 容器,默认运行在容器内的 8080 端口上,我们在命令行中使用 -p 参数将它映射到主机的相同端口上。

    \n"},{"title":"Spring Boot Redis 蜜汁 Bug","url":"/2017/spring-boot-redis-mi-zhi-bug/","content":"

    今天遇到一个问题,搞了将近小半天也没有解决,最后我给出来的结论是 Spring Boot Redis Starter 里用的 Jedis 的 Bug 导致。

    \n

    我来描述一下问题过程,今天我为了测试 Spring Boot 和 Redis 相结合,写了一个 Demo 程序,先来连接我本地的 Redis 服务,测试了一下没有问题,然后我想试试连接远程的 Redis 服务,我之前在自己的一台服务器上搭了 Redis 服务,在我的一个正在运行的 Python 项目中用到了这个库,说明服务是没有问题的。并且我已经将这个服务端口监听到了 0.0.0.0,所以外部是可以访问的,唯一的区别是我加了密码验证。我在 application.yml 中配置 hostpassword 后,进行测试,发现报错:

    \n
    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

    redis.clients.jedis.exceptions.JedisConnectionException: Unexpected end of stream.
    \tat redis.clients.util.RedisInputStream.ensureFill(RedisInputStream.java:199) ~[jedis-2.9.0.jar:na]
    \tat redis.clients.util.RedisInputStream.readByte(RedisInputStream.java:40) ~[jedis-2.9.0.jar:na]
    \tat redis.clients.jedis.Protocol.process(Protocol.java:151) ~[jedis-2.9.0.jar:na]
    \tat redis.clients.jedis.Protocol.read(Protocol.java:215) ~[jedis-2.9.0.jar:na]
    \tat
    \t...
    \n

    然后我根据网上的给出的方案,尝试修改 spring.redis.pool 相关的各种参数,都没有解决。网上有的说是 Redis 的连接数太高导致的,我看了等下 Redis 的 info 都正常。

    \n

    我在本地用命令 redis-cli -h hostname -a password 也是可以连上的,先不输密码登录成功后用 AUTH + password 的方式也可以访问。

    \n

    因为我自己服务器上的 Redis 在使用中,不方便修改密码,所以我在公司 lc7 的服务器上搭了个 Redis,修改配置监听 0.0.0.0 不过没有设置密码,修改待测试程序的配置文件,发现可以读到,然后又把 lc7 的 Redis 服务加上了密码,再次测试还是没有问题。

    \n

    后来我猜是不是我自己服务器上的密码设置的太长了,我又把 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 来进行测试。

    \n

    创建项目

    Spring Boot 提供一个 web 工具叫做 Spring Initializer 来引导一个应用。访问 http://start.spring.io 然后按照下边的步骤来生成一个新的项目:

    \n
      \n
    1. 点击页面上的 Switch to full version
    2. \n
    3. 输入如下详情
        \n
      • Group: com.example
      • \n
      • Artifact: easy-notes
      • \n
      • Name: easy-notes
      • \n
      • Description: Rest API for Note Application
      • \n
      • Package Name: com.example.easynotes
      • \n
      • Packaging: jar
      • \n
      • Java Version: 1.8
      • \n
      • Dependencies: Web,JPA,MySQL
      • \n
      \n
    4. \n
    \n

    输入完所有详情后,将上边的 Generate a 改为 Gradle Project 点击 Generate Project 来生成并下载项目。Spring Initializer 将根据你输入的信息生成项目并提供一个 zip 包含所有项目目录。下一步解压下载下来的 zip 文件,并将导入到你喜欢的 IDE 中。

    \n

    \"\"

    \n

    探索目录结构

    下边是我们记事本程序的目录结构

    \n

    \"\"

    \n

    让我们来理解几个重要文件和目录的详情

    \n

    EasyNotesApplication

    这是我们 Spring Boot 应用的主要入口。

    \n
    package com.example.easynotes;

    import org.springframework.boot.SpringApplication;
    import org.springframework.boot.autoconfigure.SpringBootApplication;

    @SpringBootApplication
    public class EasyNotesApplication {

    public static void main(String[] args) {
    SpringApplication.run(EasyNotesApplication.class, args);
    }
    }
    \n

    它包含一个名为 @SpringBootApplication 的简单注解,这个注解是以下 Spring 注解的组合:

    \n
      \n
    • @Configuration:任何使用了 @Configuration注解的类都由 Spring 引导,并且也被视为其他 bean 定义的来源。
    • \n
    • @EnableAutoConfig:这个注解告诉 Spring 根据你在 build.gradle 文件中添加的依赖来自动配置你的应用。
      比如,如果 spring-data-jpa 位于 classpath 中,它会通过从 application.properties 文件中读取数据库属性来自动尝试配置一个 DataSource。
    • \n
    • @ComponentScan:它告诉 Spring 扫描并引导当前包(com.example.easynotes)和全部子包中定义的其他组件。
    • \n
    \n

    main() 方法调用 Spring Boot 的 SpringApplication.run() 方法启动这个应用。

    \n

    resources/

    顾名思义,这个目录用于存放所有静态资源、模板和属性文件。

    \n
      \n
    • resources/static 包含静态资源,如 css、js 和图片
    • \n
    • resources/templates 包含由 Spring 渲染的服务端模板
    • \n
    • resources/application.properties 这个文件非常重要,它包含应用范围的属性,Spring 读取这个文件中定义的属性来配置你的应用,你可以在这个文件中定义服务器默认端口、服务器上下文路径、数据库 URL 等
      可以参考此页面来了解 Spring Boot 中常用的应用属性。
    • \n
    \n

    EasyNotesApplicationTests

    在这里定义单元测试和集成测试

    \n

    build.gradle

    包含所有项目依赖

    \n

    配置 MySQL 数据库

    如果 spring-data-jpa 位于 classpath 中,Spring Boot 会尝试从 application.properties文件中读取数据库配置自动配置 DataSource。所以我们只需要添加配置,Spring Boot 将负责其他部分。

    \n

    我更习惯于使用 yml 的方式管理配置,所以我们将 application.properties 删掉,新建名为 application.yml 的文本文件。

    \n
    # Spring 数据源 (DataSourceAutoConfiguration & DataSourceProperties)
    spring:
    datasource:
    url: jdbc:mysql://localhost:3306/notes_app?useSSL=false
    username: root
    password: root
    jpa:
    hibernate:
    ddl-auto: update
    properties:
    hibernate:
    dialect: # Hibernate 属性,SQL 方言使得 Hibernate 为所选数据库生成更好的 SQL
    jackson:
    serialization:
    write-dates-as-timestamps: true
    \n

    我们需要在 MySQL 中创建一个名为 notes_app 的数据库并将配置文件中的 usernamepassword 属性改为你安装的 MySQL 对应的值。

    \n

    spring.jpa.properties.hibernate.dialectspring.jpa.hibernate.ddl-auto 这两个配置是提供给 hibernate 的,Spring Boot 使用 Hibernate 作为默认 JPA 实现。

    \n

    spring.jpa.hibernate.ddl-auto 配置用于数据库初始化,我使用 update 值作为属性。

    \n

    它做了两件事:

    \n
      \n
    • 当你定义一个领域模型,将自动在数据库中创建一个表,并将领域模型的字段映射到表中的对应列。
    • \n
    • 对领域模型的任何修改将触发表的更新。例如,如果你修改一个字段的名称或类型或者将其他字段添加到模型中,所有这些修改也会反映在映射表中。
    • \n
    \n

    对于 spring.jpa.hibernate.ddl-auto 属性来说使用 update 值对于开发阶段来说非常好,但是对于生产阶段,应该保留这个属性值为 validate,并使用数据库迁移工具来管理数据库结构的修改,如 Flyway

    \n

    创建 Note 模型

    接下来创建 Note 模型,我们 Note 模型有如下字段:

    \n
      \n
    • id:自增主键
    • \n
    • title:笔记的标题(非空字段)
    • \n
    • content:笔记的内容(非空字段)
    • \n
    • createAt:笔记的创建时间
    • \n
    • updateAt:笔记的更新时间
    • \n
    \n

    现在来看一下如何在 Spring 中对它进行建模。在 com.example.easynotes 创建一个名为 model 的包,并添加一个名为 Note.java 的类,内容如下:

    \n
    package com.example.easynotes.model;


    import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
    import org.springframework.data.annotation.CreatedDate;
    import org.springframework.data.annotation.LastModifiedDate;
    import org.springframework.data.jpa.domain.support.AuditingEntityListener;

    import javax.persistence.*;
    import javax.validation.constraints.NotBlank;
    import java.io.Serializable;
    import java.util.Date;

    @Entity
    @Table(name = "notes")
    @EntityListeners(AuditingEntityListener.class)
    @JsonIgnoreProperties(value = {"createdAt", "updatedAt"},
    allowGetters = true)
    public class Note implements Serializable {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @NotBlank
    private String title;

    @NotBlank
    private String content;

    @Column(nullable = false, updatable = false)
    @Temporal(TemporalType.TIMESTAMP)
    @CreatedDate
    private Date createdAt;

    @Column(nullable = false)
    @Temporal(TemporalType.TIMESTAMP)
    @LastModifiedDate
    private Date updatedAt;

    // getter and setter
    }
    \n
      \n
    • 你所有的域模型必须使用 @Entity 进行注解,他用于将该类标记为持久 Java 类
    • \n
    • @Table 注解用于提供此实体将映射到表的详细信息
    • \n
    • @Id 注解用于定义主键
    • \n
    • @GeneratedValue 注解用于定义主键生成策略,上例中我们声明主键是一个自增字段
    • \n
    • @NotBlank 注解用于验证被注释的字段不为 null 或 空
    • \n
    • @Column 注解用于定义被映射到注解字段列的属性,可以定一个多个属性如名称、长度、可为空、可更新等

      \n

      默认情况下,名为 createAt 的字段将映射到数据库表中名为 create_at 的列,即所有驼峰命名将使用下划线替代,如果你想映射这个字段到不同的列,可以使用以下命令指定它:

      \n
      @Column(name = "created_on")
      private String createdAt;
      \n
    • \n
    • @Temporal 注解与 java.util.Datejava.util.Calendar 类一起使用,它将 Java 对象中的时间和日期转换为兼容数据库的类型,反之亦然。

      \n
    • \n
    • @JsonIgnoreProperties 注解是一个 Jackson 注解,Spring Boot 使用 Jackson 在 Java 对象和 JSON 直接进行序列化和反序列化
    • \n
    \n

    使用这个注解是因为我们不希望客户端通过 rest api 提供 createdAtupdatedAt 的值,如果它们提供这些值,我们会简单忽略他们,但是我们将在 JSON 响应中包含这些值。

    \n

    开启 JPA 审计

    Note 模型中,我们分别用 @CreatedDate@LastModifiedDate 注解标注了 createdAtupdatedAt 字段。现在我们想要的效果是只要我们创建或更新实体,这些字段会自动填充。

    \n

    为了做到这一点,我们要做两件事:

    \n
      \n
    • 添加 Spring Data JPA 的 AuditingEntityListener 到领域模型中
      我们已经在 Note 模型中使用注解 @EntityListeners(AuditingEntityListener.class) 来完成了这个工作

      \n
    • \n
    • 在主应用程序中开启 JPA 审计
      打开 EasyNotesApplication.java 并添加 @EnableJpaAuditing 注解。

      \n
    • \n
    \n

    @SpringBootApplication
    @EnableJpaAuditing
    public class EasyNotesApplication {

    public static void main(String[] args) {
    SpringApplication.run(EasyNotesApplication.class, args);
    }
    }
    \n

    创建 NoteRepository 访问来自数据库的数据

    接下来我们要做的是创建一个仓库来访问数据库中的 Note 数据。

    \n

    Spring Data JPA 让我们覆盖这里,它带有一个 JpaRepository 接口,该接口定义了实体上所有 CURD 操作的方法,JpaRepository 的默认实现为 SimpleJpaRepository

    \n

    现在来创建仓库,首先在 com.example.easynotes 下创建一个名为 repository 的包,然后创建一个名为 NoteRepository 的接口并从 JpaRepository 扩展它:

    \n
    package com.example.easynotes.repository;

    import com.example.easynotes.model.Note;
    import org.springframework.data.jpa.repository.JpaRepository;
    import org.springframework.stereotype.Repository;

    @Repository
    public interface NoteRepository extends JpaRepository<Note, Long> {
    }
    \n

    请注意我们使用 @Repository 注解标注了接口,这会告诉 Spring 在组件扫描期间引导这个仓库。

    \n

    以上这些就是你在仓库层所要做的所有工作了,你现在可以使用像 save()findOne()findAll()count()delete() 等 JpaRepository 方法。

    \n

    你不需要实现这些方法,他们已经由 Spring Data JPA 的 SimpleJpaRepository 实现,这个实现在运行时被 Spring 自动插入。

    \n

    查看 SimpleJpaRepository 文档 中提供的所有方法。

    \n

    创建自定义业务异常

    我们将在后边定义 Rest API 用来创建、检索、更新和删除笔记。API 会在数据库找不到具有指定 ID 的笔记时抛出 ResourceNotFoundException 异常。

    \n

    以下是 ResourceNotFoundException 的定义,我们在 com.example.easynotes 中创建一个名为 exception 的包来存放这个异常类。

    \n
    package com.example.easynotes.exception;

    import org.springframework.http.HttpStatus;
    import org.springframework.web.bind.annotation.ResponseStatus;

    @ResponseStatus(value = HttpStatus.NOT_FOUND)
    public class ResourceNotFoundException extends RuntimeException {

    private String resourceName;

    private String fieldName;

    private Object fieldValue;

    public ResourceNotFoundException( String resourceName, String fieldName, Object fieldValue) {
    super(String.format("%s not found with %s : '%s'", resourceName, fieldName, fieldValue));
    this.resourceName = resourceName;
    this.fieldName = fieldName;
    this.fieldValue = fieldValue;
    }

    public String getResourceName() {
    return resourceName;
    }

    public String getFieldName() {
    return fieldName;
    }

    public Object getFieldValue() {
    return fieldValue;
    }

    }
    \n

    注意,上边的异常类使用了 @ResponseStatus 注解,在你的 Controller 中抛出此异常时,Spring boot 会相应指定的 HTTP 状态码。

    \n

    创建 NoteController

    最后一步,我们将编写 REST API 来创建、检索、更新和删除笔记。

    \n

    首先在 com.example.easynotes 中创建一个新的包 controller,然后创建一个新的类 NoteController.java 内容如下

    \n
    package com.example.easynotes.controller;

    import com.example.easynotes.repository.NoteRepository;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.web.bind.annotation.RequestMapping;
    import org.springframework.web.bind.annotation.RestController;

    @RestController
    @RequestMapping("/api")
    public class NoteController {

    @Autowired
    NoteRepository noteRepository;

    // Get All Notes

    // Create a new Note

    // Get a Single Note

    // Update a Note

    // Delete a Note

    }
    \n

    @RestController 注解是 Spring 中 @Controller@ResponseBody 注解的组合。

    \n

    @Controller 注解用于定义一个控制器,@ResponseBody 注解用来表示方法的返回值应该用作请求的响应体。

    \n

    @RequestMapping("/api") 声明这个控制器中所有 api 的 URL 将以 /api 开头。

    \n

    接下来我们来一个一个实现这些 api。

    \n

    1.获取所有笔记(GET /api/notes)

    // Get All Notes
    @GetMapping("/notes")
    public List<Note> getAllNotes() {
    return noteRepository.findAll();
    }
    \n

    上边的方法非常简单,它调用 JapRepository 的 findAll() 方法来检索数据库中所有的笔记并返回整个列表。

    \n

    另外,@GetMapping("/notes") 注解是 @RequestMapping(value="/notes", method=RequestMethod.GET) 的简写形式。

    \n

    2.创建一个新的笔记(POST /api/notes)

    // Create a new Note
    @PostMapping("/notes")
    public Note createNote(@Valid @RequestBody Note note) {
    return noteRepository.save(note);
    }
    \n

    @RequestBody 注解用于将请求体与方法参数绑定。

    \n

    @Valid 注解确保请求体是有效的,记不记得我们在 Note 模型中用 @NotBlank 注解标记了 Note 的 title 和 content。

    \n

    如果请求体中没有 title 或 content,Srping 将向客户端返回 400 BadRequest 错误。

    \n

    3.获取单个笔记(GET /api/notes/{noteId})

    // Get a Single Note
    @GetMapping("/notes/{id}")
    public Note getNoteById(@PathVariable(value = "id") Long noteId) {
    return noteRepository.findById(noteId)
    .orElseThrow(() -> new ResourceNotFoundException("Note", "id", noteId));
    }
    \n

    顾名思义,@PathVariable 注解用于将路径变量与方法参数绑定。

    \n

    在上面的方法中,只要没找到指定 ID 的笔记,我们就抛出一个 ResourceNotFoundException 异常。

    \n

    这将导致 Spring Boot 向客户端返回一个 404 Not Found 错误(我们已经为 ResourceNotFoundException 类添加了 @ResponseStatus(value=HttpStatus.NOT_FOUND) 注解)。

    \n

    4.更新笔记(PUT /api/notes/{noteId})

    // Update a Note
    @PutMapping("/notes/{id}")
    public Note updateNote(@PathVariable(value = "id") Long noteId,
    @Valid @RequestBody Note noteDetails) {

    Note note = noteRepository.findById(noteId)
    .orElseThrow(() -> new ResourceNotFoundException("Note", "id", noteId));

    note.setTitle(noteDetails.getTitle());
    note.setContent(noteDetails.getContent());

    Note updatedNote = noteRepository.save(note);
    return updatedNote;
    }
    \n

    5.删除笔记(DELETE /api/notes/{noteId})


    // Delete a Note
    @DeleteMapping("/notes/{id}")
    public ResponseEntity<?> deleteNote(@PathVariable(value = "id") Long noteId) {
    Note note = noteRepository.findById(noteId)
    .orElseThrow(() -> new ResourceNotFoundException("Note", "id", noteId));

    noteRepository.delete(note);

    return ResponseEntity.ok().build();
    }
    \n

    运行应用

    我们已经成功为我们的应用程序构建了所有的 api,现在运行该应用并测试 api。

    \n

    在你的 IDE 中直接运行 EasyNotesApplication 类即可,应用将使用 Spring Boot 的默认 tomcat 端口启动。

    \n

    接下来我们使用 postman 来测试我们的 api。

    \n

    测试 API

    使用 POST /api/notes 创建一个新的笔记

    \"\"

    \n

    使用 GET /api/notes 检索全部笔记

    \"\"

    \n

    使用 GET /api/notes/{noteId} 检索单个笔记

    \"\"

    \n

    使用 PUT /api/notes/{noteId} 更新一个笔记

    \"\"

    \n

    使用 DELETE /api/notes/{noteId} 删除一个笔记

    \"\"

    \n"},{"title":"SpringMVC 常用注解","url":"/2017/spring-chang-yong-zhu-jie/","content":"

    @Controller

      \n
    • 用于标注控制层组件
    • \n
    • @Controller 用于标记在一个类上,使用它标记的类就是一个 SpringMVC Controller 对象,分发处理器将会扫描使用了该注解的类方法,并检测该方法是否使用了 @RequestMapping 注解
    • \n
    • 可以把 Request 请求 header 部分的值绑定到方法参数上
    • \n
    \n

    @RestController

      \n
    • 相当于 @Controller@ResponseBody 的组合效果
    • \n
    \n

    @Component

      \n
    • 泛指组件,当组件不好归类的时候,我们可以使用这个注解进行标注
    • \n
    \n

    @Respository

      \n
    • 用于注解 dao 层,在 daoImpl 类上面注解
    • \n
    \n

    @Service

      \n
    • 用于注解业务组件
    • \n
    \n
    \n

    @ResponseBody

      \n
    • 异步请求
    • \n
    • 该注解用于将 Controller 的方法返回的对象通过适当的 HttpMessageConverter 转换为指定格式后,写入到 Response 对象的 body 数据区
    • \n
    • 返回的数据不是 html 标签的页面,而是其他某种格式的数据时(如json、xml)使用
    • \n
    \n

    @RequestMapping

      \n
    • 一个用来处理请求地址映射的注解,可用于类或方法上。用于类上,表示类的所有响应请求的方法都是以该地址作为父路径
    • \n
    \n

    @Autowired

      \n
    • 它可以对类成员变量、方法及构造函数进行标注,完成自动装配的工作。通过 @Autowired 的使用来消除 setget 方法
    • \n
    \n

    @PathVariable

      \n
    • 用于将请求 URL 中的模板变量映射到功能处理方法的参数上,即取出 URL 模板中的变量作为参数
    • \n
    \n

    @RequestParam

      \n
    • 主要用于在 Spring MVC 后台控制层获取参数,类似的一种做法是: request.getParamter("name")
    • \n
    \n

    @RequestHeader

      \n
    • 可以把 request 请求 header 部分的值绑定到方法参数上
    • \n
    \n
    \n

    @SessionAttribute

      \n
    • 用来映射 HttpSession 中 attribute 对象的值,将值放到 session 作用域中,写在 class 上面
    • \n
    \n

    @Valid

      \n
    • 实体校验数据,可结合 hibernate validator 一起使用
    • \n
    \n

    @CookieValue

      \n
    • 用来获取 Cookies 的值
    • \n
    \n

    @ModelAttribute

    "},{"title":"--spring.profiles.active 穿透性","url":"/2017/spring-profiles-active-chuan-tou-xing/","content":"

    今天在帮助 datamaster 接入配置中心时发现一个问题,就是如果在应用的 application.yml 像这样:

    \n
    spring:
    application:
    name: datamaster-scheduler
    profiles:
    active: lc10
    \n

    指定环境的话,在启动时会自动寻找 datamaster-scheduler-lc10.yml 这个配置文件,不论使用 bootrun 启动或者打成 jar 包都没有问题。

    \n

    但是如果不在 application.yml 中指定 spring.profiles.active,而是打成 jar 包,使用 --spring.profiles.active=lc10 参数启动的话,就注册不上 Eureka,所以也就没办法正常拉取配置文件了。

    \n

    出现这个问题可以理解,因为我们在封装微服务接入的 starter 时,只定义了两个 active:

    \n
    ---
    spring:
    profiles: dev
    rabbitmq:
    host: 172.24.8.100
    port: 5672
    username: admin
    password: admin
    zipkin:
    base-url: http://172.24.8.100:7030

    eureka:
    client:
    service-url:
    defaultZone: http://172.24.8.100:7011/eureka/,http://172.24.8.100:7012/eureka/,http://172.24.8.100:7013/eureka/

    ---
    spring:
    profiles: beta
    rabbitmq:
    host: 172.24.8.100
    port: 5672
    username: admin
    password: admin
    zipkin:
    base-url: http://172.24.8.100:7030
    eureka:
    client:
    service-url:
    defaultZone: http://172.24.8.100:7011/eureka/,http://172.24.8.100:7012/eureka/,http://172.24.8.100:7013/eureka/
    \n

    所以使用这两种之外的环境是找不到配置中心的地址的,但是之前那种方式所得到的行为就不太理解了。

    \n

    这个问题我得到的结论是这样的:在应用系统的 application.yml 中定义的 active 是不具有穿透性的,所以我们的 微服务 starter 是不会得到这里定义的 active 的,而且 微服务starter 中的 bootstrap.yml 中定义了:

    \n
    spring:
    profiles:
    active: dev
    \n

    所以在没有指定时会使用 dev,没有任何问题。

    \n

    使用 --spring.profiles.active=lc10 指定的 active 具有穿透性,会让这个应用系统依赖的其他组件也使用指定的 active,所以这个时候 微服务 starter 也会切换到 lc10 的 active,因为我们没有在 starter 中配 lc10 对应的 active,也就就相当于没有指定 eureka 的注册地址,所以接下来的所有流程就都跑不通了。

    \n

    为了解决这个问题,我在 微服务 starter 的 bootstrap.yml 中加上了默认的 eureka 注册地址和其他需要用到的配置,这样即便是指定的环境不存在的话也没有任何问题。

    \n

    之后在为客户部署时,只需要在 微服务 starter 中增加一个客户所对应的环境,然后启动应用时指定客户的 active 就可以了。

    \n"},{"title":"解决低版本SpringBoot使用langchain4j Azure 冲突问题","url":"/2024/springboot-azure-openai/","content":"

    新公司使用的Java技术栈,我们有部分新业务需要调用 OpenAI 的接口进行交互,之前我找了一个比较轻量的SDK来调用OpenAI的接口,地址是:https://github.com/Lambdua/openai4j ,这个库作为日常使用足够了,但是一些高阶能力无法满足,而这些也是我们未来会用到的,比如:

    \n
      \n
    • 对接微软 Azure 上部署的 GPT 模型
    • \n
    • Function Calling
    • \n
    • RAG
    • \n
    \n

    把第一版功能完成后,这几天工作不是那么多,于是我从Github上找到了这个库https://github.com/langchain4j/langchain4j ,从名字就能看出来,这个项目是参考的 Python 的LangChain,Java 库的命名很有意思,很喜欢叫 xxxx4j,4j 的意思是 for Java,比如 log4j。

    \n

    我大致看了一下介绍,功能还算完备,给出的demo来看使用方式上可读性也很高,更重要的一点是支持古老的Java8。于是我在项目中进行了引入,将已有代码进行了改造,在跑直接调用 OpenAI 的例子时很顺利,当我切换为 Azure 后问题出现了,报错堆栈如下:

    \n
    Exception in thread \"main\" java.lang.NoClassDefFoundError: reactor/core/Disposable
    \tat java.lang.ClassLoader.defineClass1(Native Method)
    \tat java.lang.ClassLoader.defineClass(ClassLoader.java:756)
    \tat java.security.SecureClassLoader.defineClass(SecureClassLoader.java:142)
    \tat java.net.URLClassLoader.defineClass(URLClassLoader.java:473)
    \tat java.net.URLClassLoader.access$100(URLClassLoader.java:74)
    \tat java.net.URLClassLoader$1.run(URLClassLoader.java:369)
    \tat java.net.URLClassLoader$1.run(URLClassLoader.java:363)
    \tat java.security.AccessController.doPrivileged(Native Method)
    \tat java.net.URLClassLoader.findClass(URLClassLoader.java:362)
    \tat java.lang.ClassLoader.loadClass(ClassLoader.java:418)
    \tat sun.misc.Launcher$AppClassLoader.loadClass(Launcher.java:352)
    \tat java.lang.ClassLoader.loadClass(ClassLoader.java:351)
    \tat com.azure.core.http.netty.NettyAsyncHttpClientProvider.createInstance(NettyAsyncHttpClientProvider.java:81)
    \tat dev.langchain4j.model.azure.InternalAzureOpenAiHelper.setupOpenAIClientBuilder(InternalAzureOpenAiHelper.java:71)
    \tat dev.langchain4j.model.azure.InternalAzureOpenAiHelper.setupSyncClient(InternalAzureOpenAiHelper.java:51)
    \tat dev.langchain4j.model.azure.AzureOpenAiChatModel.<init>(AzureOpenAiChatModel.java:123)
    \tat dev.langchain4j.model.azure.AzureOpenAiChatModel$Builder.build(AzureOpenAiChatModel.java:536)
    \n

    我按照堆栈的引导,一步一步去看代码,发现是在创建 HttpClient 对象时挂了,我进到 ConnectionProvider 源码中查看,确实找不到上边说的 Disposable 类,这个类来自 reactor-core 包。通过IDE跳转进的路径看到,目前项目中所使用的 reactor-core 版本是 2.0.8.RELEASE,我找到最新 3.6.7 版本的 reactor-core 源码看了下是有Disposable 这个类的。

    \n

    一开始我认为是 langchain4j 的这个项目有问题,去 Github 的 Issue 中搜了下并没有相关的提问,于是我自己开始尝试动手解决,尝试了以下几种方式都不行:

    \n
      \n
    1. 直接在项目中引入最新版本的 reactor-core
    2. \n
    3. 排除(exclusions) langchain4j-azure-open-ai 下的 reactor-core 依赖,保证我自己引入的最新版本生效
    4. \n
    5. 引入 reactor-netty-core 的最新版
    6. \n
    7. 引入全部 langchain4j 的依赖
    8. \n
    9. 重启IDE
    10. \n
    11. 重启电脑
    12. \n
    \n

    在做上边的第2步时,启动调试后可以看到,IDE在进入ConnectionProvider 后确实可以正常跳转进Disposable 了,但最终还是报错。通过依赖分析也没有发现和 reactor 的任何冲突,一直搞到晚上下班也没解决。

    \n

    今天早上上班后我换了个思路来排查这个项目,创建了一个新项目,只引入 langchain4j 的依赖,可以正常执行,接下来我把我们项目中其他依赖项引进来,发现还是没问题,当我把 parent 引入后问题出现了。虽然 parent 的 pom 文件在远端,但IDEA提供了一个功能,可以修改本地的文件来进行调试,我用二分法删除 parent 中的依赖,最终将问题定位在了:

    \n
    <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-dependencies</artifactId>
    <version>${spring.boot.version}</version>
    <type>pom</type>
    <scope>import</scope>
    </dependency>
    \n

    parent 中 spring.boot.version 的值是 1.5.7.RELEASE ,我在上上家公司写Java时就有这个版本了,是个非常老的版本,但升级 SpringBoot 关联的问题会更多。我继续深入进去看,在 spring-boot-dependencies 的 pom 文件中 properties 指定了reactor.version2.0.8.RELEASE,这下破案了。之前我无法通过依赖分析找到冲突,也是因为依赖是在 parent 指定的,且这个依赖版本无法在后续进行修改。

    \n

    有种覆盖 parent 版本号的方式是在自己项目的父 pom 中的dependencyManagement 下进行声明,我尝试在 dependencyManagement 加上如下片段:

    \n
    <dependency>
    <groupId>io.projectreactor</groupId>
    <artifactId>reactor-core</artifactId>
    <version>3.6.7</version>
    </dependency>
    \n

    此时报了另一个错误:

    \n
    java.lang.VerifyError: class io.netty.channel.kqueue.AbstractKQueueChannel$AbstractKQueueUnsafe overrides final method close.(Lio/netty/channel/ChannelPromise;)V

    \tat java.lang.ClassLoader.defineClass1(Native Method)
    \tat java.lang.ClassLoader.defineClass(ClassLoader.java:756)
    \tat java.security.SecureClassLoader.defineClass(SecureClassLoader.java:142)
    \tat java.net.URLClassLoader.defineClass(URLClassLoader.java:473)
    \tat java.net.URLClassLoader.access$100(URLClassLoader.java:74)
    \tat java.net.URLClassLoader$1.run(URLClassLoader.java:369)
    \tat java.net.URLClassLoader$1.run(URLClassLoader.java:363)
    \tat java.security.AccessController.doPrivileged(Native Method)
    \tat java.net.URLClassLoader.findClass(URLClassLoader.java:362)
    \tat java.lang.ClassLoader.loadClass(ClassLoader.java:418)
    \tat sun.misc.Launcher$AppClassLoader.loadClass(Launcher.java:352)
    \tat java.lang.ClassLoader.loadClass(ClassLoader.java:351)
    \tat reactor.netty.resources.DefaultLoopKQueue.getChannel(DefaultLoopKQueue.java:50)
    \tat reactor.netty.resources.LoopResources.onChannel(LoopResources.java:243)
    \tat reactor.netty.tcp.TcpResources.onChannel(TcpResources.java:251)
    \tat reactor.netty.transport.TransportConfig.lambda$connectionFactory$1(TransportConfig.java:277)
    \tat reactor.netty.transport.TransportConnector.doInitAndRegister(TransportConnector.java:277)
    \tat reactor.netty.transport.TransportConnector.connect(TransportConnector.java:164)
    \tat reactor.netty.transport.TransportConnector.connect(TransportConnector.java:123)
    \tat reactor.netty.resources.DefaultPooledConnectionProvider$PooledConnectionAllocator.lambda$connectChannel$0(DefaultPooledConnectionProvider.java:519)
    \tat reactor.core.publisher.MonoCreate.subscribe(MonoCreate.java:61)
    \tat reactor.core.publisher.Mono.subscribe(Mono.java:4568)
    \tat reactor.core.publisher.Mono.subscribeWith(Mono.java:4634)
    \tat reactor.core.publisher.Mono.subscribe(Mono.java:4534)
    \tat reactor.core.publisher.Mono.subscribe(Mono.java:4470)
    \tat reactor.netty.internal.shaded.reactor.pool.SimpleDequePool.drainLoop(SimpleDequePool.java:437)
    \tat reactor.netty.internal.shaded.reactor.pool.SimpleDequePool.pendingOffer(SimpleDequePool.java:600)
    \tat reactor.netty.internal.shaded.reactor.pool.SimpleDequePool.doAcquire(SimpleDequePool.java:296)
    \tat reactor.netty.internal.shaded.reactor.pool.AbstractPool$Borrower.request(AbstractPool.java:430)
    \tat reactor.netty.resources.DefaultPooledConnectionProvider$DisposableAcquire.onSubscribe(DefaultPooledConnectionProvider.java:204)
    \tat reactor.netty.internal.shaded.reactor.pool.SimpleDequePool$QueueBorrowerMono.subscribe(SimpleDequePool.java:720)
    \tat reactor.netty.resources.PooledConnectionProvider.lambda$acquire$2(PooledConnectionProvider.java:170)
    \tat reactor.core.publisher.MonoCreate.subscribe(MonoCreate.java:61)
    \tat reactor.netty.http.client.HttpClientConnect$MonoHttpConnect.lambda$subscribe$0(HttpClientConnect.java:273)
    \tat reactor.core.publisher.MonoCreate.subscribe(MonoCreate.java:61)
    \tat reactor.core.publisher.FluxRetryWhen.subscribe(FluxRetryWhen.java:81)
    \tat reactor.core.publisher.MonoRetryWhen.subscribeOrReturn(MonoRetryWhen.java:46)
    \tat reactor.core.publisher.InternalMonoOperator.subscribe(InternalMonoOperator.java:63)
    \tat reactor.netty.http.client.HttpClientConnect$MonoHttpConnect.subscribe(HttpClientConnect.java:276)
    \tat reactor.core.publisher.Mono.subscribe(Mono.java:4568)
    \tat reactor.core.publisher.Mono.block(Mono.java:1778)
    \tat com.azure.core.http.netty.NettyAsyncHttpClient.sendSync(NettyAsyncHttpClient.java:199)
    \tat com.azure.core.http.HttpPipelineNextSyncPolicy.processSync(HttpPipelineNextSyncPolicy.java:51)
    \tat com.azure.core.http.policy.HttpLoggingPolicy.processSync(HttpLoggingPolicy.java:183)
    \tat com.azure.core.http.HttpPipelineNextSyncPolicy.processSync(HttpPipelineNextSyncPolicy.java:53)
    \tat com.azure.core.implementation.http.policy.InstrumentationPolicy.processSync(InstrumentationPolicy.java:101)
    \tat com.azure.core.http.HttpPipelineNextSyncPolicy.processSync(HttpPipelineNextSyncPolicy.java:53)
    \tat com.azure.core.http.policy.KeyCredentialPolicy.processSync(KeyCredentialPolicy.java:115)
    \tat com.azure.core.http.HttpPipelineNextSyncPolicy.processSync(HttpPipelineNextSyncPolicy.java:53)
    \tat com.azure.core.http.policy.CookiePolicy.processSync(CookiePolicy.java:73)
    \tat com.azure.core.http.HttpPipelineNextSyncPolicy.processSync(HttpPipelineNextSyncPolicy.java:53)
    \tat com.azure.core.http.policy.AddDatePolicy.processSync(AddDatePolicy.java:50)
    \tat com.azure.core.http.HttpPipelineNextSyncPolicy.processSync(HttpPipelineNextSyncPolicy.java:53)
    \tat com.azure.core.http.policy.RetryPolicy.attemptSync(RetryPolicy.java:211)
    \tat com.azure.core.http.policy.RetryPolicy.processSync(RetryPolicy.java:161)
    \tat com.azure.core.http.HttpPipelineNextSyncPolicy.processSync(HttpPipelineNextSyncPolicy.java:53)
    \tat com.azure.core.http.policy.AddHeadersPolicy.processSync(AddHeadersPolicy.java:66)
    \tat com.azure.core.http.HttpPipelineNextSyncPolicy.processSync(HttpPipelineNextSyncPolicy.java:53)
    \tat com.azure.core.http.policy.AddHeadersFromContextPolicy.processSync(AddHeadersFromContextPolicy.java:67)
    \tat com.azure.core.http.HttpPipelineNextSyncPolicy.processSync(HttpPipelineNextSyncPolicy.java:53)
    \tat com.azure.core.http.policy.RequestIdPolicy.processSync(RequestIdPolicy.java:77)
    \tat com.azure.core.http.HttpPipelineNextSyncPolicy.processSync(HttpPipelineNextSyncPolicy.java:53)
    \tat com.azure.core.http.policy.HttpPipelineSyncPolicy.processSync(HttpPipelineSyncPolicy.java:51)
    \tat com.azure.core.http.policy.UserAgentPolicy.processSync(UserAgentPolicy.java:174)
    \tat com.azure.core.http.HttpPipelineNextSyncPolicy.processSync(HttpPipelineNextSyncPolicy.java:53)
    \tat com.azure.core.http.HttpPipeline.sendSync(HttpPipeline.java:138)
    \tat com.azure.core.implementation.http.rest.SyncRestProxy.send(SyncRestProxy.java:62)
    \tat com.azure.core.implementation.http.rest.SyncRestProxy.invoke(SyncRestProxy.java:83)
    \tat com.azure.core.implementation.http.rest.RestProxyBase.invoke(RestProxyBase.java:124)
    \tat com.azure.core.http.rest.RestProxy.invoke(RestProxy.java:95)
    \tat com.sun.proxy.$Proxy24.getChatCompletionsSync(Unknown Source)
    \tat com.azure.ai.openai.implementation.OpenAIClientImpl.getChatCompletionsWithResponse(OpenAIClientImpl.java:1444)
    \tat com.azure.ai.openai.OpenAIClient.getChatCompletionsWithResponse(OpenAIClient.java:318)
    \tat com.azure.ai.openai.OpenAIClient.getChatCompletions(OpenAIClient.java:685)
    \tat dev.langchain4j.model.azure.AzureOpenAiChatModel.generate(AzureOpenAiChatModel.java:257)
    \tat dev.langchain4j.model.azure.AzureOpenAiChatModel.generate(AzureOpenAiChatModel.java:215)
    \n

    回到最开始的问题,报错误的根本原因是,初始化 Azure模型时需要构造一个 HttpClient,默认情况下会使用ConnectionProvider 来构造。看了下 AzureOpenAiChatModel 的 builder 方法,支持自己传入 OpenAIClient,而 OpenAIClient 可以自己构造 HttpClient,通过这个文档看到 https://learn.microsoft.com/en-us/azure/developer/java/sdk/http-client-pipeline HttpClient 有多种实现,其中可以用 OkHttpClient 来实现,于是我进行了以下魔改:

    \n
    private static OpenAIClient setupSyncClient(String endpoint, String serviceVersion, Object credential, Duration timeout, Integer maxRetries, ProxyOptions proxyOptions, boolean logRequestsAndResponses) {
    OpenAIClientBuilder openAIClientBuilder = setupOpenAIClientBuilder(endpoint, serviceVersion, credential, timeout, maxRetries, proxyOptions, logRequestsAndResponses);
    return openAIClientBuilder.buildClient();
    }

    private static OpenAIClientBuilder setupOpenAIClientBuilder(String endpoint, String serviceVersion, Object credential, Duration timeout, Integer maxRetries, ProxyOptions proxyOptions, boolean logRequestsAndResponses) {
    timeout = getOrDefault(timeout, ofSeconds(60));
    HttpClientOptions clientOptions = new HttpClientOptions();
    clientOptions.setConnectTimeout(timeout);
    clientOptions.setResponseTimeout(timeout);
    clientOptions.setReadTimeout(timeout);
    clientOptions.setWriteTimeout(timeout);
    clientOptions.setProxyOptions(proxyOptions);

    Header header = new Header(\"User-Agent\", \"langchain4j-azure-openai\");
    clientOptions.setHeaders(Collections.singletonList(header));
    // HttpClient httpClient = new NettyAsyncHttpClientProvider().createInstance(clientOptions);
    HttpClient httpClient = new OkHttpAsyncClientProvider().createInstance(clientOptions);

    HttpLogOptions httpLogOptions = new HttpLogOptions();
    if (logRequestsAndResponses) {
    httpLogOptions.setLogLevel(HttpLogDetailLevel.BODY_AND_HEADERS);
    }

    maxRetries = getOrDefault(maxRetries, 3);
    ExponentialBackoffOptions exponentialBackoffOptions = new ExponentialBackoffOptions();
    exponentialBackoffOptions.setMaxRetries(maxRetries);
    RetryOptions retryOptions = new RetryOptions(exponentialBackoffOptions);

    OpenAIClientBuilder openAIClientBuilder = new OpenAIClientBuilder()
    .endpoint(ensureNotBlank(endpoint, \"endpoint\"))
    .serviceVersion(getOpenAIServiceVersion(serviceVersion))
    .httpClient(httpClient)
    .clientOptions(clientOptions)
    .httpLogOptions(httpLogOptions)
    .retryOptions(retryOptions);

    if (credential instanceof String) {
    openAIClientBuilder.credential(new AzureKeyCredential((String) credential));
    } else if (credential instanceof KeyCredential) {
    openAIClientBuilder.credential((KeyCredential) credential);
    } else if (credential instanceof TokenCredential) {
    openAIClientBuilder.credential((TokenCredential) credential);
    } else {
    throw new IllegalArgumentException(\"Unsupported credential type: \" + credential.getClass());
    }

    return openAIClientBuilder;
    }

    private static OpenAIServiceVersion getOpenAIServiceVersion(String serviceVersion) {
    for (OpenAIServiceVersion version : OpenAIServiceVersion.values()) {
    if (version.getVersion().equals(serviceVersion)) {
    return version;
    }
    }
    return OpenAIServiceVersion.getLatest();
    }
    \n

    从开源代码中拷贝出 setupSyncClientsetupOpenAIClientBuilder 方法,并对setupOpenAIClientBuilder 中的HttpClient httpClient 的创建逻辑进行了调整

    \n
    // before
    HttpClient httpClient = new NettyAsyncHttpClientProvider().createInstance(clientOptions);
    // after
    HttpClient httpClient = new OkHttpAsyncClientProvider().createInstance(clientOptions);
    \n

    初始化Azure模型时传入我自己的 client:

    \n
    // 默认生成的client使用NettyAsyncHttpClientProvider和SpringBoot所依赖的版本不兼容,改用OkHttpAsyncClientProvider进行重写
    OpenAIClient client = setupSyncClient(System.getenv(\"AZURE_OPENAI_ENDPOINT\"), \"\",
    System.getenv(\"AZURE_OPENAI_API_KEY\"), ofSeconds(30), 2, null, true);

    model = AzureOpenAiChatModel.builder()
    .openAIClient(client)
    .deploymentName(modelName)
    .temperature(0.0)
    .build();
    \n

    并在工程中引入 azure-core-http-okhttp 的依赖

    \n
    <dependency>
    <groupId>com.azure</groupId>
    <artifactId>azure-core-http-okhttp</artifactId>
    <version>1.12.0</version>
    </dependency>
    \n

    再次执行还是报错了,不过这次的错误变为:

    \n
    java.lang.NoClassDefFoundError: reactor/util/context/ContextView

    \tat com.azure.core.http.rest.RestProxy.<init>(RestProxy.java:56)
    \tat com.azure.core.http.rest.RestProxy.create(RestProxy.java:140)
    \tat com.azure.ai.openai.implementation.OpenAIClientImpl.<init>(OpenAIClientImpl.java:144)
    \tat com.azure.ai.openai.OpenAIClientBuilder.buildInnerClient(OpenAIClientBuilder.java:283)
    \tat com.azure.ai.openai.OpenAIClientBuilder.buildClient(OpenAIClientBuilder.java:351)
    \n

    还是 reactor 的问题,但可以看到,现在已经不再使用 reactor.core.Disposable 了,也许升级一下 reactor-core 可以解决,我再次在项目的 parent 的dependencyManagement 下引入

    \n
    <dependency>
    <groupId>io.projectreactor</groupId>
    <artifactId>reactor-core</artifactId>
    <version>3.6.7</version>
    </dependency>
    \n

    再次尝试,问题解决。

    \n"},{"title":"通过蜜罐技术自制一个暴力破解字典库","url":"/2019/ssh-honeypot/","content":"

    \"\"

    \n
    \n

    蜜罐指具有缺陷的,用于吸引网络的计算机病毒侵占以便用于病毒的研究和破解的计算机。

    \n
    \n

    互联网就像一个黑暗丛林,当你拥有一个面向公网的服务器时,永远不知道会有多少双眼睛在盯着你。

    \n

    基于这个信条,我相信我的几台 vps 经常受到各种 ssh 的暴力破解的骚扰,为了安全考虑我也早就把默认 ssh 端口号 22 改为了某个随机值(有些是运营商强行修改)。

    \n

    前段时间突发奇想,是否可以监听下 22 端口,感受下在这个黑暗丛林中来自各方的打击?继而又想到,既然来感受打击,为何不把这些打击详细记录一下,假以时日,我是不是就可以得到一个「丛林常用爆破密码库」了?

    \n

    部署

    说干就干,选择了我其中一个坐落于米国的服务器来部署蜜罐程序。修改 ssh 默认端口号的步骤就不再介绍了,直奔主题。

    \n

    为了方便,我通过 Docker 来部署,只需 1 行命令:

    \n
    docker run -itd --name ssh-honeypot -p 22:22 txt3rob/docker-ssh-honey
    \n

    这里用 Docker 启动了一个 ssh 蜜罐镜像,然后把蜜罐的 22 端口映射到本地 22 端口,验证一下 22 端口的开放情况:

    \n
    # lsof -i:22
    COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME
    docker-pr 20832 root 4u IPv6 22035968 0t0 TCP *:ssh (LISTEN)
    \n

    后边就坐等蜜罐来收集登录信息吧。

    \n

    程序默认把登录日志输出到 Docker 容器的控制台中,日志格式如下:

    \n
    [Thu Nov  7 17:59:00 2019] 187.189.55.192 root 123456
    [Thu Nov 7 17:59:00 2019] 187.189.55.192 root password
    [Thu Nov 7 17:59:00 2019] 187.189.55.192 root default
    [Thu Nov 7 17:59:01 2019] 187.189.55.192 root root
    [Thu Nov 7 17:59:01 2019] 187.189.55.192 root 000000
    [Thu Nov 7 17:59:01 2019] 187.189.55.192 root 111111
    [Thu Nov 7 17:59:01 2019] 187.189.55.192 root password
    \n

    我们可以通过管道(|) + 重定向(>)的方式把结果导出出来:

    \n
    docker logs  $(docker ps -f name=ssh-honeypot -q) | grep -v 'Error exchanging' | grep -v 'Session' | awk '{print $6, $7, $8}' > ./ssh_password.log
    \n

    上边命令只保留了 ip、username、password 三列,同时过滤掉了程序自己打印的日志(包含 Session或者 Error exchanging 的行)。

    \n

    拿到数据后需要再进行一下简单的清洗:

    去掉行首行尾的空格

    sed -i 's/^[ \\t]*//g' ssh_password.log
    sed -i 's/[ \\t]*$//g' ssh_password.log

    \n

    去除空行

    \n
    sed -i '/^$/d' ssh_password.log
    \n

    去掉数据结尾的 ^M

    \n
    dos2unix -f ssh_password.log
    \n

    收网

    运行三周后,我一共收到了 1340万+ 的用户名密码(看到结果后有些震惊),去重后(cat ssh_password.log | awk '{print $2$3}' | uniq -c | wc -l)也有 1290万,之后我对这些数据进行了统计。

    \n

    统计常用用户名的 top5:

    # 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
    admin:7179731
    root:6236949
    test:1062
    guest:814
    mysql:799
    \n

    统计常用密码的 top5:

    # 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
    admin:727668
    12345:726670
    1234:726205
    password:725527
    default:724872
    \n

    统计常用用户名密码组合的 top5:

    # 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
    admin_admin:478249
    admin_1234:478020
    admin_pfsense:477780
    admin_12345:477733
    admin_admin1234:477222
    \n

    数据很有意思,也确实是我们最常用的那些用户名密码。

    \n

    关于安全

    简单通过修改 ssh 端口号的方式也不是最为稳妥的方法,真正安全的做法应该是:

    \n
      \n
    1. 配置 ssh 密钥
    2. \n
    3. 禁用密码登录
    4. \n
    5. 配置 ssh ip 登录白名单
    6. \n
    \n

    最后

    我把我的蜜罐中采到的蜂蜜分享出来,下载链接:http://developer.jpanj.com/ssh_password.log

    \n

    大家一起来享用这份喜悦吧。

    \n"},{"title":"开始刷 leetcode 了","url":"/2022/start-leetcode/","content":"

    我从上个月 26 号开始每天做 1-2 道 leetcode 算法题,到今天刚好一个月时间。当时在 github 上建了个私有仓库,今天也公开了,其中一个目的也是为了每天督促自己。之前不想公开的原因是担心后边真的有面试的时候面试官认为我是个刷题选手,但是 whatever 无所谓了,反正短期内也没打算换工作

    \n

    https://github.com/Panmax/go-leetcode

    \n

    我从毕业刚工作开始面试就没有在刷算法题上下过功夫,觉得意义不是很大,到现在也是这么认为。

    \n

    这次开始做题的目的是考虑到人过了 30,记忆力和思维力都不如年轻时候了,所以需要一些刻意练习,在这里记录一下做算法题的过程。每天写写题倒不是为了去面试,而是为了保持思维的敏捷,语法的熟练,以及对算法的理解。

    \n

    做题的另一个契机是公司每隔一段时间会抽一些人去写三道算法题,评估下公司研发人员的平均水平,这给了我一个开始的提示。最近刚好在读一本书:《福格行为模型》,里边提到开始一个行为需要是三个要素:动机+能力+触发器。我的触发器是公司的水平测验,动机是提升自己思维敏感度,能力方面自己还是不错的,所以最终促成了这个行为的实施。

    \n

    我不是每天疯狂的做新题,毕竟不是为了突击面试,而是每天做 1-2 道新题(根据题的难易程度而定),回顾 3-4 道之前的题目。所以在每个目录下可以看到一个 main.go 文件是我第一次做这道题的代码,其他 review_<日期>.go 文件是我复习的代码。

    \n

    比如 206-reverse-linked-list 目录,意思是 leetcode 里的第 206 道题,我在 8 月 2 号、8 月 5 号、8 月 15 号复习过。

    \n
    .
    ├── main.go
    ├── review_20220802.go
    ├── review_20220805.go
    └── review_20220814.go
    \n

    当天该复习哪道题是我通过 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

    itty-bitty

    https://write.jiapan.me

    \n

    匿名发布内容,不需要后端存储内容,而是把内容编码到 URL 上,比如打开这个链接 就可以看到我写的这段话了。

    \n

    \n

    还可以给字体改颜色,加超链接、插入图片等富文本功能。

    \n

    excalidraw

    https://draw.jiapan.me/

    \n

    一个制作手绘图的网站,功能很强大,自己部署的纯静态版本除了不支持多人协作,其他功能都是完整的。

    \n

    不支持登录,你绘制的内容保存在你的浏览器里,只要不清浏览器内容,下次访问内容不会丢失。

    \n

    \n

    password-generator

    https://password.jiapan.me/

    \n

    给一个你常用、好记的密码,并填写一个区分场景,这个工具会帮你转成一个强度很高的密码,每次你在输入密码时可以来这里转换出你的密码。

    \n

    比如下图中,我生成了一个用于 QQ 登录的密码,下次登录 QQ 时来这里查我的密码是什么就可以了。密码是纯前端生成的,没有任何后端逻辑。

    \n

    \n

    ddia

    https://ddia.jiapan.me

    \n

    ddia 那本书中文翻译的开源版本,没什么用,放在自己域名下就是好玩🤗。

    \n

    \n

    Blog

    最后我索性把自己当前这个博客也托管到了 Cloudflare 的 Pages,之前是把静态页放在一个海外的服务器上,前边用 Cloudflare 的 DNS 做一次代理,现在一身轻松了。

    \n

    以上这些我都是让 Cloudflare Pages 关联了我 GitHub 上的 Repo,当那些 Repo 有更新这些网站也会跟着自动更新,对应的 Repo 分别是:

    \n\n

    \n"},{"title":"《暴雨下载病房里》节选","url":"/2022/storm-in-ward-select/","content":"

    五月初的时候读了一本短篇小说集《暴雨下载病房里》,作者叫苏方,之前并没有读过她的其他书,这本书是在「小宇宙」中一档叫「文化有限」的节目听到的,感觉苏方的写作风格和王烁的有点像,都带着一股子痞劲。

    \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
    1. 每次传递过程中都会有好奇心比较旺盛的同学先打开信看一遍然后才继续往下传
    2. \n
    3. 遇到爱搞恶作剧的同学还会修改信的内容,比如有一次我告诉小h,今晚9点操场见,但是这个同学改成了今晚7点操场见,结果导致小h提前两小时到了操场,因为这事俩人差点分手
    4. \n
    5. 甚至有更讨厌的同学,冒充大p给小h写信
    6. \n
    \n
    \n

    以上三中情况对应的是 HTTP 协议传输时存在的风险:
    窃听风险:第三方节点可以获知通信内容
    篡改风险:第三方节点可以修改通信内容
    冒充风险:第三方节点可以冒充他人身份参与通信

    \n
    \n

    大p想到了一个对策,把信放在一个带有密码锁的盒子里,这样就由之前的直接传递小纸条改为了传递有密码锁的盒子。示意图如下:

    \n

    \"\"

    \n

    现在对策有了,带密码锁的盒子有了,信也写好了,但另一个问题来了,密码怎么告诉小h呢,最简单的办法肯定是大p直接把密码告诉小h,但他们两个平时单独约会的时间非常少,每次独处时大p都把心思用在其他地方了,把给密码这件事忘的一干二净。另一个方法是把写有密码的信还通过之前的方法传给小h,但是这样的话中间那个传信的同学就有可能打开信看到密码,把信放在密码盒中就没有任何意义了。

    \n

    因为锁子的密码安全传递问题解决不了,大p暂时否定了这个方案。

    \n
    \n

    以上介绍的是对称加密算法,带有密码锁的盒子和密码分别对应的算法和密钥,常见的对称加密算法有AES、DES、3DES、RC5、RC6

    \n
    \n

    因为担心信中的内容被其他人看到,两个人之间的信件交流就越来越少了,这让大p很苦恼。有一天大p在学校小卖部里看到了一种新的密码锁,这种密码锁神奇之处在于它需要配合一对密码来使用,由一个密码锁上的锁头必须由另一个密码才能解开,反之亦然。

    \n

    大p立刻来了精神,买了把这样的锁回去,并且生成了两对密码。大p和小h协商好,给每对密码中的每个密码分别起个名字:公钥、私钥,公钥表示这个密码能够随意分发,让任何人得到:可以直接把写有公钥的纸条传给对方,甚至把公钥直接写在黑板上都没有问题,但是私钥只能自己知道,甚至连对方都不能告诉。

    \n

    也就是说此时他们两个每人有一个属于自己的私钥。这样只需要每次写完信后,用对方的的公钥把盒子锁上,对方拿到后用自己的私钥解开盒子取出信件,写完回信后再用另一方的公钥锁上盒子即可。

    \n

    假如

    \n
      \n
    • 大p的公钥是abc,私钥是cba;
    • \n
    • 小h的公钥是123,私钥是321,流程示意图如下:
    • \n
    \n

    \"\"

    \n
    \n

    以上介绍的是非对称加密也叫公钥加密,这套密码算法包含配对的密钥对,分为加密密钥和解密密钥。发送者用加密密钥进行加密,接收者用解密密钥进行解密。加密密钥是公开的,任何人都可以获取,因此加密密钥又称为公钥(public key),解密密钥不能公开,只能自己使用,因此它又称为私钥(private key),常见的公钥加密算法有 RSA

    \n
    \n

    于是两个人又开始频繁的给对方写信了,但是在慢慢使用中,他们两个都人发现了一个问题,这个密码锁的加密和解密的效率很低,简直就是写信5分钟,加/解密2小时。

    \n

    后来,大p想到一个方法,他们可以结合两种密码锁,先通过非对称密码锁把之前对称密码锁的密码传给对方,两个人后边直接用对称密码锁来加密解密就可以了。

    \n

    为了后边不再出什么漏洞,大p决定对这种方案进行了严格的推敲,推敲过程中他突然意识到一种可怕的情况,虽然他们两个都持有对方的公钥,但他们自己并不知道自己拿到的是不是真的就是对方的公钥,假如中间传信的人里有一个既邪恶又聪明的同学小x,他可能就会想到一种破解方法:

    \n
      \n
    • 小x手中有两对他自己生成好的密钥:
        \n
      • 第一对:公钥xyz,私钥zyx
      • \n
      • 第二对:公钥456,私钥654
      • \n
      \n
    • \n
    \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

    消息摘要(message digest)函数是一种用于判断数据完整性的算法,也称为散列函数或哈希函数,函数返回的值叫散列值,散列值又称为消息摘要或者指纹(fingerprint)。这种算法是一个不可逆的算法,因此你没法通过消息摘要反向推倒出消息是什么,所以它也称为单向散列函数。常用的散列算法有MD5、SHA。

    \n
    \n

    再之后班长使用自己的私钥把之前计算出来的信息摘要进行了签名(实际就是用私钥对这个值进行了加密),加密后的值我们叫它数字签名,最后把数字签名和原始信息一起打包,生成了最终的数字证书,也就是说数字证书中有两块内容,一块是小h的公钥+小h基本信息组成的明文,另外一块是把明文部分进行散列计算后的值再次通过私钥加密后得到的数字签名。

    \n

    \"\"

    \n

    之后班长把带有自己签名的证书(数字证书)交给了小h。大p找小h索要公钥时,小h只需要把这个数字证书交给他就行了,大p需要用相同的散列算法将明文部分进行计算得到一个散列值a,并且因为大p确定自己手中拿的班长的公钥是可信的,于是大p用班长的公钥对证书中的数字签名进行解密得到得到班长计算出的散列值b,散列值a和散列值b进行比对,如果相同就可以确定明文部分是没有被篡改过的,也就是说此时大p可以相信自己拿到的小h的公钥一定是可靠的了。

    \n

    这也印证了一句名言:“一切计算机问题都可以通过添加中间层解决”。

    \n
    \n

    上边的部分班长就是认证机构(CA),CA 把用户的姓名、组织、邮箱地址等个人信息收集起来,加上公钥,由 CA 提供数字签名生成公钥证书(Public-Key Certificate)PKC

    \n
    \n

    至此,一套比较完善的数据传输方案就完成了。HTTPS(SSL/TLS)就是在这样一套流程基础之上建立起来的。

    \n

    https 简化流程图如下:

    \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

    上边大p用到了4种方案来处理短信这件事:

      \n
    • 同步阻塞(傻傻的等短信过来)
    • \n
    • 同步非阻塞(大p写会作业,检查下手机有没有新短信,这样交替轮询)
    • \n
    • 异步阻塞(手机开启振动模式,但大p 还是盯着看。用这种方案的大p很傻,所以大p只尝试了一次)
    • \n
    • 异步非阻塞(大p只管学习,手机一震大p就知道短信来了)
    • \n
    \n
    \n

    阻塞非阻塞都是相对于大p来说的,取决于大p等待短信时的状态。全心投入等短信达到的大p是阻塞的,可以抽出时间来做其他事情的大p是非阻塞的。

    \n
    \n
    \n

    而同步和异步是对手机来说的,同步需要让大p自己去检查有没有新短信达到,而异步(也就是手机开启振动模式)可以主动告诉大p有短信来了。

    \n
    \n","tags":["同步","异步","阻塞","非阻塞"]},{"title":"Go 结构体方法值传递与引用传递区别","url":"/2021/struct-method-value-vs-pointer/","content":"

    Go 结构体方法中,有一个很重要的点就是值传递和引用传递,我们通过一个例子来看下什么是值传递、什么是引用传递,二者有什么区别。

    \n

    我们声明 person 结构体,里边有一个 name 字段,用两种方式实现 SetName 方法,分别是值传递和引用传递。

    \n
    type person struct {
    name string
    }

    func (p person) SetName1(name string) {
    p.name = name
    }

    func (p *person) SetName2(name string) {
    p.name = name
    }
    \n

    如上,SetName1 就是值传递,SetName2 为引用传递。

    \n

    main 方法中,我们分别调用两个 SetName 方法对 name 进行赋值,并打印每次赋值后的 name 值。

    \n
    func main() {
    p := &person{}

    p.SetName1("张三")
    fmt.Println(p.name)

    p.SetName2("李四")
    fmt.Println(p.name)
    }
    \n

    执行这个程序得到如下结果:

    \n

    李四
    \n

    可以看到 张三 并没有打印出来,而 李四 打印了出来。

    \n

    在调用 SetName1 时,实际上是复制了一个新的 person,方法内操作的也是那个新 person 把复制 personname 改为了张三,而我们在 main 方法中打印的确是原始 personname 字段。因为我们在初始化 person 时并没有指定 name 的值,所以第一次打印出来的是个空串。

    \n

    对程序稍作调整,先调用 SetName2 再调用 SetName1

    \n
    func main() {
    p := &person{}

    p.SetName2("李四")
    fmt.Println(p.name)

    p.SetName1("张三")
    fmt.Println(p.name)
    }
    \n

    这时输出的结果为:

    \n
    李四
    李四
    \n

    第一次调用后,p 结构体指针中 name 的值已经被改为了 李四,接下来我们调用 SetName1 时,因为是复制了一个新的 person,并没有影响之前的 person,所以打印结果还是 李四

    \n

    **我们日常开发中,编写结构体方法时大部分情况都是用引用传递。

    \n

    值传递的问题是,如果我们结构体的成员数量非常多时,每次调用方法都会进行一次拷贝,会有额外的内存开销。

    \n

    验证一下值传递有没有分配新的内存:

    \n
    type person struct{
    name string
    }

    func (p person) SetName1(name string) {
    fmt.Printf("SetName1: %p\\n", &p)
    p.name = name
    }

    func (p *person) SetName2(name string) {
    fmt.Printf("SetName2: %p\\n", p)
    p.name = name
    }

    func main() {
    p := &person{}
    fmt.Printf("Origin: %p\\n", p)

    p.SetName1("张三")
    p.SetName2("李四")
    }
    \n

    运行结果:

    \n
    Origin: 0xc000010200
    SetName1: 0xc000010210
    SetName2: 0xc000010200
    \n

    可以看到,原始的 person 地址为 0xc000010200,在通过值传递时位置发生了改变,变为了0xc000010210,这也就意味着系统为这个新的 person 分配了新的内存地址,而用引用传递的方式地址是不会变的。

    \n

    引用传递容易犯的错误

    我们假设要实现一个发送邮件的功能,定义一个 email 结构体,里边有两个成员 fromto,实现两个方法用来更新这两个成员变量。

    \n
    type email struct {
    from string
    to string
    }

    func (e *email) SetFrom(from string) {
    e.from = from
    }

    func (e *email) SetTo(to string) {
    e.to=to
    }
    \n

    再实现一个发送邮件的方法,这里简单将 fromto 打印出来即可:

    \n
    func (e *email) Send() {
    fmt.Printf("from: %s, to: %s\\n", e.from, e.to)
    }
    \n

    main 方法中,我们写一个循环,实现发送 10 次邮件,0 发送给 1,1 发送个 2,一次以此类推:

    \n
    func main() {
    e: = &email{}

    for i:=0; i<10; i++ {
    e.SetFrom(fmt.Sprintf("%d", i))
    e.SetTo(fmt.Sprintf("%d", i+1))
    e.Send()
    }
    }
    \n

    输出如下:

    \n
    from: 0, to: 1
    from: 1, to: 2
    from: 2, to: 3
    from: 3, to: 4
    from: 4, to: 5
    from: 5, to: 6
    from: 6, to: 7
    from: 7, to: 8
    from: 8, to: 9
    from: 9, to: 10
    \n

    这时候,如果我们改成并发发送这些邮件,同时发给10个人,很容易就会把上边的代码改写如下:

    \n
    func main() {
    e := &email{}

    for i:=0; i<10; i++ {
    go func(i int) {
    e.SetFrom(fmt.Sprintf("%d", i))
    e.SetTo(fmt.Sprintf("%d", i+1))
    e.Send()
    }(i)
    }

    time.Sleep(1 * time.Second)
    }
    \n

    再次运行,结果如下:

    \n
    from: 2, to: 3
    from: 1, to: 2
    from: 3, to: 4
    from: 4, to: 5
    from: 5, to: 6
    from: 6, to: 7
    from: 0, to: 1
    from: 7, to: 8
    from: 9, to: 1
    from: 8, to: 9
    \n

    没有按照递增的顺序发送是在我们意料之中的,但是我们可以看到其中有一行输出为:from: 9, to: 1,这个并不是我们想要的结果。

    \n

    出现这个问题的原因是在每个 go routine 中都是对原始 email 进行的修改,再并发操作的过程中,fromto 有可能被其他的 go routine 改掉,这是个非常严重的 bug。

    \n

    两种修改方法

    每一次都初始化一个新的 email 结构体:

    func main() {
    for i:=0; i<10; i++ {
    e := &email{}
    go func(i int) {
    e.SetFrom(fmt.Sprintf("%d", i))
    e.SetTo(fmt.Sprintf("%d", i+1))
    e.Send()
    }(i)
    }

    time.Sleep(1 * time.Second)
    }
    \n

    改用值传递,并返回修改后的结构体

    package main

    import(
    "fmt"
    "time"
    )

    type email struct {
    from string
    to string
    }

    func (e email) SetFrom(from string) email {
    e.from = from
    return e
    }

    func (e email) SetTo(to string) email {
    e.to = to
    return e
    }

    func (e email) Send() {
    fmt.Printf("from: %s, to: %s\\n", e.from, e.to)
    }

    func main() {
    e := &email{}

    for i:=0; i<10; i++ {
    go func(i int) {
    e.SetFrom(fmt.Sprintf("%d", i)).
    SetTo(fmt.Sprintf("%d", i+1)).
    Send()
    }(i)
    }

    time.Sleep(1 * time.Second)
    }
    \n

    读者可以自己想一想,为什么这种写法可以解决并发赋值出现的问题。

    \n"},{"title":"sublime 实现多处文本替换","url":"/2022/sublime-replace-multi/","content":"

    最近接到一个任务,配合 DBA 进行数据库升级,其实也没有多大难度,就是将依赖这个库的服务配置文件中的 URI、端口号替换下就好了,但我们这个库由于历史原因依赖的服务非常多,有 30 多个,而且每个服务的配置文件可能都有些许差别,还有就是有可能采用了不同的连接池,每个配置文件中即便是同一个字段也可能会声明在多处,修改时要处理多处。

    \n

    前期我使用人肉查找替换的方式来做这件事,但由于要做两轮上线,第一轮是先将配置文件中的从库修改后上线,第二轮再修改主库配置。

    \n

    由于工作太机械化,所以我准备发挥程序员的最大美德:懒惰。

    \n

    先来分析下任务,其实就是把文本中命中的多个字段进行替换,如:

    \n
      \n
    • master.db.com 替换为 new.master.db.com
    • \n
    • username 替换为 new_username
    • \n
    • password 替换为 new_password
    • \n
    \n

    普通的文本编辑器只支持单个字段的替换,上边这种替换多个的情况需要人工手动进行多次操作。

    \n

    我在一开始准备写个 Python 脚本,把每个服务的配置文件复制下来保存成文件,然后用脚本遍历这些文件,将里边的内容替换掉。

    \n

    分析后觉得有些用杀鸡用宰牛刀,不能拿着锤子找钉子,于是就想探索下 sublime 中有没有类似的插件可以实现这个需求。于是就找到了这个 RegReplace 插件。

    \n

    下面我记录下我使用这个插件的过程:

    \n
    \n

    这里避免数据敏感,我用个其他例子做为演示:将一个文档中的 <p></p> 替换为 <h1></h1>

    \n
    \n

    安装

    command+shift+p,输入 install

    \n

    \"20220217054253.png\"
    从列表中搜索 RegReplace 回车安装就可以了。

    \n

    自定义替换配置

    \"20220217054701.png\"

    \n

    编辑上边打开的配置文件,添加以下配置:

    \n
    {
    \"replacements\": {
    \"replace_opening_ps\": {
    \"find\": \"<p>\",
    \"replace\": \"<h1>\",
    \"greedy\": true,
    \"case\": false
    },
    \"replace_closing_ps\": {
    \"find\": \"</p>\",
    \"replace\": \"</h1>\",
    \"greedy\": true,
    \"case\": false
    }
    }
    }
    \n

    估计一眼就能看明白,replace_opening_ps<p> 替换为 <h1>replace_closing_ps</p> 替换为 </h1>

    \n

    自定义触发命令

    \"20220217055403.png\"

    \n

    填入:

    \n
    [
    {
    \"caption\": \"Reg Replace: Replace P to H1\",
    \"command\": \"reg_replace\",
    \"args\": {
    \"replacements\": [
    \"replace_opening_ps\",
    \"replace_closing_ps\"
    ]
    }
    }
    ]
    \n

    也是一看就懂,这里不多解释了。

    \n

    使用

      \n
    1. 准备一段文本:
    2. \n
    \n
    <p>hello,world</p>
    <p>你好,世界</p>
    \n
      \n
    1. command+shift+p 输入 replace ps 定位到我们配置的命令上,回车即可完成多处替换工作:
    2. \n
    \n

    \"20220217061456.png\"

    \n

    替换后的效果如下:

    \n
    <h1>hello,world</h1>
    <h1>你好,世界</h1>
    \n

    现在时间凌晨 5.35 分,先写到这,准备去切换主库了。

    \n"},{"title":"讨论成功与失败","url":"/2023/success-failure/","content":"

    关于成功和失败,有我见过两派说法,一派认为成功的经历很重要,另一派认为失败的经历很重要。

    \n

    认为成功的经历很重要的人们这样说:对那些仅仅满足不失败的人来讲,失败的教训可以让他们避免犯同样的错误;但是对于想成功的人而言,失败的教训远没有成功的经验重要。一个经常失败的人会习惯性失败,相反,成功才是成功之母。 虽然人很难做一件事情就成功一件,但总该尽量避免失败,这样才能少受挫折。

    \n

    认为失败的经历很重要的人们这样说:学会失败,从失败中学习,要想进步就必须学会失败。把失败看成成长的工具,历史上最成功的艺术家、科学家,也是失败最多的,经历过最多的失败。

    \n
    \n

    我们不要有二极管思维,上述两种观点乍一看似乎相互矛盾,但实际上它们各有各的道理。它们从不同的角度阐述了成功和失败的经验。

    \n

    认为成功更重要,是在强调成功的经验比失败的经验更重要,只有不断积小胜才能获大胜。成功会让一个人越来越自信,而失败会让一个人越来越对失败免疫(有点类似于习得性无助)。

    \n

    认为失败更重要,强调的是不要把失败看作是过不去的坎,因为失败无法避免,真正来自失败的痛苦远小于我们的想象。面对失败是磨练心性和个人成长的绝佳机会。

    \n

    一个强调成功的经验很宝贵,一个强调失败的痛苦并不可怕。

    \n

    许多讲述失败重要性的故事都会以爱迪生为例,说他在找到最适合电灯泡的灯丝前失败了5000多次实验。但他们没有说的是,爱迪生在1879年发明出灯泡前已经有过多次成功发明经历,例如1868年的投票计数器、1870年的印刷机、1877年的留声机等等。正是因为有这么多次成功经验才给了爱迪生很大的信心。我相信,爱迪生在进行电灯泡实验时,更多的经验一定来自之前成功发明的经历。

    \n

    成本和机会这两个因素也极其重要,穷人家的孩子失败一次就再没有重试的机会了,而富人家的孩子失败了还可以重头再来,他们有可以试错的资本。

    \n

    在初始成功和失败率相同的情况下,富人家的孩子有更多的重试机会。随着重试次数增多,成功次数也随之增加,成功经验也会积累得更多,这样就越不容易失败。很多企业家的儿子就是很好的例子,比如王思聪。

    \n

    作为普通人,我们既无法避免失败,也没有太多试错成本,我们能做些什么?

    \n
      \n
    • 积极的重建,把失败看成成长的工具,这可以更好的了解自己
    • \n
    • 要认命,及时止损。不要总想着挽回损失,这样损失就会被限制在局部
    • \n
    • 对于已经无法挽回的错误,不是懊恨不已,而是承认往日错误已是人力无法企及的范畴,既不能从头来过,也不能改变结果
    • \n
    • 行动前多推演,行动后多复盘
    • \n
    • 同时具备两种互相冲突的信念
        \n
      • 一方面,要像初生牛犊一样,对自己的能力信心万丈
      • \n
      • 另一方面,你又要像历经沧桑的老人一样,对自己的能力抱着怀疑态度
      • \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

    让 Mac 合盖后不休眠

    我一直使用 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
      \n
    1. 判断网络状况:定期 ping 一个地址,这个地址我跑在 AWS 的 Lambda 上,服务收到请求后记录最新请求时间,服务自身也会有定时任务检查上一次请求时间和当前时间的差值,如果超过一个阈值,则通过 Bark 给我发个推送。
    2. \n
    3. 监控设备电量:当低于某个阈值时通过 Bark 提醒我。
    4. \n
    \n

    可以看出这个监控需要两组脚本,一个是部署在 Lambda 上的,用来判断设备网络情况,另一个是客户端本地用来 ping 服务和监控电量。

    \n

    先来看启动在 Lambda 上的 handler:

    def mac_health(event, context):  
    update = False
    if event.get('queryStringParameters') and event.get('queryStringParameters').get('update'):
    update = True

    t = time.time()
    ts = int(t)

    if update: # 我的Mac发送的携带 ?update=1 的请求,只更新最新时间
    with open('/tmp/now.txt', 'w') as f:
    print(\"====\")
    f.write(str(ts))
    print(ts)
    else: # 有 cronjob 触发的请求,只判断健康状态
    try:
    f = open('/tmp/now.txt', 'r')
    tmp_ts = f.read()
    if tmp_ts:
    tmp_ts = int(tmp_ts)
    if ts - tmp_ts > 3 * 60:
    print(\"no heart beat\")
    requests.get('https://api.day.app/YOUR_KEY/' + '请注意:你的Mac离线了' + '/' + '请检查网络状态')
    else:
    print(\"status health\")
    print(tmp_ts)
    f.close()
    except Exception:
    print(\"read error\")
    with open('/tmp/now.txt', 'w') as f:
    print(\"====\")
    f.write(str(ts))
    print(ts)
    return {
    \"statusCode\": 200,
    \"body\": json.dumps(
    {
    \"time\": ts
    }
    )
    }
    \n

    serverless.yml:

    \n
    functions:
    mac_health:
    handler: handler.mac_health
    events:
    - schedule: cron(*/2 * * * ? *)
    - http:
    path: mac_health
    method: get
    \n

    这里我不再介绍 Lambda 如何使用,可以参考我之前的其他文章:

    \n\n

    上边代码我做个补充说明:

    \n
      \n
    • Lambda 是完全无状态的服务,而且随时有可能被 kill 然后启动在其它实例上,所以我们最好不要将最后 ping 时间记录在内存中,如果也不想借助外部存储的话,我们可以借助 /tmp 目录来临时存储这个数据,AWS 为每个 Lambda 提供 500MB /tmp 下的存储空间。
    • \n
    • 我在用 Mac 访问 Lambda endpoint 的时候会带上 ?update=1 这个参数,这样就可以把最新时间记录下来,而系统通过 cronjob 调用自己的时候不记录时间,只进行时间差判断。
    • \n
    \n

    再来看下Mac断的脚本:

    import os
    import time
    import requests

    if __name__ == '__main__':
    while True:
    try:
    res = requests.get(\"https://xxxxxxxx.execute-api.ap-east-1.amazonaws.com/dev/mac_health?update=1\")
    except Exception:
    print(\"requests error\")
    time.sleep(10)
    continue
    print(res.status_code)
    battery = os.popen('pmset -g batt | grep -Eo \"\\d+%\" | cut -d% -f1').read()
    battery = int(battery)
    print(battery)
    if battery < 50:
    requests.get('https://api.day.app/YOU_KEY/' + '请注意:你的Mac电量过低' + '/' + '请检查电源状态,剩余电量: %d%%' % (battery,))
    else:
    print(\"ok\")
    time.sleep(60)
    \n

    逻辑也比较简单,这里是通过一个系统 shell 调用来获取的电量。

    \n

    将电脑电源拔掉,把报警阈值调到 97 的效果:

    \n

    拔掉网线后的效果:

    \n

    将最大充电量设置为 70%

    电脑像这样长期插着电源并保持开机状态对电池的损耗非常大,为了避免对电池损耗过快,建议将最大充电量设置为 60%-80% 之间,我们可以借助 bclm 这个小工具实现,工具名是 Battery Charge Level Max 的缩写。安装方式参考官方文档,我自己是将二进制包下下来放到 bin(/usr/local/bin) 目录来直接使用的。

    \n

    bclm 提供命令非常简单:

    \n
    # 获取当前电池的最大充电量
    bclm read

    # 设置最大充电量,一定要在最前边加 sudo
    sudo bclm write 70

    # 持久化最大充电量设置,避免覆盖和重启失效
    sudo bclm persist
    \n

    \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
      \n
    • 所谓阻塞 I/O,是指应用程序在执行 I/O 操作后,如果没有获得响应,就会阻塞当前线程,不能执行其他任务。
    • \n
    • 所谓非阻塞 I/O,是指应用程序在执行 I/O 操作后,不会阻塞当前的线程,可以继续执行其他的任务。
    • \n
    \n

    从系统角度

    根据 I/O 响应的通知方式的不同,可以把文件 I/O 分为同步 I/O 和异步 I/O。

    \n
      \n
    • 所谓同步 I/O,是指收到 I/O 请求后,系统不会立刻响应应用程序;等到处理完成,系统才会通过系统调用的方式,告诉应用程序 I/O 结果。
    • \n
    • 所谓异步 I/O,是指收到 I/O 请求后,系统会先告诉应用程序 I/O 请求已经收到,随后再去异步处理;等处理完成后,系统再通过事件通知的方式,告诉应用程序结果。
    • \n
    \n

    总结

    阻塞 / 非阻塞和同步 / 异步,其实就是两个不同角度的 I/O 划分方式。它们描述的对象也不同:

    \n
      \n
    • 阻塞 / 非阻塞针对的是 I/O 调用者(即应用程序)
    • \n
    • 同步 / 异步针对的是 I/O 执行者(即系统)
    • \n
    \n

    举例

    比如在 Linux I/O 调用中:

    \n
      \n
    • 系统调用 read 是同步读,所以,在没有得到磁盘数据前,read 不会响应应用程序。
    • \n
    • aio_read 是异步读,系统收到 AIO 读请求后不等处理就返回了,而具体的 read 结果,再通过回调异步通知应用程序。
    • \n
    \n

    再如,在网络套接字的接口中:

    \n
      \n
    • 使用 send() 直接向套接字发送数据时,如果套接字没有设置 O_NONBLOCK 标识,那么 send() 操作就会一直阻塞,当前线程也没法去做其他事情。
    • \n
    • 当然,如果你用了 epoll,系统会告诉你这个套接字的状态,那就可以用非阻塞的方式使用。当这个套接字不可写的时候,你可以去做其他事情,比如读写其他套接字。
    • \n
    \n"},{"title":"日拱一卒 - 缓存","url":"/2022/system-design-cache/","content":"
    \n

    “计算机科学只存在两个难题:缓存失效和命名。” ——Phil KarIton

    \n
    \n

    \"20220830134522.png\"

    \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

    写入式缓存(Write-through)

    \"20220830134809.png\"
    数据同时被写入缓存和相应的数据库中。

    \n

    优点:快速检索,缓存和存储之间的数据完全一致。

    \n

    缺点:写操作的延迟较高。

    \n

    绕写式缓存(Write-around or Write-aside)

    \"20220830141332.png\"

    \n

    直接写到数据库或永久存储,绕过缓存。

    \n

    优点:可以减少写操作延迟。

    \n

    缺点:增加了缓存失效。在缓存失效的情况下,缓存系统必须从数据库中读取信息。因此,在应用程序快速写入和重新读取信息的情况下,这可能导致更高的读取延迟。读取发生在较慢的后端存储中并经历较高的延迟。

    \n

    回写缓存(Write-back or Write-behind)

    \"20220830141357.png\"

    \n

    只对缓存层进行写入,一旦写入缓存完成,就会确认写入。之后,缓存异步地将这个写入同步到数据库中。

    \n

    有点:降低写密集应用的延迟并提高吞吐量。

    \n

    缺点:在缓存层崩溃的情况下,存在数据丢失的风险。我们可以通过让一个以上的副本确认缓存写入成功改善这个问题。

    \n

    淘汰策略

    以下是一些最常见的缓存淘汰策略:

    \n
      \n
    • 先入先出(FIFO),缓存优先淘汰最早访问的项,而不考虑它之前被访问的频率或次数。
    • \n
    • 后进先出(LIFO),缓存优先淘汰最近访问的项,而不考虑它之前被访问的频率或次数。
    • \n
    • 最近最少使用(LRU),优先淘汰最近使用最少的项。
    • \n
    • 最近使用(MRU),与 LRU 相反,首先淘汰最近使用的项。
    • \n
    • 最不经常使用(LFU),计算一个项的使用频率,优先淘汰那些使用频率最低的项。
    • \n
    • 随机淘汰(RR),随机选择一个候选项,必要时淘汰它以腾出空间。
    • \n
    \n

    分布式缓存

    \"20220830135848.png\"

    \n

    分布式缓存是一种系统,它将多台联网计算机的随机存取存储器(RAM)集中到一个单一内存数据存储中,用作数据缓存,以提供对数据的快速访问。

    \n

    虽然传统中的大部分缓存都在一个物理服务器或硬件组件中,但通过将多台计算机连接在一起,分布式缓存可以超越单个计算机的内存限制。

    \n

    全局缓存

    \"20220830135724.png\"

    \n

    顾名思义,我们将有一个单一的共享缓存,所有的应用节点都访问这个缓存。当请求的数据在全局缓存中找不到时,缓存负责从底层数据存储中查找缺失的数据。

    \n

    使用案例

    在现实世界中缓存有许多使用场景,如:

    \n
      \n
    • 数据库缓存
    • \n
    • 内容分发网络(CDN)
    • \n
    • 域名系统(DNS)缓存
    • \n
    • API 缓存
    • \n
    \n

    什么时候不使用缓存?

    我们也来看看在哪些情况下不应该使用缓存:

    \n
      \n
    • 当访问缓存的时间和访问主数据存储的时间一样长时,缓存就没有用了。
    • \n
    • 当请求的重复性较低(随机性较高)时,缓存的作用就不大了,因为缓存的高性能来自于重复的内存访问。
    • \n
    • 当数据经常变化时,缓存没有帮助,因为当缓存版本不同步时就需要访问主数据存储。
    • \n
    \n

    另外需要注意的是,缓存不应该被用作永久的数据存储。大部分缓存都是在易失性内存中实现的,因为它的速度更快,因此缓存应该被认为是临时性的。

    \n

    优点

    以下是缓存的几个优点:

    \n
      \n
    • 提高性能
    • \n
    • 减少延时
    • \n
    • 减少数据库的负载
    • \n
    • 降低网络成本
    • \n
    • 增加读吞吐量
    • \n
    \n

    实例

    下面是一些常用的缓存技术:

    \n\n"},{"title":"日拱一卒 - 内容分发网络(CDN)","url":"/2022/system-design-cdn/","content":"

    内容分发网络(CDN)是一组在地理上广泛分布的服务器,它们一起工作以提供互联网内容的快速交付。通常静态文件,如 HTML/CSS/JS、照片和视频,都是由 CDN 提供的。

    \n

    \"20220902121333.png\"

    \n

    为什么使用 CDN?

    内容分发网络(CDN)增加了内容的可用性和冗余度,同时降低了带宽成本并提高了安全性。通过 CDN 获取内容可以显著提高性能,因为用户从靠近他们的数据中心接收内容,我们的服务器也不必为 CDN 满足的请求提供服务。

    \n

    CDN 是如何工作的?

    \"20220902121342.png\"

    \n

    在 CDN 中,源服务器包含内容的原始版本,边缘服务器分布在世界各地并且数量众多。

    \n

    为了最大限度地减少访问者与服务器之间的距离,CDN 将其内容的缓存版本存储在多个地理位置,称为边缘位置。每个边缘位置包含一些缓存服务器,负责向其附近的访问者提供内容。

    \n

    一旦静态资源被缓存在特定位置的所有 CDN 服务器上,之后所有网站访问者对静态资源的请求都将由这些边缘服务器提供(而不是源站),从而减少源站负载并提高可扩展性。

    \n

    假如一名位于英国的用户请求本站,本站服务器当前托管在美国,他们将从最近的边缘位置(如伦敦)获得服务。这比让访问者向源站服务器发出完整的请求要快得多,后者会增加延迟。

    \n

    类型

    CDN 通常分为两种类型。

    \n

    推模式(Push)

    当服务器上内容发生更改时,使用了推模式的 CDN 会收到新内容。我们完全负责提供内容,直接上传至 CDN,并重写 URL 以指向 CDN。我们可以配置内容何时过期、何时更新。内容只有在新的或改变的时候才会被上传,最大程度地减少流量,最大限度地提高存储。

    \n

    流量小的网站或内容不经常更新的网站使用推模式效果很好。内容被放置在 CDN 上一次,而不是定期被重新拉取。

    \n

    拉模式(Pull)

    在拉模式的情况下,缓存是根据请求更新的。当客户端发送一个要求从 CDN 获取静态资源的请求时,如果 CDN 中没有,那么它将从源站服务器获取新的资源,并用这个新资源填充其缓存,然后将这个新的缓存资源发送给用户。

    \n

    与推模式 CDN 相反,拉模式需要较少的维护,因为 CDN 节点上的缓存更新是基于客户端对源站服务器的请求进行的。流量大的网站使用拉模式效果很好,因为流量分散地更均匀,只有最近请求的内容留在 CDN 上。

    \n

    缺点

      \n
    • 额外费用:使用 CDN 可能是昂贵的,特别是对于高流量的服务。
    • \n
    • 限制:一些组织和国家已经封锁了流行的 CDN 的域名或 IP 地址。
    • \n
    • 地点:如果我们的大部分受众位于没有部署 CDN 服务器的国家,他们访问我们网站上的数据,可能比不使用任何 CDN 的情况下延迟更高。
    • \n
    \n

    例子

    以下是一些广泛使用的 CDN:

    \n\n"},{"title":"关于 TCP 的 14 个问题","url":"/2021/tcp-questions/","content":"

    每个TCP数据包都有一个源/目的IP地址吗?

    是的

    \n

    下边是一个TCP数据包的结构

    \n
    +-+-+-+-+-+-+-+-+--
    | Ethernet header |
    +-+-+-+-+-+-+-+-+--
    | IP header |
    +-+-+-+-+-+-+-+-+--
    | TCP header |
    +-+-+-+-+-+-+-+-+-+
    | packet contents |
    +-+-+-+-+-+-+-+-+-+
    \n

    这就是为什么我们称它为 「TCP/IP」– TCP数据包总是有一个 IP 头。

    \n

    每个TCP数据包都有一个端口号吗?

    是的

    \n

    TCP 头中的端口字段为16位。

    \n

    TCP连接中发送的第一个数据包是什么?

    SYN

    \n

    每个TCP连接都是以三次向握手开始的:

    \n
      \n
    • client: SYN
    • \n
    • server: SYN + ACK
    • \n
    • client: ACK
    • \n
    \n

    如果防火墙拦截了连接,TCP握手能否完成?

    不能

    \n

    被防火墙拦截的常见症状是 SYN 数据包发出后无 ACK 返回。

    \n

    电子邮件使用TCP吗?

    是的

    \n

    电子邮件是通过SMTP发送的,它使用了 TCP。

    \n

    FTP、POP3、HTTP/1、HTTP/2、websockets 等很逗互联网协议也都使用了 TCP。

    \n

    TCP连接是双向的吗?(客户端和服务器都能发送消息吗?)

    是的

    \n

    一个HTTP 请求/响应 是个 TCP 连接 —— 客户端发送 HTTP 请求,然后服务器发送响应。

    \n

    如果服务器发送响应给客户端,客户端能否发送更多数据?

    是的

    \n

    例如,websockets 使用 TCP 使客户端和服务器根据需要来回发送数据。

    \n

    如果你通过TCP发送一个长消息,是否会被分成多个数据包?

    是的

    \n

    数据包有最大限制(通常是1500字节),所以需要将一个 TCP 消息分割成许多数据包。

    \n

    当你发送 TCP 数据包时,数据包是否可以按照你发送的顺序到达目的地?

    不是

    \n

    无法确保网络数据包会按照你发送的顺序到达。

    \n

    负责理解TCP协议的代码在哪?

    通常是在操作系统中

    \n

    Linux、Mac、Windows等都有 TCP 实现。如果你愿意,也可以编写自己的TCP实现。

    \n

    你的操作系统使用 TCP 数据包中的哪个字段将数据包按正确顺序排列?

    序列号(sequence number)

    \n

    数据包的序列号告诉你它在整个数据流中的位置。序列号计算的是字节数,而不是包数。

    \n

    下面是一组数据包内容和序列号的例子(每个字符是1个字节):

    \n
    +-+-+-+-+-+-+-+-+-+-+-
    | contents | seq # |
    +-+-+-+-+-+-+-+-+-+-+-
    | hello I | 0 |
    +-+-+-+-+-+-+-+-+-+-+-
    | x! | 15 |
    +-+-+-+-+-+-+-+-+-+-+-
    | 'm panma | 7 |
    +-+-+-+-+-+-+-+-+-+-+-
    \n

    如果按照正确的顺序排列,这些数据包将重新排列为“hello I’m panmax!”

    \n

    顺序号总是从0开始吗?

    不是

    \n

    通常连接中的第一个序列号是一个很大的数字,比如:1737132723,其他序列号都是与这个数字相对的。所以在计算时需要减去第一个序列号。

    \n

    如果你在 Wireshark/tcpdump 中查看TCP数据包,它们会进行减法处理,使其看起来像序列号从0开始,让人类更易阅读。

    \n

    如果你发送一个 TCP 数据包,如何知道它被接收?

    你会收到一个ACK

    \n

    例如,服务器收到了一个序列号为1200的数据包,并且也已收到所有在它之前的数据包,服务器会发送一个序列号为1200的ACK数据包。

    \n

    被丢弃的TCP数据包是否会被重试发送?

    是的

    \n

    如果客户端(或服务器)在一定时间内没有收到其发送数据包的ACK,它将会重试发送该数据包。

    \n"},{"title":"不到 40 行代码实现 Telegram 自动发消息机器人","url":"/2019/telegram-bot-send-taily-message/","content":"

    \"\"

    \n

    创建一个 Telegram 机器人,定时发送消息,并部署到 AWS Lambda。

    \n
    \n

    AWS Lambda 是一项计算服务,可使你无需预配置或管理服务器即可运行代码。

    \n
    \n

    我们要做什么?

      \n
    • 创建一个 Telegram 机器人
    • \n
    • 自动发送日常信息
    • \n
    • 把它部署到 AWS Lambda
    • \n
    \n

    需要准备什么?

      \n
    • 一个 Telegram 帐号
    • \n
    • Python 3.6
    • \n
    • Node.js
    • \n
    • 一个 AWS 帐号
    • \n
    \n

    AWS Lambda 可以在一定配额内免费使用,所以需要避免发送大量请求。

    \n

    AWS Lambda 定价方案如下:

    \n

    \"\"

    \n

    创建机器人

    待办清单上的第一件事是创建一个机器人,遵循 Telegram 官方说明:

    \n
      \n
    • 在 Teletram 中搜索用户 @BotFather
    • \n
    • 发送命令 /newbot 并为你的机器人指定 nameusername
    • \n
    • 拿到 token 并记录在一个安全的地方,后边会用到。
    • \n
    \n

    现在机器人准备好了,开始编写代码。

    \n

    准备部署设施

    有很多部署 Lambda 的方法,我准备使用 serverless 框架,所以我们先来安装它:

    \n
    $ npm install serverless --global
    \n

    Serverless 的文档中提供了一些范例,还支持生成模板,像下边这样:

    \n
    $ serverless create --template aws-python3 --path scheduled_telegram_bot
    \n

    执行这个命令后,会创建出一个 scheduled_telegram_bot 目录,并已经生成了 3 个文件:
    .gitignoreserverless.ymlhandler.py

    \n

    serverless.yml 文件用来描述:部署什么、何时运行、如何运行。 handler.py 文件包含将要运行的代码,所以我们先来编写它。

    \n

    编写代码

    我们将使用是一个封装好的包来调用 Telegram 的 API:python-teletram-bot,创建一个新的文件 requirements.txt 写入:

    \n
    python-telegram-bot==12.2.0
    \n

    我们需要在程序中导入这个库,不过后边我们会遇到一个问题:由于 python-telegram-bot 不是 AWS Lambda 所提供的标准库,因此我们在部署时需要同时包含这个包中的文件。

    \n

    所以我们后边会在本地安装这个包的所有内容。

    \n
    pip install requirements.txt --target=.
    \n

    现在让我们来定义一个发送消息的函数,打开 handler.py 修改内容如下:

    \n
    import telegram
    import os

    TOKEN = os.environ['TELEGRAM_TOKEN']
    CHAT_ID = 000000 # Change this


    def send_message(event, context):
    bot = telegram.Bot(token=TOKEN)
    bot.sendMessage(chat_id=CHAT_ID, text='Hey there!')
    \n

    你需要把 CHAT_ID 修改为你想让机器人互动的群组、频道或者会话的 ID。获取 ID 的方法如下,我以频道为例:

    \n

    首先创建自己的频道,将机器人拉入频道并设置为管理员,随意在频道内发送一个消息。

    \n

    添加 GetIDsBot,并将上边发的那条消息转发给这个机器人,它会返回这个频道相关信息:

    \n
    👤 You
    ├ id: xxxxxxx
    ├ is_bot: false
    ├ first_name: xxxxx
    ├ username: xxxxxx
    ├ language_code: zh-hans (-)
    └ created: ~ 9/2017 (?)

    💬 Origin chat
    ├ id: -1001156324531
    ├ title: Panmax Channel
    └ type: channel

    📃 Message
    ├ message_id: 82
    └ forward_date: Fri, 15 Nov 2019 15:38:41 GMT
    \n

    这样可以得到我所在这个频道的 ID 为 -1001156324531

    \n

    部署定义

    现在我们来定义如何运行我们的代码。

    \n

    编辑 serverless.yml

    \n
    service: scheduled-telegarm-bot

    frameworkVersion: ">=1.2.0 <2.0.0"

    provider:
    name: aws
    runtime: python3.6
    environment:
    TELEGRAM_TOKEN: ${env:TELEGRAM_TOKEN}

    functions:
    cron:
    handler: handler.send_message
    events:
    - schedule: cron(*/2 * * * ? *)
    \n

    这里我们告诉了 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]
    $ export AWS_SECRET_ACCESS_KEY=[your key goes here]
    $ export TELEGRAM_TOKEN=[your token goes here]
    \n

    在本地安装 Python 的依赖包(这也是 AWS Lambda 所需要的):

    \n
    $ pip3 install -r requirements.txt -t .
    \n

    最后将所有的东西部署到 AWS:

    \n
    $ serverless deploy
    \n

    如果前边配置没有问题,会看到如下输出:

    \n
    Serverless: Packaging service...
    Serverless: Excluding development dependencies...
    Serverless: Uploading CloudFormation file to S3...
    Serverless: Uploading artifacts...
    Serverless: Uploading service scheduled-telegarm-bot.zip file to S3 (5.64 MB)...
    Serverless: Validating template...
    Serverless: Updating Stack...
    Serverless: Checking Stack update progress...
    .........
    Serverless: Stack update finished...
    \n

    完成!机器人会在每 2 分钟给我们发送一次消息。

    \n

    \"\"

    \n

    参考

    AWS 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
    1. 从最容易的事情开始
    2. \n
    3. 每天必须做一件事情
    4. \n
    5. 每天必须不做一件事情
    6. \n
    7. 不要积累太多的未完成的事情
    8. \n
    9. 有决定胜过没决定
    10. \n
    \n

    人生最大的痛苦莫过于知道该怎么做却没有去做,你会自责,你会对自己不满意,你会觉得自己是渺小的、不讲信誉不可信的。总而言之,就是你开始不信任自己,自信心降低了。

    \n

    痛苦时,不要只沉浸在痛苦中,或者以寻找刺激的方式来降低或麻木自己的痛苦,而要思考一下“我为什么这么痛苦,我重复了童年的什么体验?”

    \n

    自己的惧怕与愤怒是建立在有限的人生体验上的,是不合理的。

    \n

    痛苦背后的问题恰恰是我们的一部分,须臾不可分离,根本逃避不了。所谓的逃避,只不过是运用种种自欺的方式扭曲了我们对问题的认识,从而减少我们的痛苦。我们以为看不到它们了,但其实它们还是我们甩不掉的尾巴。

    \n

    潜意识的特点是,我们越想控制它,就越控制不了,它的活动会越来越频繁。

    \n

    不要总是和潜意识过不去,不必和走神、坏念头等偶尔出现的问题较真。否则,它们就会成为真正的问题。

    \n

    按照存在主义哲学,只要你渴望触及人类、社会乃至世界的真相,那么你会一直焦虑下去。因为,不管成长到哪一层次,你一定会发现新的局限性,这时焦虑就势必会发生。所以,许多哲人越深入这个世界,就越明白自己无知。从这一点而言,焦虑是推动我们认识世界的动力

    \n

    一个人在原生家庭中的关系决定了这个人的心理健康程度。

    \n

    人生的悲剧本身并不一定会导致心理问题,它之所以最后令我们陷入困境,是因为我们想否认自己人生的悲剧性。

    \n

    我们的力量不在于我们看上去有多快乐,而在于我们的心离我们的人生真相有多近。

    \n

    作为一个人,我们必须深入地探讨自己经历过的所有事件以及教训,只有在这个深度上我们才能发现我们自己的真实,找到自己的决策能力。

    \n

    一个总是不断诞生强人的社会,必然是一个失序与窒息不断轮回的社会。

    \n

    对世界而言,控制欲望是万恶之源。对个人而言,控制欲望是万病之源。

    \n

    强人们其实首先想控制自己内心的失序,但他们做不到,于是他们去追求控制别人。他们内心越失序,就越渴望控制更多的人。最终,不管他们意识上的目的是什么,制造的或留下的多是苦难。

    \n
    \n

    想到了希特勒

    \n
    \n

    不管一份体验带给我多大的痛苦,只要不作任何抵抗地沉到这份痛苦中,体会它、看着它,那么它最多半个小时后就会融解并转化。

    \n

    看心理医生,随着安全感和信任感的增加,患者一些更深层的痛苦反而会映现出来,于是会体会到平时生活中都体会不到的痛苦。

    \n

    任何一次袭来的痛苦,不管多么难过,只要你沉入其中体会它觉察它,那么最多半个小时就会融解并转化,有时会以喜悦结束,有时会以平静结束。

    \n

    当你非要压制自己的悲伤,并相反表现出极大的快乐时,你最终收获的,会是更大的悲伤。

    \n

    我们都在寻求价值感,如果童年时,某一种方式令我们找到了价值感,此后我们便会执着在这个方式上。并且,这世界上的大多数人一般只找到了一套寻求价值感的方式,越困难的时候,我们会越执着于这一套方式,认为这是唯一的,但其实在最困难的时候,改变或调整这一方式会更好。

    \n

    你想让一个人对你好,就请他帮你一个忙。这个办法之所以更好,是因为我们都很自恋。多数时候,我们看似爱的是别人,其实爱的是自己在这个人身上的付出。

    \n

    每一次挫折事件都是一次机遇,因为它暴露了自己的缺点和弱点。进行自我归因的人会借此完善自己。这样一来,挫折就成了人生的一种财富。

    \n

    好的愤怒,针对的必须是导致你愤怒的那个人。你对这个人愤怒,你才能捍卫自己的空间,并且愤怒的表达才会有效果。如果这个人惹了你,你不敢对他愤怒,你跑去把愤怒发泄到其他人身上。那么,你发泄得再厉害都没用,因为对象选错了,那样愤怒就没有任何意义。

    \n

    治疗痛苦的唯一办法就是直面并接受人生悲剧

    \n

    抑郁症常源自两个原因:一是重大的丧失;二是压抑的愤怒

    \n

    失去发生时的第一时间所产生的悲伤与泪水,是有治疗效果的,只要悲伤能在我们身体上自然流动,这份疗愈就会自然产生。

    \n

    爱的关系中,付出和接受的循环被破坏,很多时候不是因为不愿意给予,而是因为不愿意接受。

    \n

    假如你没有一点儿负罪感,而只有清白感,那其实就是你把负罪感强加给其他人了,而那个被强加者一般都是你最亲密的人。

    \n

    从情感上看,单纯的“付出者”其实并不伟大,他们不计得失的付出,从根本上是一种自恋。

    \n

    “付出者”其实在享受这种逻辑:既然我是付出的一方,那么我们的关系无论出现什么问题都是你的错了。

    \n

    最好的关系是彼此慷慨地付出和坦然地接受,通过这种交换,双方的接受和付出达成了一种平衡,且彼此都感到自己在这个关系中富有价值。

    \n

    好人,势必有一个特点——牺牲自己的需要。坏人,势必有一个特点——纵容自己的需要。

    \n

    关系有两种,一种是我与你,一种是我与它。

    \n
      \n
    • 当我将你视为满足我的需要的工具与对象时,这一关系就是我与它。
    • \n
    • 当我没有任何期待与目的,而是带着我的全部存在与你的全部存在相遇时,这一刻的关系就是我与你。
    • \n
    \n

    我们在与别人交往时,多数时候不过是在重复小时候我们与父母等亲人打交道的方式而已。

    \n

    童年得到的爱越多,一个人就越是难追。这样的人会相信自己的感觉,凭感觉去找到适合自己的人。如果他觉得你是他想要的,那他可能很快接纳你;如果不是,那么可能无论你怎么努力,都是没有用的。
    相对而言,童年得到的爱越少,一个人就越容易追。只要你对他很好,他就很容易感动,而暂时接纳你。但是,他是一开始容易追到,而以后会很难相处,因为他会过于敏感。

    \n

    作为人类一种最基本的情绪,恐惧和其他情绪一样,也有着它的独特价值,而一味地追求战胜恐惧,就忽略了恐惧所传递的重要信息。

    \n

    我们越恐惧一件事情,那件事情背后隐藏着的信息可能就越重要。

    \n

    恐慌的背后,常藏着我们生命中重要的答案;恐慌程度越高,答案就越重要。

    \n

    关系匮乏所带来的恐惧,在相当的程度上可以说是源自对死亡的恐惧

    \n

    我们的人格也源自我们与父母的关系,父母和我们的原生关系,最终被我们内化为“内在的父母”和“内在的小孩”。

    \n

    当我们想与死去的亲人同甘共苦的时候,我们忽视了很重要的一点:死去的亲人不希望我们这样做。

    \n

    我们很容易只沉浸在自己的痛苦自己的幻想中,自以为死去的亲人希望我们怎么样,却忘记了他们对我们真切的叮嘱。如果真是这样,那才是对爱的误解。

    \n

    追求优秀不是克服自卑的良药,特别自控也不是情绪化的答案。

    \n

    替别人承担问题,这会令自己获得一种价值感。但若心理医生在咨询室中追求这种价值感,他便在一定程度上阻碍了病人的自我发展。

    \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

    人们越是依赖立竿见影的解决办法,越是加剧了问题潜在的隐患。

    \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

    下边我介绍一种(自我认为)比较科学的方式:

    \n

    以我们已有的 demo 项目 app-c 为例,在 build.gradle 中加入:

    \n
    springBoot {
    executable = true
    }
    \n

    这样可以编译出来可执行的 jar 包,看下前后对比,下边两张图分别是加之前和加之后 app-c ,可以看到多了 x 权限。

    \n

    不带 x 权限的 app-c
    \"\"

    \n

    x 权限的 app-c
    \"\"

    \n

    我们来直接用 output/app-c-0.0.1-SNAPSHOT.jar 运行一下试试:

    \n

    \"\"
    没有任何问问题。

    \n

    接下来我们把这个可执行程序通过软链接的方式注册为系统服务:

    sudo ln -s /opt/demo-projects/output/app-c-0.0.1-SNAPSHOT.jar /etc/init.d/app-c

    \n

    这样就可以使用 startstoprestart 来管理我们的应用了

    \n

    /etc/init.d/app-c start|stop|restart



    service app-c start|stop|restart
    \n

    并且 status 可以查看运行状态:

    \n

    \"\"
    用这样的方式来管理线上应用,比之前的方式方便了很多:不需要再进到存放 jar 包的目录来用冗长的代码启动,只需知道应用名称,就可以直接启动、重启、停止我们想管理的应用。

    \n"},{"title":"第三者的重要性","url":"/2023/the-importance-of-third-parties/","content":"

    看到第三者这个词是不是想歪了?我这里指的是一个事件中的第三方参与者。

    \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

    本文使用了 emoji 对部分内容进行了加密,可以看我这篇文章了解详情。

    \n
    \n

    马老师说过,离职无非两个原因:1、钱,没给到位;2、心,委屈了。

    \n

    最近读到 MacTalk 的一篇文章,这篇文章将离职原因分成了三点,前两点是把马老师的第二点做了个个拆分:

    \n
      \n
    • 第一,你确实在这公司没成长了,你每天在空转,你在消耗自己,你本是一把利剑,现在快被磨成废铜烂铁了。
    • \n
    • 第二,你讨厌所在团队的人文环境,大家做一些看起来无厘头、很可笑的事情,这已经影响了你的身心健康。
    • \n
    • 第三,工资和实力不匹配,沟通无果,无法共鸣。
    • \n
    \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
    \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

    为什么 ++i 通常比 i++ 更好?

    \n
    \n

    \"\"

    \n

    介绍

    如果你之前写过 for 循环,那么你一定使用过 i++ 来增加你的循环变量。

    \n

    然而,你是否考虑过为什么要选择这种做法呢?

    \n

    我们在执行完 i++ 后,i 的值会比它先前大 1,这是我们想要的结果。与此同时还有很多方法可以做到,比如:++i 甚至 i = i + 1

    \n

    接下来,我会对比介绍两种实现变量加 1 的方法:++ii++,并解释为什么大多数情况下 ++i 可能好于 i++

    \n

    后递增(i++)

    i++ 方法(或者叫后递增)是最常见的使用方式。

    \n

    在伪代码中,后递增操作符对变量 i 的操作大致如下:

    \n
    int j = i;
    i = i + 1;
    return j;
    \n

    由于后递增需要返回 i 的原值而不是返回 i + 1 后的增量值,所以需要将 i 的旧值进行存储。

    \n

    这意味着 i++ 需要额外的内存来存储这个值,但这是不必要的。因为在大多数情况下,我们并不会使用 i 的旧值,而是直接将其丢弃。

    \n

    前递增(++i)

    ++i 方法(或者叫前递增)比较少见,通常是使用 CC++ 的老程序员在用。

    \n

    在伪代码中,前递增操作符对变量 i 的操作大致如下:

    \n
    i = i + 1;
    return i;
    \n

    需要注意的是,在前递增中,我们不必保存 i 的旧值,我们只需简单的对它加 1 并返回。这与 for 循环中的经典用例更加匹配:正如上文所说,我们很少需要 i 的旧值。

    \n

    说明

    看过后递增和前递增之间的区别后,你可能会想到:由于 i 的旧值在后递增中未被使用,因此在编译阶段,编译器将会优化掉这一行,使两个操作符等价。

    \n

    对于基本类型来说(如整形)确实如此。

    \n

    但是对于复杂类型,例如(在 C++ 中)用户自定义类型或带有 + 操作重载的迭代器,编译器就无法对此进行优化了。

    \n

    所以如果说在你用不到所要递增变量旧值的情况下,使用前递增运算符要好过(或等价于)后递增。

    \n"},{"title":"Titan 边标签 SIMPLE 和 ONE2ONE 的区别","url":"/2017/titan-edge-label-simple-and-one2one/","content":"

    昨天在读 Titan 文档关于边的多样性时看到两个设置,分别是 SIMPLEONE2ONE,这两个设置的介绍有点绕,我琢磨了很久,最终通过程序弄明白了这两种模式的区别。

    \n

    \"\"

    \n

    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

    先给结论,一张图来解释:

    \n

    \"\"

    \n

    程序验证

    gremlin> graph = TitanFactory.open("conf/titan.properties")
    ==>standardtitangraph[cassandra:[172.24.8.84]]
    gremlin> mgmt = graph.openManagement()
    ==>com.thinkaurelius.titan.graphdb.database.management.ManagementSystem@d7109be
    gremlin> mgmt.makeEdgeLabel('simple').multiplicity(SIMPLE).make()
    ==>simple
    gremlin> mgmt.makeEdgeLabel('one2one').multiplicity(ONE2ONE).make()
    ==>one2one
    gremlin> mgmt.commit()

    gremlin> a = graph.addVertex("name", "a")
    ==>v[4152]
    gremlin> b = graph.addVertex("name", "b")
    ==>v[8248]
    gremlin> c = graph.addVertex("name", "c")
    ==>v[4128]
    gremlin> d = graph.addVertex("name", "d")
    ==>v[4328]
    \n

    先分别创建 multiplicitySIMPLEONE2ONEEdge Label,然后创建 a b c d 四个点。

    \n

    首先来验证 SIMPLE:

    gremlin> a.addEdge("simple", b)
    ==>e[1zb-37c-t1-6d4][4152-simple->8248]
    gremlin> a.addEdge("simple", b)
    An edge with the given label already exists between the pair of vertices and the label [simple] is simple
    Display stack trace? [yN] n
    gremlin> b.addEdge("simple", a)
    ==>e[2dj-6d4-t1-37c][8248-simple->4152]
    gremlin> a.addEdge("simple", c)
    ==>e[2rr-37c-t1-36o][4152-simple->4128]
    gremlin> a.addEdge("simple", d)
    ==>e[35z-37c-t1-3c8][4152-simple->4328]
    gremlin> a.addEdge("simple", c)
    An edge with the given label already exists between the pair of vertices and the label [simple] is simple
    Display stack trace? [yN] n
    gremlin> c.addEdge("simple", b)
    ==>e[16s-36o-t1-6d4][4128-simple->8248]
    \n

    得到的结论是,只要两点之间不存在相同方向的 SIMPLE 边就可以。

    \n

    然后验证 ONE2ONE

    gremlin> a.addEdge("one2one", b)
    ==>e[3k7-37c-1lh-6d4][4152-one2one->8248]
    gremlin> a.addEdge("one2one", c)
    An edge with the given label already exists on the out-vertex and the label [one2one] is out-unique
    Display stack trace? [yN] n
    gremlin> a.addEdge("one2one", d)
    An edge with the given label already exists on the out-vertex and the label [one2one] is out-unique
    Display stack trace? [yN] n
    gremlin> d.addEdge("one2one", a)
    ==>e[17h-3c8-1lh-37c][4328-one2one->4152]
    gremlin> d.addEdge("one2one", b)
    An edge with the given label already exists on the out-vertex and the label [one2one] is out-unique
    Display stack trace? [yN] n
    gremlin> b.addEdge("one2one", c)
    ==>e[3yf-6d4-1lh-36o][8248-one2one->4128]
    \n

    结论是,一个点上的 ONE2ONE 边只能有一次 in 和一次 out

    \n"},{"title":"Spring 中最常用的 5 个注解","url":"/2019/top-5-spring-annotation/","content":"
    \n

    Java 中注解的引入改变了 Java 开发人员配置应用的方式。注解在 Java 的 1.5 版本中引入进来,它使开发人员能够在代码中维护配置而不必依赖于外部配置文件。

    \n
    \n

    \"\"

    \n

    注解是可以添加到 Java 类、方法、变量、参数或者包中的一种语法元数据。

    \n

    Spring 框架推荐开发人员通过使用它提供的大量内置注解来配置应用。在这篇文章中,我们重点介绍 Spring Core 框架中最常用的几个注解。

    \n

    1. @Autowired 注解

    这个注解用于声明类中的依赖项。基于这个注解,Spring DI 框架可以注入对应的依赖。@Autowired 可以用在构造函数、属性和 setter 方法上。它是 JSR-330(Java 依赖注入)@Inject 注解的替代方法。

    \n

    属性注入

    下面的代码演示了如何将属性作为依赖项注入:

    \n
    import org.springframework.beans.factory.annotation.Autowired;

    public class UserController {

    @Autowired
    private UserRepository userRepository;

    }
    \n

    Setter 方法注入

    也可以通过 setter 方法完成依赖注入,如下所示:

    \n
    import org.springframework.beans.factory.annotation.Autowired;

    public class UserController {

    private UserRepository userRepository;

    public UserRepository getUserRepository() {
    return userRepository;
    }

    @Autowired
    public void setUserRepository(UserRepository userRepository) {
    this.userRepository = userRepository;
    }


    }
    \n

    构造函数注入

    还可以在构造函数上使用:

    \n
    import org.springframework.beans.factory.annotation.Autowired;

    public class UserController {

    private UserRepository userRepository;

    @Autowired
    public UserController(UserRepository userRepository) {
    this.userRepository = userRepository;
    }

    }
    \n

    2. @Bean 注解

    这个注解应用在方法上,并生成由 Spring 管理的 bean。Spring 配置类通常包含 bean 声明。通常,应用的 POJO 部分被声明为 Spring 组件,并且 Spring 提供的组件扫描机制会在 Spring IoC 容器中自动创建 bean。但是,当 POJO 的代码不可用并且我们需要创建 Sprint 管理的 bean 时,@bean 注解就会非常有用。

    \n

    以下代码演示了 @Bean 注解的用法:

    \n
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;

    @Configuration
    public class SampleConfiguration {

    @Bean
    public User user(){
    return new User();
    }

    @Bean(name = "admin", initMethod = "", destroyMethod = "")
    public Admin admin(){
    return new Admin();
    }
    }
    \n

    此外,这个注解还提供给了一些属性用以管理 bean 配置,例如:名称、初始化方法、销毁方法等。

    \n

    3. @Value 注解

    Spring 的 @Value 注解非常有用,可以方便地使用 Spring 表达式语言(Spring Expression Language,SpEL)提供默认值或控制变量的值。

    \n

    默认值

    在下边的示例中,name 变量配置了 @Value 注解。如果实例化 User 类时没有为 name 提供值,就会使用 @Value 配置的默认值 default-user

    \n
    import org.springframework.beans.factory.annotation.Value;

    public class User {

    @Value("default-user")
    private String name;
    }
    \n

    从环境中读取属性

    下面的示例演示如何从环境中读取一个值并赋给变量:

    \n
    public class User {

    @Value("${NAME}")
    private String name;
    }
    \n

    使用 Spring 表达式语言

    下边的示例演示如何使用 Spring 表达式语言来获取值并赋值给变量。注意这里使用了 # 来替代之前使用的 $

    \n
    public class User {

    @Value("#{systemProperties['user.name']}")
    private String name;
    }
    \n

    4. @Profile 注解

    在开发的生命周期中,应用会经历多个环境和阶段。比如,devtestuat(预发布)、industry(生产)等。根据不同的环境和阶段,需要有不同的配置。在这种情况下,@Profile 注解非常方便,它使开发人员可以灵活地控制应该激活的组件。

    \n
    @Profile("dev")
    public class DevDataSource {
    }

    @Profile("test")
    public class QADataSource {

    }
    \n

    在上边的示例中,我们提供了两个配置,一个用于 dev 环境,另一个用于 test 环境。根据环境类型,我们可以提供不同的配置,Spring 将会确保加载与之对应的配置。

    \n

    5. @Import 注解

    @Import 注解使我们可以将一个或多个组件的配置导入到另一个配置类中。

    \n
    @Configuration
    @Import(SampleConfiguration.class)
    public class AnotherConfiguration {

    @Bean
    public Product product(){
    return new Product();
    }
    }
    \n

    在上边的配置类中,我们导入了另一个配置类中定义的配置。

    \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":"

    是否可以向 UDP 的 90000 端口发送数据包?

    不可以

    \n

    TCP 或 UDP 数据包中的端口字段为 16 位,2^16 是 65536,所以最大的端口号是 65535。

    \n

    每个UDP数据包都有一个目的端口吗?

    是的

    \n

    UDP 报头为 8 个字节。

    \n

    根据 RFC,源端口是可选的,但目的端口是必须的。以下是 UDP 的报头结构:

    \n
     <-   16 bits  ->
    +-+-+-+-+-+-+-+-+--+-+-+-+-+-+-+-+-
    | source port | dest port |
    +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
    | length | checksum |
    +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
    \n

    可以发送一个长度为 100万字节的 UDP 数据包吗?

    不可以

    \n

    UDP 数据包的长度字段同样为 16 位,所以单个包的最大长度是 65535。

    \n

    可以把把 JSON 放在 UDP 数据包里吗?

    可以

    \n

    UDP 数据包中可以放入任何字节,甚至可以放一个很短的 MP3 文件。

    \n

    能否保证 UDP 数据包的到达顺序与发送顺序一致?

    不能

    \n

    发送一个UDP数据包后,有没有方法办法判断它是否到达?

    没有

    \n

    协议没有提供。

    \n

    如果把 UDP 数据包发送到同一数据中心的另一台服务器上,是否能保证到达?

    不能

    \n

    即使在同一台计算机内发送,数据包仍然可能被丢弃(例如:缓冲区满了)。

    \n

    当你发送一个 UDP 数据包时,如果发生丢失会怎样?

    那就真的就丢了

    \n

    如果想在 UDP 之上实现重试,只能自己去实现。

    \n

    操作系统的 TCP 协议实现了 TCP 包的重试。

    \n

    UDP 的 80 端口与 TCP 的 80 端口一样吗?

    不一样

    \n

    UDP 和 TCP 都支持相同的端口号(1-65535),但它们是不同的协议。

    \n

    你可以同时在 UDP 的 80 端口和 TPC 的 80 端口运行 2 个不同的服务。

    \n

    建立在 UDP 之上的协议有哪些?

    DNS、DHCP, QUIC, NTP, statsd 和各种视频会议协议。

    \n","tags":["udp"]},{"title":"关于 Unix 权限的 13 个问题","url":"/2021/unix-permission-questions/","content":"

    文件权限是多少位(bits)?

    12位

    \n

    分为 4 个组,每组 3 位。

    \n

    例如,4755 对应的是 100 111 101 101

    \n

    下面是各部分对应的内容:

    \n
    100: setuid, setgid, sticky bits
    111: user r/w/x bits
    101: group r/w/x bits
    101: other r/w/x bits
    \n

    当我们运行 ls -l 时,显示的权限是 -rwxr-xr-x,这里的 r、w 和 x 是什么意思?

    读、写、执行

    \n

    每个文件有 3 套 读/写/执行 权限:

    \n
      \n
    • 拥有该文件的用户
    • \n
    • 拥有该文件的组
    • \n
    • 其他用户
    • \n
    \n

    如果一个文件的权限是 0644,拥有该文件的组是否能写这个文件?

    不能

    \n

    0644 在二进制中是 000 110 100 100

    \n

    说明如下:

    \n
    000
    110 拥有该文件的用户可以读写此文件
    100 拥有该文件的组可以读此文件
    100 其他用户可以读此文件
    \n

    所以任何人都可以读取该文件,但只有拥有该文件的用户才可以写文件。

    \n

    操作系统内核是否关心你的用户名是什么?

    不关心

    \n

    内核基于 用户ID/组ID 进行所有的权限检查 —— 用户名和组名的存在只是为了让人类更容易识别和使用。

    \n

    如果一个目录被设置为可读权限,这意味着什么?

    意味着你可以列出该目录中的文件

    \n

    对于目录来说,下面是读/写/执行的含义:

    \n
      \n
    • 读:你可以列出文件
    • \n
    • 写:你可以创建文件
    • \n
    • 执行:你可以进入该目录并访问其下的文件
    • \n
    \n

    如果一个文件的权限被设置为 0666,这是否意味着任何人都可以阅读它?

    不一定

    \n

    如果该文件的父目录的执行位被置为 0,这将使你无法读取该目录下的任何文件。

    \n

    如果一个文件的权限被设置为 0000,这是否意味着没有人可以读它?

    不是

    \n

    root 可以读写权限为 0000 的文件。

    \n

    每个进程都有一个用户ID(UID)吗?

    是的

    \n

    当你以用户身份登录时,几乎你启动的所有进程都会把它们的 UID 设置为你的 UID。

    \n

    一个进程可以有多个组ID(GID)吗?

    是的

    \n

    进程有一个主 GID,也有一个补充(supplementary)组ID列表。文件权限检查将检查进程的任何一个组ID是否与文件的所有者匹配。

    \n

    如果你把一个用户添加到一个组,以该用户身份运行的现有进程是否会自动将该 GID 添加到他们的 GID 列表中?

    不会

    \n

    退出并重新登陆后才会生效。

    \n

    setuid 位的作用是什么?

    在一个可执行文件上,它意味着该进程将以文件所有者的 UID 运行

    \n

    例如,passwd(用来修改密码)通常设置了setuid位,因为它需要以 root 身份运行,以便能够写入修改密码的文件。

    \n

    一个没有特权的进程有可能改变其 UID 吗?

    不可能

    \n

    你必须有超级用户的权限来改变你的 UID。

    \n

    为什么 sudo 可以让你以 root 身份运行命令?

    它设置了 setuid 位

    \n

    sudo 总是以 root 身份运行。

    \n

    所以如果 /etc/sudoers 允许你以 root 身份启动程序,则它将以 root 身份为你启动程序。

    \n"},{"title":"利用 AWS Lambda 定期清理 S3 文件","url":"/2022/use-aws-lambda-delete-s3-regularly/","content":"

    背景

    因为我的 bossku ,需要定期将全量数据库数据进行备份,我之前写过一篇文章分享我是如何将数据库备份到 S3 的:https://jiapan.me/2020/auto-backup-database/

    \n

    由于不想为这个存储付费,所以我在 Things 中创建了一个周期性的任务,每周六提醒我来清理前一段时间的过期数据,通常我只保留最近两天的,将其余的删除。

    \n

    最开始我是登录到 S3 的网站上进行操作,后来嫌麻烦,就将 S3 挂载到了本地(使用的是 QSpace 这个软件),每周六定期在本地进行删除操作。

    \n

    本着 DRY(Don’t repeat yourself)原则,能自动化的事就不要自己重复去做,所以我准备写个脚本定期处理。

    \n

    S3 提供了很完善的 API 可以让程序方便的进行操作,各个语言也都提供了 S3 API 的 SDK 封装,我要做的就是周期性的调取文件列表,判断如果文件超过 2 天则进行删除。这样的动作使用 Serverless 最合适不过了,这一次我还是选择使用我最熟悉的 AWS Lambda ,使用的语言也是万能、灵活的 Python。

    \n

    初始化项目

    首先我们初始化一个 Serverless 项目:

    \n
    SLS_GEO_LOCATION=en serverless create --template aws-python --path s3-clean
    \n

    没有 serverless 的可以先参考官方手册 进行安装,这不是本文的重点。

    \n

    注意下上边命令最前边的 SLS_GEO_LOCATION=en,这个一定要加,因为 serverless 做了一件有些流氓的事:判断你的所在地是中国的话,会走腾讯的服务,他们是没有 aws 模板的,报错如下:

    \n

    \"1.png\"

    \n

    加上 SLS_GEO_LOCATION=en 可以将我们的地区强制指定到国外,这样就会走官方的逻辑(腾讯这个行为太 low 了)。

    \n

    进入上边 serverless 为我们创建出来的项目,可以看到生成好了两个文件,我们来编辑 handler.py 文件:

    \n
    import os  
    import boto3


    def delete_expire():
    # 初始化 s3 sdk
    \ts3 = boto3.resource('s3',
    \t\tregion_name=os.environ['S3_REGION'],
    \t\taws_access_key_id=os.environ['S3_ACCESS_KEY_ID'],
    \t\taws_secret_access_key=os.environ['S3_SECRET_ACCESS_KEY'])

    # 绑定 bucket bucket = s3.Bucket(os.environ['S3_BUCKET'])
    print('Objects:')

    # 获取bucket中所有文件
    all_file = []
    for item in bucket.objects.all():
    print(' - ', item.key)
    all_file.append(item)

    # 文件超过3个时进行清理工作
    if len(all_file) > 2:
    # 文件按照上次修改时间排序
    all_file.sort(key=lambda x: x.last_modified)

    # 只保留最后两个文件,删除其余文件
    for item in all_file[:-2]:
    item.delete()
    print(\"%s deleted\" % item.key)
    print(\"delete done\")
    return True
    else:
    print(\"less than 3 ignore\")
    return False


    def delete_backup(event, context):
    delete_expire()
    return {
    \"statusCode\": 200,
    }
    \n

    流程比较简单:

    \n
      \n
    1. 初始化 sdk
    2. \n
    3. 关联 bucket
    4. \n
    5. 取全部文件列表
    6. \n
    7. 对全部文件按照时间正序排(旧的在前)
    8. \n
    9. 删除倒数第二个文件之前的所有文件
    10. \n
    \n

    上边代码中一些参数通过环境变量进行获取,稍后我们会在配置文件中介绍这几个参数。

    \n

    然后我们编辑 serverless.yml 文件,这个是我们服务的配置文件:

    \n
    service: bossku-s3-clean  

    frameworkVersion: '3'

    provider:
    name: aws
    region: ${env:S3_REGION}
    runtime: python3.8
    environment:
    S3_REGION: ${env:S3_REGION}
    S3_BUCKET: ${env:S3_BUCKET}
    S3_ACCESS_KEY_ID: ${env:S3_ACCESS_KEY_ID}
    S3_SECRET_ACCESS_KEY: ${env:S3_SECRET_ACCESS_KEY}

    functions:
    delete_backup:
    handler: handler.delete_backup
    events:
    - schedule: cron(45 0 * * ? *)
    - http:
    path: delete_backup
    method: get
    \n

    provider.region 用来指定我们的服务启动在哪个地区,我这里配置了一个 S3_REGION 的占位,用来从我本地环境变量获取,目的是和我们的 S3 在同一个地区,这样理论上连通性会跟好一些。

    \n

    provider.environment 就是给程序提供运行时环境变量的地方,也就对应我们程序中 os.environ['xxx'],每一个我都和本地一个同名的环境变量相关联。

    \n
      \n
    • S3_REGION 表示存储文件时使用的 S3 区域,比如:ap-east-1
    • \n
    • S3_BUCKET 用来指定程序要读写的 bucket
    • \n
    • S3_ACCESS_KEY_ID S3 API 的 ACCESS KEY
    • \n
    • S3_SECRET_ACCESS_KEY S3 API 的 SECRET KEY
    • \n
    \n

    再往下的 functions 是用来声明函数的区域,我将我们的 delete_backup 关联了两个事件:

    \n
      \n
    1. 每天 0 点 45 分(北京时间早上 8 点 45 分)定时启动。
    2. \n
    3. 让 Lambda 提供给我们一个 HTTP 的 GET 请求,用来手动触发便于调试。
    4. \n
    \n

    做完这些我们还有一个工作,将程序所依赖的 boto3 安装在项目目录下,这样就会在发布时会一起上传到 Lambda 中,Lambda 本身是不带这个包的,而且不支持 pip 安装。

    \n
    pip install boto3==1.23.8 --target=.
    \n

    接下来就可以部署到 Lambda 进行验证了,我们可以先将程序中的 item.delete() 进行注释,观察下日志看看流程是否正常。

    \n

    部署

    部署脚本如下:

    \n
    export AWS_ACCESS_KEY_ID=YOUR_AWS_ACCESS_KEY_ID
    export AWS_SECRET_ACCESS_KEY=YOUR_AWS_SECRET_ACCESS_KEY
    export S3_REGION=YOUR_S3_REGION
    export S3_BUCKET=YOUR_S3_BUCKET
    export S3_ACCESS_KEY_ID=YOUR_S3_ACCESS_KEY_ID
    export S3_SECRET_ACCESS_KEY=YOUR_S3_SECRET_ACCESS_KEY

    serverless deploy
    \n

    \"3.png\"

    \n

    如图可以看到发布成功了,我们访问 Lambda 提供给我们的 endpoint 来手动触发下这个函数。

    \n

    \"\"

    \n

    成功了,我们再到 Lambda 的操作界面看下日志,我通常是在函数的【监控】-【查看 CloudWatch 中的警报】-【日志组】中看日志,应该有我不知道的更方便的方式,后边学会了再做补充吧。

    \n

    \"\"

    \n

    可以看到整个流程是 ok 的。

    \n

    我们将 item.delete() 的注释取消掉再发布一次就可以了。

    \n

    通过 Bark 通知我

    为了在每次删除后都能及时的收到通知,我通过 Bark 给我的手机发个通知。我们只需将 delete_backup 函数改成这样就可以了:

    \n
    def delete_backup(event, context):  
    if delete_expire():
    requests.get('https://api.day.app/{YOUR_KEY}/Bossku备份清理成功')
    else:
    requests.get('https://api.day.app/{YOUR_KEY}/Bossku备份清理失败')
    return {
    \"statusCode\": 200,
    }
    \n

    别忘了在本地目录安装 requests 包:

    \n
    pip install requests==2.27.1 --target=.
    \n

    再次发布,然后我手动触发两次,第一次文件超过 2 个所以可以执行成功,第二次文件不足两个执行失败,符合我们的预期。

    \n

    \"6.png\"

    \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 即可,其他保持默认:

    \n


    \n

    我们在通过 SDK 写入数据时会自动帮我们将需要的列创建出来,所以也不用做列的新增。

    \n

    handler

    再来看下核心代码:

    \n
    import os  
    import requests
    import leancloud

    def blog_comment(event, context):
    leancloud.init(os.environ['LEANCLOUD_APPID'], os.environ['LEANCLOUD_APP_KEY'])

    # 取出最近10条评论
    Comment = leancloud.Object.extend('Comment')
    query = Comment.query.descending(\"createdAt\")
    comments = query.limit(10).find()
    print(len(comments))

    # 判断是否有新评论
    new_comment = 0
    BarkComment = leancloud.Object.extend('BarkComment')
    for comment in comments:
    if not BarkComment.query.equal_to(\"commentId\", comment.id).find():
    new_comment += 1
    bark_comment = BarkComment()
    bark_comment.set(\"commentId\", comment.id)
    bark_comment.save()

    # 如果有新评论发送通知
    if new_comment > 0:
    print(\"=============\")
    print(new_comment)
    msg = \"博客收到 %d 条新评论\" % (new_comment,)
    requests.get('https://api.day.app/YOUR_KEY/' + msg)
    else:
    print(\"no new comment\")

    return {
    \"statusCode\": 200,
    }
    \n

    看过之前文章的已经知道,我们需要将 leancloudSDK 在项目目录下也下载一份:

    \n

    pip install leancloud==2.9.10 --target=.

    \n

    serverless.yml

    provider.environment 新增本次需要的环境变量:

    \n
    provider:
    ...
    environment:
    ...
    LEANCLOUD_APPID: ${env:LEANCLOUD_APPID}
    LEANCLOUD_APP_KEY: ${env:LEANCLOUD_APP_KEY}
    LEANCLOUD_API_SERVER: ${env:LEANCLOUD_API_SERVER}
    \n

    functions 空间内添加:

    \n
    functions:  
    ...

    blog_comment:
    handler: handler.blog_comment
    events:
    - schedule: cron(*/5 * * * ? *)
    - http:
    path: blog_comment
    method: get
    \n

    表示每 5 分钟执行一次,同时提供给我们一个 HTTP 终端来进行调试。

    \n

    我们将这个服务进行发布,手动访问下分配给我们的 endpoint,通过下图可以看到我已经收到了通知:

    \n

    \n

    因为之前 BarkComment 表中没有数据,所以系统认为这 10 条都是新评论。

    \n

    然后我自己在博客里留了 3 条评论,等待每个 5 分自动执行的时候再看下效果:

    \n

    \n

    在 14 点 25 分时,我们收到了「博客收到3条新评论」的通知,说明我们的程序成功判断出了新的增量数据,同时可以检查下 BarkComment 表也确实有了新数据。

    \n

    其实我们可以做的更完善一些,比如通知我们具体是哪篇文章有新评论,评论的内容是什么等等。大家需要的话可以自己实现,我只在这里进行抛砖。

    \n"},{"title":"使用 gist 管理动态配置","url":"/2023/use-gist-manage-config/","content":"

    上一篇文章中提到,我找到了一个非常方便的方法来管理token,那就是使用Github提供的 Gist 功能。

    \n

    https://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 就可以进入编辑页面。

    \n

    \n

    获取原始内容

    链接尾部加上 /raw 可以取得原始内容。

    \n

    如:https://gist.githubusercontent.com/Panmax/5e3444141772e987719147a316782f54/raw/

    \n

    \n

    通过上述特性,我们可以将Gist用作动态配置的管理工具。

    \n

    程序可以通过使用 /raw 获取动态内容,我们可以使用 /edit 页面更新内容。理论上,只要不泄露 Gist 链接,您的内容就不会泄露。当然,还要确保源代码不会泄露。

    \n

    有些人可能认为这样不安全,因为数据存放在互联网上,获取链接的人就可以访问数据。

    \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":"

    这篇文章完全是要写两件事:

    \n
      \n
    1. 安装 virtualenvwrapper 后如何配置
    2. \n
    3. Python 2 上安装 iPython
    4. \n
    \n

    如果分成两篇文章来写的话,每篇文章就会非常短,不值当的,所以直接合成一篇来写。

    \n
    \n

    配置 virtualenvwrapper

    安装 virtualenvwrapper 的过程就不再讲解了,直接 pip install 就可以完成,主要是安装完成后的配置,因为每次我装完都需要问一下谷歌然后才能继续,所以不如记到自己的 Blog 下,即便下次再忘了也能快速找到解决方法。

    \n

    安装完 virtualenvwrapper 后,要根据自己使用的 shell 来配置不同的文件,比如 bash 需要配置 .bashrczsh 配置 .zshrc

    \n

    配置如下:

    \n
    export WORKON_HOME=$HOME/.virtualenvs
    source /usr/local/bin/virtualenvwrapper.sh
    \n

    第一行是给定一个虚拟环境保存的目录,第二行是执行 virtualenvwrapper 的脚本使 workon, mkvirtualenv 等命令生效。

    \n

    大多数时候都卡在第二行那个命令上,因为不同发行版的机器 virtualenvwrapper.sh 所在位置不同,所以需要通过:

    \n

    find / -name virtualenvwrapper.sh

    \n

    找到 virtualenvwrapper.sh 所在的位置后,根据自己机器上的实际位置来写那一行脚本。

    \n

    修改完成后保存退出,重新启动一个命令窗口,检查有没有配置成功。

    \n
    \n

    在 Python 2 上安装 iPython

    最新版的 iPython 已经不支持 Py2 了,所以直接用 pip 安装 iPython 时,会提示安装失败,所以要手动指定安装版本。

    \n

    最后一个支持 Py2 的版本是 5.4.0,所以用 pip install ipython==5.4.0 就行了。

    \n
    \n

    UPDATE AT 2017-07-19

    \n
    \n

    今天在一台服务器上将 pip 改为了阿里源后发现安装 ipython==5.4.0 时会报错:

    \n
    Running setup.py egg_info for package ipython
    error in ipython setup command: Invalid environment marker: sys_platform == "win32" and python_version < "3.6"
    Complete output from command python setup.py egg_info:
    error in ipython setup command: Invalid environment marker: sys_platform == "win32" and python_version < "3.6"
    \n

    这个问题使用 pip install pip --upgrade 将 pip 更新为最新版本就可以解决了。

    \n

    顺便再记一下 pip 源的地址,虽然知道修改方法,但每次还要去网上搜一下源地址

    \n

    vi ~/.pip/pip.conf

    \n
    [global]
    trusted-host = mirrors.aliyun.com
    index-url = http://mirrors.aliyun.com/pypi/simple
    \n"},{"title":"Python、Java、GoLang 基于 Web 的性能测试","url":"/2019/web-benchmark-for-python-java-golang/","content":"

    最近一段时间学习了一下 Go 这门语言,其中提到最多的就是 GoLang 的高性能 & 高并发,所以本着没有对比就没有伤害的原则,我准备将其与另外两个我所掌握的语言(Python、Java)进行一个简单的性能对比。

    \n

    测试环境\u0001

    我的 MacBook Pro,12个逻辑CPU + 16G内存

    \n

    测试工具

    https://github.com/wg/wrk

    \n

    wrk -t8 -c100 -d30s --latency http://www.baidu.com

    \n

    模拟8线程、100个并发,持续30秒的性能测试

    \n

    实现

    \n

    以下程序完整源码已放在 GitHub:https://github.com/Panmax/web-benchmark

    \n
    \n

    Python

    框架:Flask
    容器:Gunicorn
    运行环境:Docker

    \n

    核心代码:

    # -*- coding: utf-8 -*-

    from flask import Flask
    app = Flask(__name__)

    @app.route("/")
    def hello():
    return "Hello Python!"

    if __name__ == '__main__':
    app.run()
    \n

    Dockerfile

    FROM ubuntu:14.04

    ADD sources.list /etc/apt/sources.list
    ADD pip.conf ~/.pip/pip.conf

    # Update OS
    # RUN sed -i 's/# \\(.*multiverse$\\)/\\1/g' /etc/apt/sources.list
    RUN apt-get update
    RUN apt-get -y upgrade

    # Install Python
    RUN apt-get install -y python-dev python-pip

    # Add requirements.txt
    ADD requirements.txt /webapp/requirements.txt

    # Install gunicorn Python web server
    RUN pip install gunicorn==19.6.0
    # Install app requirements
    RUN pip install -r /webapp/requirements.txt

    # Create app directory
    ADD . /webapp

    # Set the default directory for our environment
    ENV HOME /webapp
    WORKDIR /webapp

    # Expose port 5000 for gunicorn
    EXPOSE 5000

    ENTRYPOINT ["gunicorn", "-w", "24", "wsgi:app", "-b", "0.0.0.0:5000", "-n", "docker-flask", "--timeout", "45", "--max-requests", "10000"]
    \n

    这里设置 24 个 worker,因为我的机器有 12 个逻辑CPU

    \n

    启动命令

    docker build -t panmax/docker-flask-benchmark .
    docker run -d --name docker-flask-benchmark --restart=always -p 8081:5000 panmax/docker-flask-benchmark
    \n

    Java

    框架:SpringBoot

    \n

    容器采用 SpringBoot 的默认 tomcat 容器,不进行其他修改。

    \n

    核心代码

    package com.jpanj.benchmark;

    import org.springframework.boot.SpringApplication;
    import org.springframework.boot.autoconfigure.SpringBootApplication;
    import org.springframework.web.bind.annotation.GetMapping;
    import org.springframework.web.bind.annotation.RestController;

    @SpringBootApplication
    @RestController
    public class BenchmarkApplication {

    \tpublic static void main(String[] args) {
    \t\tSpringApplication.run(BenchmarkApplication.class, args);
    \t}

    \t@GetMapping
    public String hello() {
    return "Hello Java!";
    }

    }
    \n

    配置文件

    server:
    port: 8082
    \n

    启动命令

    ./gradlew build -xtest
    cd build/libs

    java -jar benchmark-0.0.1-SNAPSHOT.jar
    \n

    GoLang

    框架:Gin

    \n

    不需要配置任何容器

    \n

    核心代码

    package main

    import (
    \t"github.com/gin-gonic/gin"
    \t"net/http"
    )

    func main() {
    \trouter := gin.Default()
    \trouter.GET("", func(c *gin.Context) {

    \t\tc.String(http.StatusOK, "Hello GoLang!")
    \t})
    \trouter.Run(":8083")
    }
    \n

    启动命令

    go build .
    ./gin-benchmark
    \n

    go build 可以直接编译出一个可以执行文件,这个二进制文件可以直接放在其他机器上无需安装任何环境就可以运行起来,甚至可以在 Mac 上编译 Linux / Windows 的可执行文件,在 Linux 上编译 Mac / Windows 的可执行文件,这个特性非常爽。

    \n
    \n

    通过浏览器可以验证以上使用 3 种语言开发的简单 Web 程序已经启起来了:

    \n

    \"\"

    \n

    接下来我们逐个进行性能测试:

    Python

    ➜ wrk -t8 -c100 -d30s --latency http://127.0.0.1:8081/
    Running 30s test @ http://127.0.0.1:8081/
    8 threads and 100 connections
    Thread Stats Avg Stdev Max +/- Stdev
    Latency 21.45ms 12.75ms 237.13ms 85.43%
    Req/Sec 332.10 111.19 640.00 71.40%
    Latency Distribution
    50% 19.20ms
    75% 26.01ms
    90% 33.23ms
    99% 90.03ms
    15917 requests in 30.08s, 2.63MB read
    Socket errors: connect 0, read 560, write 0, timeout 0
    Requests/sec: 529.21
    Transfer/sec: 89.41KB
    \n

    Java

    ➜ wrk -t8 -c100 -d30s --latency http://127.0.0.1:8082/
    Running 30s test @ http://127.0.0.1:8082/
    8 threads and 100 connections
    Thread Stats Avg Stdev Max +/- Stdev
    Latency 8.22ms 26.99ms 438.58ms 93.31%
    Req/Sec 6.93k 3.00k 16.38k 50.11%
    Latency Distribution
    50% 1.26ms
    75% 2.09ms
    90% 12.81ms
    99% 132.29ms
    1631200 requests in 30.06s, 194.75MB read
    Requests/sec: 54256.97
    Transfer/sec: 6.48MB
    \n

    GoLang

    ➜ wrk -t8 -c100 -d30s --latency http://127.0.0.1:8083/
    Running 30s test @ http://127.0.0.1:8083/
    8 threads and 100 connections
    Thread Stats Avg Stdev Max +/- Stdev
    Latency 1.80ms 1.95ms 24.69ms 85.85%
    Req/Sec 8.68k 786.97 11.03k 65.72%
    Latency Distribution
    50% 1.36ms
    75% 2.68ms
    90% 4.32ms
    99% 8.42ms
    2078830 requests in 30.10s, 257.73MB read
    Requests/sec: 69064.35
    Transfer/sec: 8.56MB
    \n
    \n

    可以看到,在每秒请求数量(Requests/sec),也就是并发能力方面,测试结果为:

    \n
      \n
    • Python: 529.21
    • \n
    • Java: 54256.97
    • \n
    • GoLang: 69064.35
    • \n
    \n

    线程平均延迟(Thread Stats - Avg - Latency)的测试结果为:

    \n
      \n
    • Python: 21.45ms
    • \n
    • Java: 8.22ms
    • \n
    • GoLang: 1.80ms
    • \n
    \n

    可以看出,Go 在性能方面甩出 Python 几十条街是没有问题的,比 Java 的性能确实也好很多。

    \n
    \n

    最后说明一下,这个测试可能存在不严谨性,但是我所采用的部署方案是大部分公司或者程序员最常使用的方式,也能在一定程度上说明问题。

    \n
    \n"},{"title":"微信登录流程梳理","url":"/2018/wechat-login/","content":"

    如果没什么意外,接下来要实现一个对接微信登录的需求,今天浏览了下相关文档,简单在这里用自己的文字把整个流程描述一下。因为还没有实际操作,所以可能会有理解上的偏差,真正实现后会再来修改和补充。

    \n

    文档链接:https://mp.weixin.qq.com/wiki?t=resource/res_main&id=mp1421140842

    \n

    首先在微信公众平台中的「开发 - 接口权限 - 网页服务 - 网页帐号 - 网页授权获取用户基本信息」设置一个回调 URL,这里配置一个二级域名就行了,例如 wechat.xxx.com

    \n

    用户在登录时,先通过 snsapi_base 接口获取用户 open_id,然后在我们的系统中通过这个 open_id 来获取用户信息,如果存在则直接完成登录,因为这种方式是用户无感的,使用体验比较友好。但这种方式无法获取用户最新的微信信息,不过通常来说不重要,因为用户会在应用内重新维护自己的资料,所以首次拿到用户头像昵称等信息后就可以了,之后不需要再通过重新获取用户信息来更新了。

    \n

    如果查询 open_id 不存在的话,再通过 snsapi_userinfo 方式来获取用户信息,获取用户信息的流程如下:

    \n
      \n
    1. 发起一个重定向让用户到 snsapi_userinfo 授权页面,用户点击登录按钮,然后系统会访问我们的回调地址并带上一个 code
    2. \n
    3. 我们通过 code 换取 access_tokenrefresh_token,可以把 refresh_token 保存起来,这样的话近 30 天内都可以通过这个直接换取 access_token 来拉取用户资料,但通常没必要。
    4. \n
    5. 通过 access_toekn + open_id 拉取用户资料。
    6. \n
    \n

    拉取完资料后将用户 open_id 和资料保存在我们自己的数据库中,这个步骤可以看具体业务逻辑,大部分业务会在首次登录时将用户资料拉取下来展示在一个编辑页面,用户可以编辑后再提交保存,不管那种方式我们都已经为这个 open_id 创建了用户,这样之后用户再访问我们的应用时就无需再点授权按钮了。

    \n"},{"title":"记体重与学英语","url":"/2023/weight-and-english/","content":"

    记录体重

    人们常说减肥是一辈子的事情,我以前也是这么认为的。我也一直在为减肥而努力,最早从大学就开始努力了(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

    坚持一件大事,是靠价值观;长期坚持一件大事,是靠信仰。

    \n
    \n"},{"title":"我获取信息的 6 个渠道","url":"/2022/what-i-access-to-information/","content":"

    我日常获取信息主要有如下 6 个渠道:

    \n
      \n
    • Twitter
    • \n
    • NewsLetter
    • \n
    • 掘金的 GitHub 热门榜
    • \n
    • GitHub Following && For you
    • \n
    • Telegram Channel
    • \n
    • 书籍
    • \n
    \n

    Twitter

    我在上大学期间做过的最有价值、对我人生影响最大的一件事,可能就是掌握了科学上网的手段,并且从大二开始没有再用过百度搜索,把搜索这个任务全部交给了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 在国内有流行起来的趋势,国内也出现了做 NewsLetter 业务的平台,如竹白,我也订阅了一些 Letters,有免费的也有付费的,下边分享几个我觉得质量不错的 Letter(排名不分先后),这些 Letter 我基本都是通过 Twitter 发现的:

    \n\n

    我会在上班想摸🐟时翻看一下近期的 NewsLetter,我统一用 Google 邮箱接收这些信件,同时设置好了规则,让这些信件汇总在一个目录下,并且不会实时给我发推送(因为我并不需要立即阅读它们)。

    \n

    \n

    掘金的 GitHub 热门榜

    我用掘金提供的插件作为浏览器新标签页的默认首页,这个页面中间一栏有 GitHub 上的项目列表,我会不定期看一些我关注语言(如 Go、Rust、Python)又出了哪些新玩具,按热门排序,如果想看新鲜有趣的就看今日,如果想看长盛不衰的就看本周或者本月。

    \n

    \n

    GitHub Following && For you

    上边提到的掘金的 GitHub 项目列表,是按照 Star 增量和项目创建时间计算得出的,所有人看到的都是相同的静态数据,GitHub 官方也有自己的两个 Feed 流,分别叫 Following 和 For you。

    \n

    Following 是看你关注的人的动态(如他关注了什么项目、他贡献了什么项目)

    \n

    \n

    For you 是 GitHub 今年新推出的根据我们的喜好,使用算法推荐给我们的与我们相关、我们可能感兴趣的项目。

    \n

    \n

    Telegram Channel

    Telegram channel 是个小宝藏,类似于一个除了群主其他人禁止发言的群,群主产生优质内容后随时发到群里,列几个我自己常看的 channel:

    \n\n

    \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

    假设让你来做一个在线购物网站的数据库设计,按照传统的数据库设计方案将会有 usersproductsorders 表 —— 用来代表系统的状态

    \n

    再假设你完成了前期的编码工作并发布上线了这个在线购物网站,几周后你的老板想知道用户平均更新电子邮箱的次数。

    \n

    在这种传统的数据库设计中,当用户更新电子邮箱时,执行的查询语句大致如下:

    \n
    UPDATE users SET email='newemail@mail.com' WHERE id=1;
    \n

    问题在于,我们并没有在数据库中存储修改电子邮件的事件日志。

    \n

    你可以创建一个额外的列 event_log 并在每次用户更新电子邮箱后记录一次 user changed email。但这样仍存在一些问题:

    \n
      \n
    • 需要额外的开发工作才能支持这个特性
    • \n
    • 使得数据库设计更加复杂
    • \n
    • 只有在实现这个功能后才能生成这些事件,无法对之前产生过的事件进行追溯
    • \n
    \n

    这是让事件溯源派上用场的绝佳场景。

    \n

    根据事件溯源设计,你不需要存储系统状态。取而代之的是存储事件

    \n

    比如:当用户注册时,一个 UserCreated 事件被存储。之后当用户更新电子邮箱时,一个 UserChangedEmail 事件被存储。

    \n

    \"事件溯源系统中的用例事件\"

    \n
    事件溯源系统中的用例事件
    \n\n

    为什么使用事件溯源

    代表我们的思考方式

    在现实世界里,人们思考的是事件:当其他人询问你今天过得怎么样时,你会告诉他们一些发生过的有趣事件,不大可能描述你在某一时刻的确切状态。

    \n

    同样,一个领域专家在描述业务流程时谈论的也是一系列事件。通过事件溯源让我们在系统中对其建模变得更加容易。

    \n

    容易生成报告

    想知道用户修改了多少次电子邮箱?通过事件溯源,你已经将这些数据详尽记录在案了。

    \n

    想知道一个商品在购物车中被用户移除了多少次?只需简单的对 EventRemovedFromCart 事件进行计算就可以了。

    \n

    通过事件溯源,你可以对你的数据进行全方位的洞察,同时使生成的报告可以追溯。

    \n

    你拥有了可靠的审计日志

    你可以生成审计日志用来准确记录系统如何进入了某个状态。

    \n

    比如,考虑一下你的银行账户,事件溯源生成了交易事件的日志,这样就可以清楚的说明为什么你每个月的工资都不够花了。

    \n

    为什么不使用事件溯源

    听起来不错,但事件溯源有什么要注意的呢?

    \n

    事件溯源增加了系统额外的复杂性,更多的复杂性意味着难以让新进入的开发人员上手,花更多的时间添加新功能并且也让系统更难以维护。

    \n

    如果你要构建的是一个规模较小的系统,不需要安全审计日志,此时使用事件溯源方法带来的麻烦可能比它的价值更大。

    \n"},{"title":"为什么我说宝玉是双子座","url":"/2023/why-baoyu-is-gemini/","content":"

    我在上一篇流水账中提到贾宝玉是双子座,虽然宝玉生日在《红楼梦》原文中一直没有明确写明,但通过一些线索可以推断出宝玉生日是在夏天,且是在农历四、五月左右。证据如下:

    \n
      \n
    • 在六十三回「寿怡红群芳开夜宴」一回中,宝玉晚上想搞个 party,林之孝家过来象征性嘱咐了两句:“还没睡呢?如今天长夜短了,该早些睡,明儿起的方早……”「天长夜短」很明显是夏天的特点。
    • \n
    • 六十二回「呆香菱情解石榴裙」中,由于香菱和其他姐妹玩斗草游戏才把石榴裙弄脏了,而且也已经提到这一天是宝玉的生日。「斗草」是端午节的游戏,前文中没有描述过斗草,也没提到端午节,所以姐妹们大概率是在端午节前玩的。
    • \n
    • 还是六十二回「憨湘云醉眠芍药裀」,北京地区芍药的盛花期是阳历四五月间,芍药花飞了湘云一身,说明已经是凋谢期了,按照花期来推的话应该是阳历五月底。
    • \n
    • 网上还有很多证据说宝玉生日就是农历四月二十六,比如根据张道士说的话、送花神等等
    • \n
    \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

    路径依赖法则:是指人类社会中的技术演进或制度变迁均有类似于物理学中的惯性,即一旦进入某一路径(无论是「好」还是「好」)就可能对这种路径产生依赖。一旦人们做了某种选择,就好比走上了一条不归之路,惯性的力量会使这一选择不断自我强化,并让你轻易走不出去。第一个使「路径依赖」理论声名远播的是道格拉斯・诺斯,由于用「路径依赖」理论成功地阐释了经济制度的演进,道格拉斯・诺斯于 1993 年获得诺贝尔经济学奖。「路径依赖」理论被总结出来之后,人们把它广泛应用在选择和习惯的各个方面。在一定程度上,人们的一切选择都会受到路径依赖的可怕影响,人们过去做出的选择决定了他们现在可能的选择,人们关于习惯的一切理论都可以用「路径依赖」来解释。

    \n
    \n

    在现实中我们经常看到这个法则随处都会发生,微信刚出来的时候很多人说这不就是手机上的 QQ 吗,朋友圈刚出来的时候他们又会说这不就是抄袭微博吗。很多时候当你兴致冲冲给朋友介绍一个新的东西时,朋友一句话就能让你万念俱灰:这不就是 XXX 吗?之所以这样,是因为人类在接触到新知识新概念的时候,都会下意识的使用以前知道的概念进行套用,这样的思维方式是人类从小到大学习新事物的时候使用的模式,它已经固化成我们大脑操作系统的一部分了。

    \n

    理解了这个法则,我们就可以很容易的明白,已经在单体架构下开发了多年的软件工程师,当被要求要使用微服务架构来进行设计和开发的时候,本能的反应方式肯定是:这不就是把原来的单体做小了吗?但是这样做出来的「微服务」真的能够给我们带来微服务架构的那些好处吗?真的能提高一个企业的数字化响应力吗?

    \n

    不断变化的软件需求和经常被视为效率低下的软件开发一直都是这个行业里最难解决的顽疾,从瀑布到敏捷,都是在尝试找到一个解决这个顽疾的方法,领域驱动设计(Domain Driven Design)也是其中一个药方,而且随着十多年的不断实践,我们发现这个药方有它自己的独特之处,下面我们先来介绍一下这个药方。

    \n

    DDD 简史

    \"\"

    \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

    DDD 思辨

    从计算机发明以来,人类用过表达世界变化的词有:电子化,信息化,数字化。这些词里面都有一个「化」字,代表着转变,而这些转变就是人类在逐渐的把原来在物理世界中的一个个概念一个个工作,迁移到虚拟的计算机世界。但是在转变的过程中,由于两个世界的底层逻辑以及底层语言不一致,就必须要有一个翻译和设计的过程。这个翻译过程从软件诞生的第一天起就天然存在,而由于有了这个翻译过程,业务和开发之间才总是想两个对立的阶级一样,觉得对方是难以沟通的。

    \n

    \"\"

    \n

    于是乎有些软件工程界的大牛就开始思考,能不能有一种方式来减轻这个翻译过程呢。然后就发明了面向对象语言,开始尝试让计算机世界有物理世界的对象概念。面向对象还不够,这就有了 DDD,DDD 定义了一些基本概念,然后尝试让业务和开发都能够理解这些概念名词,然后让领域专家使用这些概念名词来描述业务,而由于使用了规定的概念名词,开发就可以很好的理解领域业务,并能够按照领域业务设计的方式进行软件实现。这就是 DDD 的初衷:让业务架构绑定系统架构。

    \n

    \"\"

    \n

    用 DDD 走出设计微服务拆分困境

    上面介绍了使用 DDD 可以做到绑定业务架构和系统架构,这种绑定对于微服务来说有什么关系呢。所谓的微服务拆分困难,其实根本原因是不知道边界在什么地方。而使用 DDD 对业务分析的时候,首先会使用聚合这个概念把关联性强的业务概念划分在一个边界下,并限定聚合和聚合之间只能通过聚合根来访问,这是第一层边界。然后在聚合基础之上根据业务相关性,业务变化频率,组织结构等等约束条件来定义限界上下文,这是第二层边界。有了这两层边界作为约束和限制,微服务的边界也就清晰了,拆分微服务也就不再困难了。

    \n

    \"\"

    \n

    而且基于 DDD 设计的模型中具有边界的最小原子是聚合,聚合和聚合之间由于只通过聚合根进行关联,所以当需要把一个聚合根从一个限界上下文移动到另外一个限界上下文的时候,非常低的移动成本可以很容易地对微服务进行重构,这样我们就不需要再纠结应不应该这样拆分微服务?拆出的微服务太少了以后要再拆分这样的问题了。

    \n

    所以,经过理论的严密推理和大量实践项目的验证,ThoughtWorks 认为 DDD 是当前软件工程业界设计微服务的最佳实践。虽然学习和使用 DDD 的成本有点高,但是如果中国的企业想在软件开发这个能力上从冷兵器时代进入热兵器时代,就应该尝试一下 DDD 了解一下先进的软件工程方法。

    \n
    \n

    本文转自:https://www.jianshu.com/p/e1b32a5ee91c 并修改了几处错别字

    \n
    \n"},{"title":"为什么用个人博客","url":"/2023/why-use-personal-blog/","content":"

    为什么我要用一个关联了个人域名,毫不起眼的个人博客写流水账,而不是在时下更流行的公众号、简书、知乎之类这些平台发布?有以下几个原因:

    \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

    我发现谷歌对独立的博客还是很友好的,我有好几篇流水账通过某个关键字可以排在首页,而且有几篇我随手记问题解决方案帮助过很多人。

    \n

    证明我来过

    Web 和域名都是伟大的发明,就像我前边说的,微信未来有一天会死去,但 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
    1. 公民信息安全
    2. \n
    3. 用户财产安全
    4. \n
    5. 在需求列表中
    6. \n
    \n"},{"title":"昨天是我31岁生日","url":"/2023/yesterday-is-my-birthday/","content":"

    昨天是我的生日,最近也是我很长时间以来最灰暗的一段时期。

    \n

    再前一天是我来这家公司的第1000天。

    \n

    这段时间一个人在北京,昨天凌晨一点吃了片安眠药胡乱睡了几个小时。

    \n

    最近组内突然离职很多人,有出国的、有回老家躺平的。

    \n

    我去年因为不想做太多管理工作,并且希望在技术上更精进一些,所以在去年这个月份申请转岗到了现在的新组。

    \n

    但转过来后还是有一部分精力花在虚线带人上,技术上也没什么长进。

    \n

    因为上级离职,最近又转成了实现带人,但是之前的虚线组也还挂在我身上,实线人员招齐后会带一个10人团队,加上虚线组一共能有25人,是在有点应付不过来,打算跟老板聊聊把虚线分出去。

    \n

    最近业务需求压力也非常大,接需求的带宽又很小,我每天忙的脚不沾地,恨不得把自己分成几个人用,跟同事开玩笑说我快学会飞了。

    \n

    \n

    这段时间的作息是:

    \n
      \n
    • 早上七点起床,七点半出门
    • \n
    • 九点到公司赶紧写会代码
    • \n
    • 十点半后被拉着开各种会,或者处理各种线上问题
    • \n
    • 晚上八点半下班十点到家短暂休息几个小时
    • \n
    \n

    希望这段灰暗的时间赶快过去,一切都会好起来的。

    \n

    此刻心情非常down,花几分钟时间记录一下,也算有个释放的口子,自己诉一诉苦。

    \n

    想到基督山伯爵里的一句话(虽然我还没看过全书):

    \n
    \n

    人类的一切智慧是包含在这四个字里面的:「等待」和「希望」

    \n
    \n"},{"title":"werkzeug.middleware.proxy_fix.ProxyFix 问题复盘","url":"/2020/werkzeug-middleware-proxy-fix-ProxyFix-review/","content":"

    werkzeug.middleware.proxy_fix.ProxyFix 问题复盘

    \"\"

    \n

    背景

    很多人知道我在运营着一个 SaaS 站点:https://bossku.cn/,用来给中小商家提供进销存服务,对于没有特殊需求的商户来说基本是免费使用的,目前注册商家 5000+,平稳运行 5 年多了。为了降低成本,中间做了好几次迁移,最早是在阿里云,后来迁移到了新浪的SAE,后边因为腾讯云有活动,又迁移到了腾讯云。

    \n

    这段时间由于疫情的原因在家办公,这周末时间比较充裕所以就看了看 Sentry 日志准备改几个 bug,改 bug 花了 15 分钟,踩坑踩了大半天,接下来我把这次遇到的坑进行一下复盘。

    \n

    这个项目是刚毕业没多久开始写的,那个时候也不懂什么自动化运维的东西,每次改完代码都是手动把新代码更新到服务器上(项目用 Python 写的,所以线上服务器 git pull 一下再重启一下 gunicorn 就可以了)。

    \n

    迁移到 SAE 后就完全不用考虑运维的事情了,项目目录下写好描述文件,把代码推到指定地址上就可以完成一次发布了(甚至连负载均衡、HA 这些都不用考虑,很省心)。用了两年 SAE 感觉成本还是有些高,毕竟这个项目并没有太多收入,正好又看到腾讯云的活动,算了一下如果迁移到腾讯云可以实现收支平衡甚至能有些小盈,于是大概两年前把项目迁移到了现在在用的腾讯云上,那个时候已经对自动化和容器化有意识了,在调研一些方案后,最后选择了把项目进行容器化,借助 DaoCloud 实现持续发布。

    \n

    这个方案的实现流程很简单:

    \n
      \n
    1. 首先在我的腾讯云主机上安装 DaoCloud 的监控插件。
    2. \n
    3. 在 DaoCloud 上 hook 一个 git 项目,每当项目有更新后会根据 Dockerfile 的描述进行项目打包生成镜像。
    4. \n
    5. 然后在页面上进行配置,镜像生成完成后自动在指定机器上进行部署。
    6. \n
    \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
    Traceback (most recent call last):
    File "/usr/local/lib/python2.7/site-packages/gunicorn/arbiter.py", line 583, in spawn_worker
    worker.init_process()
    File "/usr/local/lib/python2.7/site-packages/gunicorn/workers/ggevent.py", line 203, in init_process
    super(GeventWorker, self).init_process()
    File "/usr/local/lib/python2.7/site-packages/gunicorn/workers/base.py", line 129, in init_process
    self.load_wsgi()
    File "/usr/local/lib/python2.7/site-packages/gunicorn/workers/base.py", line 138, in load_wsgi
    self.wsgi = self.app.wsgi()
    File "/usr/local/lib/python2.7/site-packages/gunicorn/app/base.py", line 67, in wsgi
    self.callable = self.load()
    File "/usr/local/lib/python2.7/site-packages/gunicorn/app/wsgiapp.py", line 52, in load
    return self.load_wsgiapp()
    File "/usr/local/lib/python2.7/site-packages/gunicorn/app/wsgiapp.py", line 41, in load_wsgiapp
    return util.import_app(self.app_uri)
    File "/usr/local/lib/python2.7/site-packages/gunicorn/util.py", line 350, in import_app
    __import__(module)
    File "/usr/local/lib/python2.7/site-packages/gevent/builtins.py", line 96, in __import__
    result = _import(*args, **kwargs)
    File "/code/wsgi.py", line 5, in <module>
    from werkzeug.contrib.fixers import ProxyFix
    File "/usr/local/lib/python2.7/site-packages/gevent/builtins.py", line 96, in __import__
    result = _import(*args, **kwargs)
    ImportError: No module named contrib.fixers
    \n

    其实这个原因写的很清楚了,/code/wsgi.py 文件的第 5 行包导入失败。因为在处理前期问题时花费了很长时间,已经没有太多耐心了,而且在我本地直接启动是没有问题的,所以没有太关心项目代码的报错,只注意到了最后的:

    \n
    ImportError: No module named contrib.fixers
    \n

    解决

    Google 后发现遇到这个问题的人很少,而且根据几个有限的回答进行修改尝试后也都无济于事,比如更新 pip 和 setuptools 等。

    \n

    于是我就开始自己找问题。最开始怀疑的是 Python 源有问题,我一直使用的是阿里的源:

    \n
    RUN pip install -r requirements.txt -i http://mirrors.aliyun.com/pypi/simple/ --trusted-host mirrors.aliyun.com
    \n

    我尝试换成豆瓣源和清华源都不行,再后来我怀疑是 DaoCloud 打的包有问题,可是自己在本机打包还是有这个问题,再往后我又尝试修改镜像的 Python 的版本,因为这个项目用的 Python2,最新的 Python 版本是 2.7.17,我尝试降到 2.6、2.7.16、2.7.15等都不起作用,我甚至把上午写的代码进行了回滚,这个问题依然存在,这个时候心态有些爆炸了,于是准备探究下 werkzeug.contrib.fixers 这个包究竟是做什么的,于是在 Google 上搜索了这个包名,就在这时我看到一句话使我眼前一亮:

    \n

    \"\"

    \n

    点进去后:

    \n

    \"\"

    \n

    这个 ProxyFix 类已经在 werkzeug 1.0 版本中移除了,通过查看我的 requirement.txt 的文件:

    \n
    celery==3.1.24
    enum34==1.1.6
    Flask==0.12.2
    Flask-Caching==1.3.1
    Flask-DebugToolbar==0.10.0
    Flask-WTF==0.14.2
    Flask-Login==0.4.0
    Flask-SQLAlchemy==2.1
    Flask-Script==2.0.5
    Flask-Migrate==2.0.0
    gunicorn==19.9.0
    gevent==1.3.7
    ipython==5.1.0
    MySQL-python==1.2.5
    redis==2.10.5
    raven==6.3.0
    msgpack-python==0.4.8
    captcha==0.2.1
    yunpian-python-sdk==1.0.0
    flask-cors==3.0.7
    \n

    可以看到我并没有指定要安装 Werkzeug 的版本,Werkzeug 是一个 WSGI 工具包。熟悉 Flask 的同学都知道,Flask 依赖了 Werkzeug,大部分情况下都只需要安装 Flask 就可以直接使用 Werkzeug 这个工具包了。

    \n

    查看 Flask 的源码,setupy.py 部分内容如下:

    \n
    setup(
    name='Flask',
    version=version,
    url='http://github.com/pallets/flask/',
    license='BSD',
    author='Armin Ronacher',
    author_email='armin.ronacher@active-4.com',
    description='A microframework based on Werkzeug, Jinja2 '
    'and good intentions',
    ……
    install_requires=[
    'Werkzeug>=0.7',
    'Jinja2>=2.4',
    'itsdangerous>=0.21',
    'click>=2.0',
    ],
    \n

    可以看到 Flask 声明了自己需要依赖 0.7 以上的 Werkzeug,这个时候答案已经浮出水面了。

    \n

    为什么我本地可以启动?

    因为我本地的依赖包是很久之前安装的,所以我本地的 Werkzeug 版本是 0.14.1 的:

    \n
    ➜ pip freeze | grep Werk
    Werkzeug==0.14.1
    \n

    为什么 DaoCloud 之前一直没问题?

    因为 DaoCloud 打包时有缓存层,从第一次构建完后,pip install 那一层就被缓存了下来,所以后边的都是用的缓存,安装的包也都是老版本,但是我昨天把 DaoCloud 上原有项目进行了删除、创建了一个新项目,之前的那些缓存层也就失效了,重新 pip install 时自然会去安装最新版本的 Werkzeug,这时候就安装了 1.0 以上版本,所以我在代码里引用的 ProxyFix 就找不到了。

    \n

    为什么我要用 ProxyFix?

    我在部署时采用了 Nginx 作为反向代理,这时候需要重写一些 HTTP 头来让应用正常工作,如:

    \n
    server {
    listen 80;

    server_name _;

    access_log /var/log/nginx/access.log;
    error_log /var/log/nginx/error.log;

    location / {
    proxy_pass http://127.0.0.1:8000/;
    proxy_redirect off;

    proxy_set_header Host $host;
    proxy_set_header X-Real-IP $remote_addr;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    }
    }
    \n

    Nginx 添加了一些请求头来辅助应用获取真实的请求来源,这个时候需要用到 ProxyFix 处理一下这些请求头来让 WSGi 去正确处理这个请求,否则 WSGI 就可能认为这个请求来自服务器而不是真实的客户端。

    \n

    所以我在程序入口前加入了:

    \n
    from werkzeug.contrib.fixers import ProxyFix

    from bossku.app import create_app

    app = create_app()
    app.wsgi_app = ProxyFix(app.wsgi_app)
    \n

    修复

    修复这个问题有两种做法:

    \n
      \n
    1. 根据官方文档修改源码:从 werkzeug.middleware.proxy_fix 导入 ProxyFix 类。
    2. \n
    3. 手动指定 Werkzeug 的版本号。
    4. \n
    \n

    我选择了第二种方式,原因是我不清楚新版本的 Werkzeug 还会有哪些不兼容问题,于是我在 requirements.txt 中安装 Flask 前加入了一行 werkzeug==0.14.1,重新在本地 Docker build 并启动,一切正常,问题解决了。

    \n

    总结

    回顾整个问题的解决过程,还是因为自己太浮躁导致的,只想着快速把问题解决,而没有踏下心来看看每一行报错提示,更没有想着去官方文档中看看这个类库有没有变动,其实一开始也压根没想到是因为依赖的类库做了不兼容更新导致的。

    \n

    通过解决这个问题,还让我知道了为什么很多地方推荐 requirements.txt 这个文件要通过在本地执行 pip freeze > requirements.txt 来生成(我通常也都是这样做的),这样生成的描述文件中依赖所依赖的那些包的版本号也会固定下来。但是在这个项目中我为了追求精简和美观,采取了手动维护这个文件:每新增一个依赖,就手动在这个文件内新增一行,这就导致了依赖所依赖的那些包的版本可能在一台新机器上重新安装时发生变动。

    \n"},{"title":"为什么要写Blog?","url":"/2015/%E4%B8%BA%E4%BB%80%E4%B9%88%E8%A6%81%E5%86%99Blog%EF%BC%9F/","content":"

    原文地址:http://www.ruanyifeng.com/blog/2006/12/why_i_keep_blogging.html

    \n

    到今年12月为止,我写Blog已经满3年了,一共写了接近600篇,平均每2天写一篇。今后应该还会继续写下去。

    \n

    3年前,我开始写的时候,并没有想过自己会坚持这么久。3年中,也遇见过几次有人问我”为什么要写Blog?”

    \n

    是啊,为什么要写Blog?毕竟这里没有人支付稿酬,也看不出有任何明显的物质性收益。

    \n\n

    Darren Rowse在他的Blog上,讲到了7个理由,我觉得说得很好。

    \n

    1. 学会写作Blog的技巧(teach you the skills of blogging)

    \n

    没有人天生会写Blog,我刚开始的时候也不知道该怎么写。但是,经过不断的尝试,现在我知道怎么可以写出受欢迎的文章。

    \n

    2. 熟悉Blog工具(familiarize you with the tools of blogging)

    \n

    写作Blog,可以选择自己搭建平台,也可以选择网上的免费Blog提供商。我曾经试用过不少Blog软件,最后才选择了现在的Moveable Type,这本身也是一个学习过程。

    \n

    3. 便于更好地安排时间(help you work out how much time you have)

    \n

    写作Blog花费的时间,要比大家想象的多,甚至也比我自己想象的多。但是,另一方面,每天我们又有很多时间被无谓地浪费了。坚持写作Blog的过程,也是进行更好的时间安排的过程。

    \n

    4. 便于你了解自己是否可以长期做一件喜欢的事情(help you work out if you can sustain blogging for the long term)

    \n

    很多人都有自己的爱好,但是只有当你享受到这种爱好时,你才会长期坚持下去。写作Blog可以帮助你体验到这种感觉。

    \n

    5. 便于体验Blog文化(give you a taste of blogging ‘culture’)

    \n

    Blog的世界有一种无形的礼仪、风格和用语。熟悉它们,会使你更好地表达自己和理解他人。

    \n

    6. 便于你形成和了解自我(help you define a niche)

    \n

    长期写作Blog最大的好处之一就是,写着写着,你的自我会变得越来越清晰。你最终会明白自己是一个什么样的人,以及自己热爱的又是什么东西。

    \n

    7. 帮助你找到读者(help you find a readership)

    \n

    与他人交流是生命最大的乐趣之一。写作Blog可以帮助我们更好地做到这一点。

    \n

    如果你觉得你想说的东西不适宜让他人知道,你可以在自己的电脑里写,不用放到网上。这样除了上面第7点以外,其他6点的好处也还是适用的。
    总之,正是因为以上7个理由,所以我强烈建议,每一个朋友都应该有一个自己的Blog,尝试将自己的生活和想法记录下来,留下一些印记。

    \n","categories":["转载"],"tags":["阮一峰"]},{"title":"优化 blog 速度","url":"/2017/%E4%BC%98%E5%8C%96blog%E9%80%9F%E5%BA%A6/","content":"

    之前已经做过对 Blog 静态资源的优化,但是都没有进行记录,今天又做了一个比较实用的优化,赶紧记一下。

    \n

    我打开 Chrome 的调试器进入站点时,看到 Network 里有一条访问 fonts.gstatic.com,这个请求应该是访问的 谷歌 CDN,但是国内的访问速度是非常不友好的,由于我平时都是开着 Surge 所以没什么感觉。

    \n

    这个资源是在由主题渲染成静态站点时插入进来的,因为我用的是自己修改的 Next 模板,所以在 themes/next 目录下查找带 fonts.gstatic.com 关键字的文件竟然没有找到,很是费解。后来我又看那条请求,发现它的 refererfonts.googleapis.com,所以我又尝试用这个关键字进行查找,最后在 themes/next/layout/_partials/head/external-fonts.swig 找到了它。

    \n
    {% if font_families !== '' %}
    {% set font_families += '&subset=latin,latin-ext' %}
    {% set font_host = font_config.host | default('//fonts.googleapis.com') %}
    <link href="{{ font_host }}/css?family={{ font_families }}" rel="stylesheet" type="text/css">
    {% endif %}
    \n

    然后我在网上找了找 fonts.googleapis.com 国内镜像,有两个用的比较多的:360网站卫士 和 中科大。但是一些地方写到 360网站卫士 提供的源不支持 HTTPS,虽然我的博客现在并不是 HTTPS 的,但保不齐以后我要改呢。所以我选择使用中科大镜像。只需用 fonts.lug.ustc.edu.cn 替代之即可。然后再 hexo g 重新生成下站点就可以了。

    \n

    下边是几个常用的替代镜像:

    \n
      \n
    1. ajax.googleapis.com => ajax.lug.ustc.edu.cn
    2. \n
    3. fonts.googleapis.com => fonts.lug.ustc.edu.cn
    4. \n
    5. themes.googleusercontent.com => google-themes.lug.ustc.edu.cn
    6. \n
    \n
    \n

    updateAt: 2017-06-20

    然鹅,在我用了一段时间后,发现中科大的源速度我也不满意,最终将那个 css 文件包括 css 文件里边用到的 ttf 文件都下载到了本地,使用本地路径来路由,这样的话我部署到七牛后的速度比之前快了很多。

    \n

    \"\"

    \n

    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 地址为本地地址:

    \n

    \"\"

    \n

    最后将之前修改的 themes/next/layout/_partials/head/external-fonts.swig 文件中的地址改为:/css/fonts.css

    \n"},{"title":"使用 KM 处理 HHKB 方向键","url":"/2017/%E4%BD%BF%E7%94%A8-KM-%E5%A4%84%E7%90%86-HHKB-%E6%96%B9%E5%90%91%E9%94%AE/","content":"

    对于上了 HHKB 这条贼船的人来说,刚开始使用起来最大的别扭可能就是没有方向键的问题了。

    \n

    最早的我使用 Karabiner 来解决,里边有一些内置的组合可以替代方向键,我用 control + hjkl(同vi) 替代四个方向键,因为 HHKB 的 control 在 caps lock 的位置,所以使用起来还是很舒服的,But 当系统升级到 macOS Sierra 后,Karabiner 就不能工作了,作者也在官网中写了:

    \n
    \n

    Karabiner does not work on macOS Sierra at the moment.

    \n
    \n

    同时也给出了替代方案,使用 Karabiner-Elements,但是新版的 Karabiner 并不支持这样的组合,所以我就又走上了寻找解决方向键之路。

    \n

    后来找到了 Keyboard Maestro(简称 KM) 这个神器,这个软功能非常多,不过我只用了里边的设置组合键的功能,我自定义了 5 个组合,用来解决 HHKB 中的不方便的方向键问题。

    \n

    分别是 control + hjkl 来操作方向和 control + delete 来反向删除(也就是删除光标后边的内容),但是用起来有些问题:不能连击(比如按住 control + h 光标不可以一直前移,需要手动敲击多次),然鹅就在我将就用了小一年后,今天尝试将触发方式 is pressed 改成了 is down,成功解决了不能连击的问题,所以 HHKB 方向键 的问题现在可以说是完美解决了。

    \n

    不过我现在并没有将 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 操作系统上面的进程。它可以同时启动,关闭多个进程,使用起来特别的方便。

    \n

    组成部分

    supervisor 主要由两部分组成:

    \n
      \n
    1. supervisord(server 部分):主要负责管理子进程,响应客户端命令以及日志的输出等;
    2. \n
    3. supervisorctl(client 部分):命令行客户端,用户可以通过它与不同的 supervisord 进程联系,获取子进程的状态等。
    4. \n
    \n

    安装 Supervisord:

    在有 Python 环境的 Linux 机器上(基本上所有 Linux 发行版都有),直接通过 sudo easy_install supervisor 即可完成安装。

    \n

    然后初始化配置文件:

    \n
    mkdir /etc/supervisor
    echo_supervisord_conf >/etc/supervisor/supervisord.conf
    \n

    所有需要管理的进程需要在上边的配置文件中进行管理,但是都放在一起并不是一个好主意,一旦管理的进程过多,就很麻烦。

    \n

    所以一般都会新建一个目录来专门放置进程的配置文件,然后通过 include 的方式来获取这些配置信息

    \n

    修改配置文件,在最下边加上:

    \n
    [include]
    files = /etc/supervisor/conf.d/*.conf
    \n

    并且新建相应目录:mkdir /etc/supervisor/conf.d

    \n

    然后可以通过 supervisord 命令启动 supervisord

    \n
    $ ps -ef | grep super
    root 2675 1 0 2017 ? 00:48:43 /usr/lib64/cmf/agent/build/env/bin/python /usr/lib64/cmf/agent/build/env/bin/supervisord
    magneto 6020 27867 0 11:13 pts/7 00:00:00 grep --color=auto super
    magneto 27388 1 0 09:50 ? 00:00:01 /usr/bin/python /usr/bin/supervisord
    \n

    可以看到 supervisord 已经被启动了, 然后进入 supervisorctl 的 shell 界面。

    \n
    $ supervisorctl
    supervisor> status
    supervisor>
    \n

    由于目前没有添加任何需要管理的进程,所以 status 没有输出任何结果,接下来我们添加一个需要管理的进程:

    \n
    cd /etc/supervisor/conf.d
    sudo vi cat.conf


    写入以下内容:


    [program:foo]
    command=/bin/cat
    \n

    然后运行以下命令更新配置并启动进程:

    \n
    $ supervisorctl reread (只更新配置文件)
    foo: available

    $ supervisorctl update (只启动有改动的进程)
    foo: added process group

    $ supervisorctl status
    foo RUNNING pid 6537, uptime 0:00:18
    \n

    来检查下 cat 进程有没有真的启动了:

    \n
    $ ps -ef | grep cat
    magneto 6537 27388 0 11:18 ? 00:00:00 /bin/cat
    magneto 6588 27867 0 11:19 pts/7 00:00:00 grep --color=auto cat
    \n

    然后杀掉这个进程号,再次检查有没有重启:

    \n
    $ kill -9 6537
    $ ps -ef | grep cat
    magneto 6736 27388 0 11:20 ? 00:00:00 /bin/cat
    magneto 6738 27867 0 11:20 pts/7 00:00:00 grep --color=auto cat
    \n

    进阶,让 supervisord 管理我们的项目进程

    vi eyes-tw


    [program:eyes-tw]
    command=java -jar /opt/eyes-tw/eyes-tw-0.0.1-SNAPSHOT.jar
    stdout_logfile=/opt/eyes-tw/stdout.log
    stderr_logfile=/opt/eyes-tw/stderr.log
    autostart=true
    autorestart=true
    startsecs=20


    # command 设置启动命令
    # stdout_logfile 设置标准输出的输出位置
    # stderr_logfile 设置标准错误的输出位置
    # autostart 是否在 supervisord 启动时启动此进程
    # autorestart 是否在程序异常退出后重启
    # startsecs=20 启动 20 秒后没有异常退出,就当作已经正常启动
    \n

    然后再次执行:

    \n
    $ supervisorctl reread
    eyes-tw: available

    $ supervisorctl update
    eyes-tw: added process group

    $ supervisorctl status
    eyes-tw RUNNING pid 6537, uptime 0:00:24
    \n

    可以看到我们需要监控的项目进程已经启动成功,其他项目按照上边的 conf 模板添加就行了。

    \n

    我把其他几个项目的配置文件写好后,执行 reread、update,完成了所有项目的启动。

    \n

    现在 supervisorctl status 如下:

    \n
    eyes-hk                          RUNNING   pid 32180, uptime 1:09:14
    eyes-sea RUNNING pid 2744, uptime 0:51:44
    eyes-tw RUNNING pid 31008, uptime 1:17:53
    eyes-usa RUNNING pid 32182, uptime 1:09:14
    \n

    命令详解

    \n
      \n
    1. supervisord: 初始启动Supervisord,启动、管理配置中设置的进程;
    2. \n
    3. supervisorctl stop(start, restart) xxx,停止(启动,重启)某一个进程(xxx);
    4. \n
    5. supervisorctl reread: 只载入最新的配置文件, 并不重启任何进程;
    6. \n
    7. supervisorctl reload: 载入最新的配置文件,停止原来的所有进程并按新的配置启动管理所有进程;
    8. \n
    9. supervisorctl update: 根据最新的配置文件,启动新配置或有改动的进程,配置没有改动的进程不会受影响而重启;
    10. \n
    \n"},{"title":"使用 keepalived 实现虚拟IP + IP漂移","url":"/2018/%E4%BD%BF%E7%94%A8-keepalived-%E5%AE%9E%E7%8E%B0%E8%99%9A%E6%8B%9FIP-IP%E6%BC%82%E7%A7%BB/","content":"

    这段时间在调研 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,保证路由器的高可用,继续提供服务,从而保证了高可用性。

    \n

    先来准备两台机器,IP地址如下:

    lc1: 172.24.8.101
    lc7: 172.24.8.107
    \n

    我们现在要实现添加一个虚IP:172.24.8.150,当 lc1 机器正常时,172.24.8.150 指向 lc1,当 lc1 出现故障时指向 lc7

    \n

    此时通过 ping 可以看到 172.24.8.150 是无法 ping 通的。

    \n

    在这两台机器上分别安装 Keepalived

    $ sudo yum install -y keepalived
    \n

    配置 Keepalived

    lc1 的配置

    \n
    $ cat keepalived.conf
    vrrp_instance VI_1 {
    state MASTER
    interface enp7s0f0
    virtual_router_id 51
    priority 101
    advert_int 1
    authentication {
    auth_type PASS
    auth_pass 123456
    }
    virtual_ipaddress {
    172.24.8.150
    }
    }
    \n

    lc7 的配置

    \n
    vrrp_instance VI_1 {
    state MASTER
    interface enp7s0f0
    virtual_router_id 51
    priority 100
    advert_int 1
    authentication {
    auth_type PASS
    auth_pass 123456
    }
    virtual_ipaddress {
    172.24.8.150
    }
    }
    \n

    启动 lc1 和 lc7 上的 Keepalived 服务

    sudo systemctl restart keepalived.service
    \n

    将 Keepalived 加入开机启动项

    sudo systemctl enable keepalived.service
    \n

    测试

    通过 ping 172.24.8.150 发现已经可以通了。

    \n

    查看 lc1 的 IP信息

    $ ip addr show enp7s0f0
    2: enp7s0f0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc mq state UP qlen 1000
    link/ether 6c:92:bf:0d:09:47 brd ff:ff:ff:ff:ff:ff
    inet 172.24.8.101/24 brd 172.24.8.255 scope global enp7s0f0
    valid_lft forever preferred_lft forever
    inet 172.24.8.150/32 scope global enp7s0f0
    valid_lft forever preferred_lft forever
    inet6 fe80::6e92:bfff:fe0d:947/64 scope link
    valid_lft forever preferred_lft forever
    \n

    其中可以看到 inet 172.24.8.150/32 scope global enp7s0f0,说明现在 lc1 是作为虚拟IP的 master 来运行的。

    \n

    查看 lc7 的 IP信息

    $ ip addr show enp7s0f0
    2: enp7s0f0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc mq state UP qlen 1000
    link/ether 6c:92:bf:0d:21:49 brd ff:ff:ff:ff:ff:ff
    inet 172.24.8.107/24 brd 172.24.8.255 scope global enp7s0f0
    valid_lft forever preferred_lft forever
    inet6 fe80::6e92:bfff:fe0d:2149/64 scope link
    valid_lft forever preferred_lft forever
    \n

    此时 lc7 中没有虚拟IP 的信息。

    \n

    验证 Failover

    我们手动停止 lc1 上的 Keepalived 服务:

    sudo systemctl stop keepalived.service
    \n

    此时 lc1 的 IP信息为:

    $ ip addr show enp7s0f0
    2: enp7s0f0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc mq state UP qlen 1000
    link/ether 6c:92:bf:0d:09:47 brd ff:ff:ff:ff:ff:ff
    inet 172.24.8.101/24 brd 172.24.8.255 scope global enp7s0f0
    valid_lft forever preferred_lft forever
    inet6 fe80::6e92:bfff:fe0d:947/64 scope link
    valid_lft forever preferred_lft forever
    \n

    可以看到 lc1 已经不在有 虚拟IP 的信息了。

    \n

    查看 lc7 的 IP信息:

    ip addr show enp7s0f0
    2: enp7s0f0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc mq state UP qlen 1000
    link/ether 6c:92:bf:0d:21:49 brd ff:ff:ff:ff:ff:ff
    inet 172.24.8.107/24 brd 172.24.8.255 scope global enp7s0f0
    valid_lft forever preferred_lft forever
    inet 172.24.8.150/32 scope global enp7s0f0
    valid_lft forever preferred_lft forever
    inet6 fe80::6e92:bfff:fe0d:2149/64 scope link
    valid_lft forever preferred_lft forever
    \n

    可以看到 lc7 的 IP信息中 已经有虚拟IP 172.24.8.150 的信息了。

    \n

    此时如果再把 lc1 上的 Keepalived 启动,可以看到 虚拟IP 又重新绑定到了 lc1 上。

    \n"},{"title":"使用 spring-boot-starter-security 集成 CAS 遇到的问题","url":"/2017/%E4%BD%BF%E7%94%A8-spring-boot-starter-security-%E9%9B%86%E6%88%90-CAS-%E9%81%87%E5%88%B0%E7%9A%84%E9%97%AE%E9%A2%98/","content":"

    根据这篇教程:http://blog.csdn.net/cl_andywin/article/details/53998986 使用 spring-boot-starter-security 集成了 CAS 单点登录的功能,但是发现一个问题是,单点退出一直不成功。

    \n

    然后我就拿出各种 debug 手段,最后发现问题是服务器在收到 POST 请求时需要 CSRF 验证(CAS 在单点退出时是发送的 POST 请求),原因是使用了 spring-security 导致的,它默认是开启 CSRF 的,所以解决办法就是关掉这个特性。

    \n

    在重写的 configure() 方法最后加上 http.csrf().disable(); 就行了。

    \n"},{"title":"使用适配器模式和__slots__优化代码小记","url":"/2016/%E4%BD%BF%E7%94%A8%E9%80%82%E9%85%8D%E5%99%A8%E5%92%8C-slots-%E5%B0%8F%E8%AE%B0/","content":"

    今天看了一篇关于设计模式方面的资料,再加上前几天看的 __slots__ 的用法,想起项目中的更新用户资料相关代码可以用上边的知识(适配器模式, __slots____setattr__)优化一下:

    修改前:

    class UserModel(object):

    def __init__(self, user_id=None):
    self.user_id = user_id
    self.query = Query(_User)

    ...

    \tdef update_profile(self, avatar=None, nickname=None, birth=None,
    \t mind=None, height=None, weight=None, sexuality=None, emotion=None,
    \t haunt=None, been=None, company=None, position=None, graduated=None,
    \t industry=None, salary=None, hometown=None):
    \t u = self.get_by_id()
    \t user_profile = UserProflieModel.get_or_create_user_profile(u)
    \t
    \t if avatar is not None:
    \t avatar_file = Query(_File).get(avatar)
    \t u.set('avatar', avatar_file)
    \t if nickname is not None:
    \t u.set('nickname', nickname)
    \t if birth is not None:
    \t birthday = datetime.fromtimestamp(birth)
    \t u.set('birthday', birthday)
    \t if mind is not None:
    \t if len(mind) > 300:
    \t raise LeanCloudError(20504, errmsg.ERRMSG[20504])
    \t u.set('mind', mind)
    \t if height is not None:
    \t user_profile.set('height', height)
    \t if weight is not None:
    \t user_profile.set('weight', weight)
    \t if sexuality is not None:
    \t user_profile.set('sexuality', sexuality)
    \t if emotion is not None:
    \t user_profile.set('emotion', emotion)
    \t if haunt is not None:
    \t user_profile.set('haunt', haunt)
    \t if been is not None:
    \t user_profile.set('been', been)
    \t if company is not None:
    \t user_profile.set('company', company)
    \t if position is not None:
    \t user_profile.set('position', position)
    \t if graduated is not None:
    \t user_profile.set('graduated', graduated)
    \t if industry is not None:
    \t user_profile.set('industry', industry)
    \t if salary is not None:
    \t user_profile.set('salary', salary)
    \t if hometown is not None:
    \t region = Query(Region).equal_to('adcode', hometown).first()
    \t user_profile.set('hometown', region)
    \t
    \t try:
    \t u.save()
    \t user_profile.save()
    \t except LeanCloudError as e:
    \t logging.error(e)
    \t return False
    \t return True
    \n

    修改后:

    class Account(object):
    __slots__ = ('user_id', 'user', 'user_profile', 'account_money')

    def __init__(self, user_id):
    self.user_id = user_id
    self.user = Query(_User).include('avatar.url', 'password').get(user_id)
    self.user_profile = UserProflieModel.get_or_create_user_profile(self.user)
    self.account_money = None

    def __setattr__(self, key, value):
    if key in self.__slots__:
    object.__setattr__(self, key, value)
    elif key in ('height', 'weight', 'sexuality', 'emotion', 'haunt', 'been', 'company',
    'position', 'graduated', 'industry', 'salary', 'hometown') and value is not None:
    if key == 'hometown':
    value = Query(Region).equal_to('adcode', value).first()
    self.user_profile.set(key, value)
    elif key in ('avatar', 'nickname', 'birth', 'mind') and value is not None:
    if key == 'avatar':
    value = Query(_File).get(value)
    elif key == 'birth':
    value = datetime.fromtimestamp(value/1000)
    elif key == 'mind':
    if len(value) > 300:
    raise LeanCloudError(20504, errmsg.ERRMSG[20504])
    self.user.set(key, value)

    def save(self):
    self.user.save()
    self.user_profile.save()
    # self.account_money.save()

    class UserModel(object):

    def __init__(self, user_id=None):
    self.user_id = user_id
    self.query = Query(_User)

    ...

    def update_profile(self, avatar=None, nickname=None, birth=None,
    mind=None, height=None, weight=None, sexuality=None, emotion=None,
    haunt=None, been=None, company=None, position=None, graduated=None,
    industry=None, salary=None, hometown=None):
    account = Account(self.user_id)
    account.avatar = avatar
    account.nickname = nickname
    account.birth = birth
    account.mind = mind
    account.height = height
    account.weight = weight
    account.sexuality = sexuality
    account.emotion = emotion
    account.haunt = haunt
    account.been = been
    account.company = company
    account.position = position
    account.graduated = graduated
    account.industry = industry
    account.salary = salary
    account.hometown = hometown

    try:
    account.save()
    except LeanCloudError as e:
    logging.error(e)
    return False
    return True
    \n

    感觉自己萌萌哒 (。◕∀◕。)

    ","categories":["Code"],"tags":["Python","魔镜"]},{"title":"关于情景的一点点思考","url":"/2017/%E5%85%B3%E4%BA%8E%E6%83%85%E6%99%AF%E7%9A%84%E4%B8%80%E7%82%B9%E7%82%B9%E6%80%9D%E8%80%83/","content":"

    这段时间的工作内容,让我更加体会到「情景」的重要性。

    \n

    想把事情做好,就要有一个已经存在的情景设定,空穴来潮地去做一定做不完美,或者做着做着会失去动力。
    这段时间学习了很多图数据库知识,接触了一下Titan,深入学习了 Neo4j 。用 Hadoop/Spark 写了一些大数据处理工具。

    \n

    因为有上亿级的数据需要处理,所以不得不使用分布式计算引擎;为了将上亿级的数据导入到图数据库,不得不使用 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

    \n

    这个 service 参数的作用其实可以认为是一个回跳的 url,将来通过认证后,还要重定向到系统A,所以其实用 redirect 可能更合适一些,但是在这里还有一个作用就是注册服务,简单来说注册服务为的是让我们的认证中心能够知道有哪些系统在我们这里完成过登录,其中一个重要目的是为了完成单点退出的功能,单点退出的一会我再来介绍。

    \n

    接下来浏览器会用 www.sso.com/login?service=www.a.com/pageA 访问认证中心,认证中心一看没登录过,就会展示一个登录框让用户去登录,登录成功以后,认证中心要做几件重要的事情:

    \n
      \n
    1. 建立一个 session
    2. \n
    3. 创建一个 ticket (可以认为是个随机字符串)
    4. \n
    5. 重定向到系统A,同时把 ticket 放在 url 中: www.a.com/pageA?ticket=T123 与此同时之前建立 session 对应的 cookie 也会发送给浏览器,比如: Set cookie : ssoid=1234, sso.com
    6. \n
    \n

    到这里会产生一个疑惑,为什么认证中心要写一个 cookie,其他系统由于跨域的限制根本读不到它啊。

    \n

    对于这个问题的回答是, sso.com 产生的 cookie 不是给其他系统用的(至于是给谁用的一会会说明),注意那个 ticket,这个东西是个重要标识,系统拿到以后需要再次向认证中心验证。所以 ticket 才是系统们要用到的东西。

    \n

    \"\"

    \n

    系统A拿着这个 ticket,去问下认证中心:这是您签发的 ticket 吗,认证中心确认无误后,系统A就可以认为用户在认证中心登录过了。这时候系统A应该为这个用户建立 session 然后返回 pageA 的资源。也就是说,系统A也需要给浏览器发一个属于自己的 cookie:Set cookie : sessionid=xxxx, a.com。这时候浏览器实际上有两个 cookie,一个是系统A发的,一个是认证中心发的。

    \n

    当用户再次访问系统A的另一个需要登录的页面时,因为系统A已经在浏览器中放入了自己的cookie,就知道它登录过了,不需要再次到认证中心去了。

    \n

    \"\"

    \n

    接下来看看,当用户访问系统A时已经通过认证中心登录了,再访问系统B www.b.com/pageB 时是什么样的情况。

    \n

    其实和首次访问 www.a.com/pageA 非常类似,唯一不同就是不需要用户输入用户名密码来登录了,因为浏览器已经有了认证中心的 cookie,直接发送给 www.sso.com 就可以了。这里解释了我上边提到的认证中心写入浏览器 cookie 的用途。

    \n

    \"\"

    \n

    同样,认证中心会返回 ticket,系统B需要做验证。

    \n

    \"\"

    \n

    整个流程的本质是一个认证中心的 cookie,加上多个子系统的 cookie 而已。

    \n

    下边来说说单点退出的原理。

    \n

    单点退出的作用是用户在一个地方退出,所有系统都要进行退出。这怎么来实现呢。还记得我前边提到的注册服务吗?没错,就是使用之前登录时给认证中心传的 service 参数,认证中心记录下来都有哪些系统进行过登录,当用户访问认证中心的 /logout 需要退出的时候,认证中心需要把自己的会话和 cookie 干掉,然后给之前注册过那些服务的地址发送退出登录的请求,默认是对根路径发一个 POST 请求,Body 中携带一些字段,比如比如之前登录时用到的ticket,这时候各个子系统根据传过来的这个 ticket 来将对应的用户 session 干掉即可。

    \n

    所以用户在系统A点击退出登录后,系统A取消本地会话然后重定向到认证中心的退出登录地址,剩下的交给认证中心来处理就好了。这里也可以传一个回跳地址参数,当认证中心完成退出后,可以再跳会到设置的地址。

    \n
    \n

    我觉得用户在一个系统中退出登录时,系统此时结不结束会话其实都可以,因为最终还是要被认证中心调用一次退出。

    \n
    \n

    说一个我做单点退出时遇到的坑,由于我把 CAS Server 部署在了机房中的一台设备上,然后在我本地启了一个 WEB 服务,这个时候登录填的 service127.0.0.1:8081/xxx 登录完之后的重定向是没有问题的,但是当我访问 CAS Logout 页面后,再访问我的系统,发现并没有退出登录,也没有访问退出登录的记录。原因是我注册服务时的 127.0.0.1 这个url认证中心根本访问不到。

    \n

    后来我在本地起了一个 CAS Server 再次验证后没有问题。

    \n

    还有人问道那个 ticket 如果被其他人截获了,岂不是就可以冒充我来登录了?并不会。

    \n

    首先来说,默认情况下,CAS 要求子系统和它之间的通讯为 https ,再有就是这个 ticket 只有一次有效性,验证一次后即失效,而且有效期还很短,默认我记得只有5秒。最重要的是即便这个 ticket 被其他人获取了也没啥用,验证这个 ticket 时,还需要带上申请这个 ticket 时的 url 信息,而且认证中心鉴定 ticket 为真后也只是返回用户的用户名、认证时间等最基本的信息,由于子系统没有拿到这些信息,所以对于子系统来说,你还是没有登录的。

    \n

    最后敬上官方的 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 是 Netflix 开源的一款提供服务注册和发现的产品。

    Eureka 由两个组件组成: Eureka 服务器Eureka 客户端。Eureka 服务器 用作服务注册服务器。Eureka 客户端 是一个 Java 客户端,用来简化与服务器的交互、作为轮询负载均衡器,并提供服务的故障切换支持。

    \n

    官方对自己的定义是:

    \n

    Eureka 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
    \n

    通俗点讲什么是服务注册发现?

    服务注册与发现就像是在一个聊天室,每个用户来的时候去服务器上注册,这样你的好友们就能看到你,你同时也将获取好友的上线列表。在微服务中,服务就相当于聊天室的用户,而服务注册中心就像聊天室服务器一样。

    \n

    Eureka特性

      \n
    1. Eureka Server 具有服务定位/发现的能力,在各个微服务启动时,会通过 Eureka Client 向Eureka Server 进行注册自己的信息(例如网络信息)。
    2. \n
    3. 一般情况下,微服务启动后,Eureka Client 会周期性向 Eureka Server 发送心跳检测(默认周期为30秒)以注册/更新自己的信息。
    4. \n
    5. 如果 Eureka Server 在一定时间内(默认90秒)没有收到 Eureka Client 的心跳检测,就会注销掉该微服务点。
    6. \n
    7. 同时,Eureka Server 本身也是 Eureka Client,多个 Eureka Server 通过复制注册表的方法来完成服务注册表的同步从而达到集群的效果。
    8. \n
    \n

    为什么选择 Eureka

    1) 它提供了完整的 Service RegistryService Discovery 实现

    首先是提供了完整的实现,并且也经受住了 Netflix 自己的生产环境考验,相对使用起来会比较省心。

    \n

    2) 和 Spring Cloud 无缝集成

    服务端和客户端都是 Java 编写的,针对微服务场景,并且和 Netflix 的其他开源项目以及 Spring Cloud 都有着非常好的整合,具备良好的生态。

    \n

    3) Open Source

    最后一点是开源,由于代码是开源的,所以非常便于我们了解它的实现原理和排查问题。

    \n

    Eureka Server 使用介绍

    在 Spring Boot 项目的 pom.xml 中加入 spring-cloud-starter-eureka-server

    使用 Spring Cloud 需要在 pom.xml 中加入 Spring Cloud 的父级引用,让 Spring 帮我们管理依赖版本。

    \n
    <dependencies>
    <dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-eureka-server</artifactId>
    </dependency>
    </dependencies>
    \n

    在application.yml中配置

    Eureka 是一个高可用的组件,它没有后端缓存,每一个实例注册之后需要向注册中心发送心跳(因此可以在内存中完成),在默认情况下 Erureka Server 也是一个 Eureka Client,必须要指定一个 Server。

    \n
    server:
    port: 8761

    eureka:
    instance:
    hostname: localhost
    client:
    register-with-eureka: false # 不用将自己注册到Eureka
    fetch-registry: false # 不用发现Eureka中的服务
    service-url:
    default-zone: http://${eureka.instance.hostname}:${server.port}/eureka/
    \n

    添加 Application.java 启动类 添加 @EnableEurekaServer 注解

    @SpringBootApplication
    @EnableEurekaServer
    public class Application {
    public static void main(String[] args) {
    SpringApplication.run(Application.class, args);
    }
    }
    \n

    Eureka Client 使用介绍

    服务注册与服务发现都是使用 Eureka Client,所以在 Spring Boot 项目的 pom.xml 中加入

    <dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-eureka</artifactId>
    </dependency>
    \n

    在 application.yml 中加入 Eureka 的 Server 配置

    server:
    port: 8762

    spring:
    application:
    name: service-hi

    eureka:
    client:
    serviceUrl:
    defaultZone: http://localhost:8761/eureka/
    \n

    需要指明 spring.application.name,这个很重要,这在以后的服务与服务之间相互调用一般都是根据这个 name。

    \n

    启动上边两个程序后,访问 http://localhost:8761/ 可以看到下边的页面,同时看到我们的 service-hi 也注册上来了。

    \n

    \"\"

    \n

    在本地调试时出现了这样的问题,如上图所示,中间部分有一行红色大字 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

    Eureka Server 在运行期间,会统计心跳成功率在 15分钟 之内是否低于85%,如果出现低于的情况(在单机调试的时候很容易满足,实际在生产环境上通常是由于网络不稳定导致),Eureka Server 会将当前的实例注册信息保护起来,同时提示这个警告。至于为什么需要有这个自我保护机制,官方的解释是:Service 不是强一致的,所以会有部分情况下没发现新服务导致请求出错,对于Service 发现服务而言,宁可返回某服务 5分钟 之前在哪几个服务器上可用的信息,也不能因为暂时的网络故障而找不到可用的服务器,而不返回任何结果。

    \n
    \n

    其他实现方式比较

    DNS 可以算是最为原始的服务发现系统,但是在服务变更较为频繁,即服务的动态性很强的时候,DNS 记录的传播速度可能会跟不上服务的变更速度,这将导致在一定的时间窗口内无法提供正确的服务位置信息,所以这种方案只适合在比较静态的环境中使用,不适用于微服务。

    \n

    基于 ZooKeeper、Etcd 等分布式键值对存储服务来建立服务发现系统在现在看起来 也不是一种很好的方案,一方面是因为它们只能提供基本的数据存储功能,还需要在外围做大量的开发才能形成完整的服务发现方案。另一方面是因为它们都是强一致性系统,在集群发生分区时会优先保证一致性、放弃可用性,而服务发现方案更注重可用性,为了保证可用性可以选择最终一致性,这两方面原因共同导致了 ZooKeeper、Etcd 这类系统越来越远离服务发现方案的备选清单,像 SmartStack 这种依赖 ZooKeeper 的服务发现方案也逐渐发觉 ZooKeeper 成了它的薄弱环节。与 ZooKeeper、Etcd 或者依赖它们的方案不同,Eureka 是个专门为服务发现从零开始开发的项目,Eureka 以可用性为先,可以在多种故障期间保持服务发现和服务注册功能可用,虽然此时会存在一些数据错误,但是 Eureka 的设计原则是“存在少量的错误数据,总比完全不可用要好”,并且可以在故障恢复之后按最终一致性进行状态合并,清理掉错误数据。

    \n

    Eureka 有个强大的对手 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":"

    基于 Hystrix 的熔断器调研

    什么是雪崩效应

    在微服务架构中通常会有多个服务层调用,基础服务的故障可能会导致级联故障,进而造成整个系统不可用的情况,这种现象被称为服务雪崩效应。服务雪崩效应是一种因 服务提供者 的不可用导致 服务消费者 的不可用,并将不可用逐渐放大的过程。

    \n

    如果下图所示:A 作为服务提供者,B 为 A 的服务消费者,C 和 D 是 B 的服务消费者。A 不可用引起了 B 的不可用,并将不可用像滚雪球一样放大到 C 和 D 时,雪崩效应就形成了。

    \n

    \"\"

    \n

    熔断器(CircuitBreaker)

    熔断器的原理很简单,如同电力过载保护器。它可以实现快速失败,如果它在一段时间内侦测到许多类似的错误,会强迫其以后的多个调用快速失败,不再访问远程服务器,从而防止应用程序不断地尝试执行可能会失败的操作,使得应用程序继续执行而不用等待修正错误,或者浪费 CPU 时间去等到长时间的超时产生。熔断器也可以使应用程序能够诊断错误是否已经修正,如果已经修正,应用程序会再次尝试调用操作。

    \n

    熔断器模式就像是那些容易导致错误的操作的一种代理。这种代理能够记录最近调用发生错误的次数,然后决定是否允许操作继续,或者立即返回错误。熔断器开关相互转换的逻辑如下图:

    \n

    \"\"

    \n

    Hystrix 是什么

    在分布式系统,我们一定会依赖各种服务,那么这些个服务一定会出现失败的情况,Hystrix 就是这样的一个工具,它通过提供了逻辑上延时和错误容忍的解决力来协助我们完成分布式系统的交互。Hystrix 通过分离服务的调用点,阻止错误在各个系统的传播,并且提供了错误回调机制,这一系列的措施提高了系统的整体服务弹性。

    \n

    Hystrix 是干嘛的

    Hystrix 被设计用来做了下面几件事:

      \n
    1. 保护系统间的调用延时以及错误,特别是通过第三方的工具的网络调用
    2. \n
    3. 阻止错误在分布式系统之前的传播
    4. \n
    5. 快速失败和迅速恢复
    6. \n
    7. 错误回退和优雅的服务降级
    8. \n
    9. 提供近乎实时的系统监控,报警和动态操控
    10. \n
    \n

    Hystrix特性

    1.熔断器机制

    \n

    熔断器很好理解,当 Hystrix Command 请求后端服务失败数量超过一定比例(默认50%),熔断器会切换到开路状态(Open)。这时所有请求会直接失败而不会发送到后端服务。熔断器保持在开路状态一段时间后(默认5秒),自动切换到半开路状态(HALF-OPEN),这时会判断下一次请求的返回情况,如果请求成功,熔断器切回闭路状态(CLOSED),否则重新切换到开路状态(OPEN)。Hystrix 的熔断器就像我们家庭电路中的保险丝,一旦后端服务不可用,熔断器会直接切断请求链,避免发送大量无效请求影响系统吞吐量,并且熔断器有自我检测并恢复的能力。

    \n

    2.Fallback

    \n

    Fallback 相当于是降级操作。对于查询操作, 我们可以实现一个 fallback 方法,当请求后端服务出现异常的时候,可以使用 fallback 方法返回的值。fallback 方法的返回值一般是设置的默认值或者来自缓存。

    \n

    3.资源隔离

    \n

    在Hystrix中, 主要通过线程池来实现资源隔离. 通常在使用的时候我们会根据调用的远程服务划分出多个线程池. 例如调用产品服务的 Command 放入 A 线程池,调用账户服务的 Command 放入 B 线程池. 这样做的主要优点是运行环境被隔离开了。这样就算调用服务的代码存在 bug 或者由于其他原因导致自己所在线程池被耗尽时,不会对系统的其他服务造成影响。但是带来的代价就是维护多个线程池会对系统带来额外的性能开销。如果是对性能有严格要求而且确信自己调用服务的客户端代码不会出问题的话,可以使用 Hystrix 的信号模式(Semaphores)来隔离资源。

    \n

    Hystrix工作方式如下:

      \n
    • 阻止一个单独的依赖耗尽系统的所有线程,比如(tomcat)
    • \n
    • 使用快速失败代替将这个请求排队
    • \n
    • 在任何可能失败的地方提供后退机制来确保用户不会看到错误
    • \n
    • 使用隔离技术(比如: 隔板,泳道,环路切断模式)降低一个依赖的失败对整个系统的影响
    • \n
    • 优化使得系统可以近乎实时的收集,监控,报警
    • \n
    • 优化使得系统可以近乎实时的修改,并且可以近乎实时生效
    • \n
    • 保护系统不仅仅在网络层面,也包括客户端层面的依赖执行的失败
    • \n
    \n

    Hystrix 的使用

    因为 Hystrix 是一个开源的 Java 库,所以可以进行像官方示例那样直接用起来,下面我们介绍如何将 Hystrix 集成到 SpringBoot 项目中。

    \n

    在pox.xml文件中加入:

    <dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-hystrix</artifactId>
    </dependency>
    \n

    在程序入口类加上:@EnableHystrix

    在需要使用熔断器的地方标记注解即可:

    @HystrixCommand(fallbackMethod = "yyy")
    public String doSomething()
    \n

    yyy 就是在 熔断器开启时 是我们要调用的函数。

    \n"},{"title":"基于前后端分离的CAS对接方案","url":"/2017/%E5%9F%BA%E4%BA%8E%E5%89%8D%E5%90%8E%E7%AB%AF%E5%88%86%E7%A6%BB%E7%9A%84CAS%E5%AF%B9%E6%8E%A5%E6%96%B9%E6%A1%88/","content":"

    最近开发的项目,需要对接公司已有的 CAS 服务,之前的项目都是用的传统模式(后端渲染或前后端不分离)来开发的,所以后端可以很容易的实现控制跳转,完成校验,写入 Cookies 等逻辑。在传统的开发模式下,使用 session-cookie 可以保证接口安全,在没有登录的情况下访问关键数据会跳转到登录界面或者请求失败。而使用 REStful API 之后,session-cookie 存在以下 3 个问题:

    \n
      \n
    • 客户端除了浏览器,可能还包括手机端 APP,对于手机端而言,管理 cookie 是一件麻烦的事情
    • \n
    • RESTful 风格的 API 不建议使用 cookie
    • \n
    • Cookie 本身有一个缺陷,不能跨域
    • \n
    \n

    解决这个问题的方案是让前端传数据时,在 URL 参数中或者 header 中携带一个 参数,我们成这个参数为 token,后端通过这个 token 来判断用户身份,这样可以免去对 Cookies 的管理。

    \n

    顺着这个思路,我们很容易想到一种方案,就是后端维护一个 Mapkey 值为 tokenvalue 为用户信息,这样只要用户登录时生成这个 key 后,放到 Map 中,然后将 key 返回给前端就行了,但是这样做有个很严重的问题,Map 是在内存中的,如果后端服务为集群时,还需要做 key 同步,非常麻烦,当然也有人会提出可以将后端生成的 token 和对应的用户信息放在 键-值 数据库中,这样就不用考虑同步问题了,当然这样做没有什么问题,但是会额外引入基础组件(我们现在做第一版,为了快速开发,不打算引入太多的组件),而且还要保证键值数据库的高可用性。

    \n

    这里我使用了一个更加优雅的方案:JSON Web Token

    \n

    JSON Web Token(JWT)是一个非常轻巧的规范。这个规范允许我们使用 JWT 在用户和服务器之间传递安全可靠的信息。

    \n

    JWT的组成:

    一个JWT实际上就是一个字符串,它由三部分组成,头部、载荷与签名。

    \n

    载荷(Payload):

    Payload 是 JWT 存储信息的部分。Payload 也是一个 json 数据,每一个 json 的 key-value 称为一个声明。

    \n

    我们将用户信息描述成一个 json 对象。其中添加了一些其他的信息,帮助今后收到这个 JWT 的服务器理解这个 JWT 。

    \n
    {
    "iss": "Panmax JWT",
    "iat": 1441593502,
    "exp": 1441594722,
    "aud": "www.example.com",
    "sub": "jrocket@example.com",
    "user_id": 1,
    "username": "jiapan"
    }
    \n

    这里面的前五个字段都是由JWT的标准所定义的

    \n
      \n
    • iss: 该JWT的签发者
    • \n
    • sub: 该JWT所面向的用户
    • \n
    • aud: 接收该JWT的一方
    • \n
    • exp(expires): JWT 的过期时间,是一个 unxi 时间戳
    • \n
    • iat(issued at): JWT 的签发时间,是一个 unix 时间戳
    • \n
    \n

    上面这个 payload 中,user_idusername 为自定义声明。

    \n

    将上面的 json 对象进行base64编码可以得到下面的字符串。这个字符串我们将它称作JWT的Payload(载荷)。

    \n

    ewogICAgImlzcyI6ICJQYW5tYXggSldUIiwKICAgICJpYXQiOiAxNDQxNTkzNTAyLAogICAgImV4cCI6IDE0NDE1OTQ3MjIsCiAgICAiYXVkIjogInd3dy5leGFtcGxlLmNvbSIsCiAgICAic3ViIjogImpyb2NrZXRAZXhhbXBsZS5jb20iLAogICAgInVzZXJfaWQiOiAxLAogICAgInVzZXJuYW1lIjogImppYXBhbiIKfQ==

    \n
    \n

    注:Base64是一种编码,也就是说,它是可以被翻译回原来的样子来的。它并不是一种加密过程。

    \n
    \n

    头部(Header)

    WT还需要一个头部,头部用于描述关于该JWT的最基本的信息,例如其类型以及签名所用的算法等。这也可以被表示成一个JSON对象。

    \n
    {
    "typ": "JWT",
    "alg": "HS256"
    }
    \n

    在这里,我们说明了这是一个 JWT,并且我们所用的签名算法(后面会提到)是 HS256 算法。

    \n

    对它也要进行Base64编码,之后的字符串就成了 JWT 的 Header(头部)。

    \n

    ewogICJ0eXAiOiAiSldUIiwKICAiYWxnIjogIkhTMjU2Igp9

    \n

    签名(签名)

    将上面的两个编码后的字符串都用句号.连接在一起(头部在前),就形成了

    \n

    ewogICJ0eXAiOiAiSldUIiwKICAiYWxnIjogIkhTMjU2Igp9.ewogICAgImlzcyI6ICJQYW5tYXggSldUIiwKICAgICJpYXQiOiAxNDQxNTkzNTAyLAogICAgImV4cCI6IDE0NDE1OTQ3MjIsCiAgICAiYXVkIjogInd3dy5leGFtcGxlLmNvbSIsCiAgICAic3ViIjogImpyb2NrZXRAZXhhbXBsZS5jb20iLAogICAgInVzZXJfaWQiOiAxLAogICAgInVzZXJuYW1lIjogImppYXBhbiIKfQ==

    \n

    最后,我们将上面拼接完的字符串用 HS256 算法进行加密。在加密的时候,我们还需要提供一个密钥(secret)。加密之后,得到一串加密字符串,最后把这串加密字符串也是用 . 拼接在 header.payload 后面,形成完整的 JWT。

    \n

    最终生成的JWT格式如下:

    \n

    xxx.yyy.zzz

    \n

    这里签名的目的是为了保证 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

    通过你的终端并且确保Git已经安装在你的电脑上,导航到你想要添加的目录后运行下边的命令。

    \n

    初始化git仓库

    git init
    \n

    增加文件到Git索引

    git add -A
    \n

    提交增加过的文件

    git commit -m "Added my project"
    \n

    增加新的远程源(在这里是GitHub)

    git remote add git@github.com:Panmax/playground.git
    \n

    or

    \n
    git remote add https://github.com/Panmax/playground.git
    \n

    Push到GitHub

    git push -u -f origin master
    \n

    到这里有些事情需要注意,-f标记代表force,这将会自动的覆盖远程目录中所有的东西。我们只是用来覆盖GitHub自动初始化README。如果你跳过了,那么-f标记就不是必须的了。

    \n

    -u标记将远程源设置为默认,这让你以后容易的使用git pushgit pull而不用指定一个源。在这种情况下,我们总是想要的GitHub。

    \n

    总起来

    git init
    git add -A
    git commit -m 'Added my project'
    git remote add origin git@github.com:Panmax/playground.git
    git push -u -f origin master
    ","categories":["GitHub"],"tags":["GitHub"]},{"title":"对 Python 列表常见操作的理解","url":"/2017/%E5%AF%B9%20Python%20%E5%88%97%E8%A1%A8%E5%B8%B8%E8%A7%81%E6%93%8D%E4%BD%9C%E7%9A%84%E7%90%86%E8%A7%A3/","content":"

    写这篇文章的原因是前几天做了一道面试题,问题大致是这样的:

    \n
    l = [1, 2, 3, 4, 5]

    l.insert(1, 6)

    l.append(6)

    的时间复杂度分表是什么
    \n

    我理所应当的认为,Python 列表的内部实现应该是一个链表,而链表的插入和追加操作应该都是 O(1),但今天看到一篇文章原文地址,发现并不是我认为的那样。

    \n

    Python 在 C 实现中,存储数据的部分是一块连续的内存数组,不过这个数组里存放的也是指针,指向具体的元素,并且会在 结构体 中记录元素的实际个数,结构体如下。

    \n
    typedef struct {
    # 列表元素的长度
    int ob_size;
    # 真正存放列表元素容器的指针,list[0] 就是 ob_item[0]
    PyObject **ob_item;
    # 当前列表可容纳的元素大小
    Py_ssize_t allocated;
    } PyListObject;
    \n

    当追加新元素的时候,可以直接通过 ob_item[ob_size]=n 来完成,所以时间复杂度为 O(1)

    \n

    在插入元素时,操作如下,将要插入位置下方的所有元素向下移动一个位置,然后将要插入位置指向我们插入的元素即可。所以时间复杂度其实是 O(n)

    \n

    (以上都没有考虑 allocated 分配的情况)

    \n

    再来看下 pop 这个操作,pop 时只需将 ob_size 减一即可,所以时间复杂的也是 O(1)

    \n

    remove 就没这么简单了,需要先通过遍历的方式找到要移除的元素,然后将找到的位置到最后一个有效位置这之间的指针都指向其 next 指向的元素(也就是 ob_item[i]=ob_item[i+1]),然后 ob_size-1,所以时间复杂度也为 O(n)

    \n

    这就是我对 list 几个常见操作的理解,如有错误之处请通过邮件方式指出: jiapan.china#gmail.com

    \n"},{"title":"将博客部署到七牛","url":"/2017/%E5%B0%86%E5%8D%9A%E5%AE%A2%E9%83%A8%E7%BD%B2%E5%88%B0%E4%B8%83%E7%89%9B/","content":"

    我觉得我是不折腾就会死的人,现在的博客部署在 Github Pages 上,访问速度一直令我非常不爽,所以准备迁移到七牛。

    \n

    七牛新建 bucket 后,在空间设置,把默认首页设置改成启动就完成了。

    \n

    还有一个就是一定要绑定自己的独立域名,否则站内数据改变后,用七牛提供的临时域名来访问的话,缓存一时半会是不会刷新的,独立域名可以设置缓存刷新时间,前提是那个域名需要在国内进行备案。

    \n

    缓存可以根据不同类型的数据有不同的策略,但我为了省事,直接将所有配置改为了 0,也就是不进行缓存,因为不是什么大流量站点,再加上七牛的 CDN 优化,所以即便不缓存速度也非常快,这样可以保证我每次在发布或者修改内容后能够及时更新。其实完全可以把非 HTML 类型的数据设置一些缓存时间。

    \n

    \"\"

    \n

    可以点这里试一试部署在七牛上的速度,只是我之前的一个备案过的域名,但我没打算用这个域名来做我的国内博客地址,新地址正在备案中: jpanj.com 贾攀家。

    \n

    接下来就是把博客生成出来的静态站按照目录结构完整上传到七牛就行了,但是这个工作非常麻烦,每次上传时如果有二级目录的话,需要自己填写前缀,而且每次生成后都需要自己登录到七牛上传一下。身为程序员的我,这不是在侮辱我的智商吗?所以我写了一个 Python 脚本,可以帮我自动完成这个工作。

    \n

    我之前是使用的 hexo 的 github 插件来进行发布的,其实也非常简单,只需要执行: hexo d -g 即可完成生成静态站和部署的工作。(g=Generate static files. d=Deploy your website.),现在只需要在执行玩这个操作后,再执行下发布到七牛的脚本,就可以完成双发布了。

    \n

    我是配合 Alfred 来用的,如果没装 Alfred Workflow 的话,直接执行脚本也是可以的,把脚本简单修改下就行了。

    \n

    上效果图:

    \n

    \"\"

    \n

    \"\"

    \n

    代码已上传到 Github:https://github.com/Panmax/qiniu-blog-deploy

    \n
    \n

    updateAt: 2017-06-20

    我把缓存策略改为了如下所示

    \n

    \"\"

    \n

    图片和样式文件进行缓存,html 等文本文件不缓存。因为七牛会自动寻找 index.html,所以在真正访问时,不带 /index.html 后缀的页面也可以打开,所以我把全局配置设为了不缓存,也就是说不在这个配置中的文件也不进行缓存。

    \n"},{"title":"技术高手如何炼成","url":"/2015/%E6%8A%80%E6%9C%AF%E9%AB%98%E6%89%8B%E5%A6%82%E4%BD%95%E7%82%BC%E6%88%90/","content":"

    原文地址:http://zhuanlan.zhihu.com/zhengyun/20270317

    \n

    \"\"

    \n

    本文档适用人员:技术人员

    \n

    面试的时候,我会问面试者,你日常如何构建自己的知识体系,如何让自己更高更快更强?多数工程师并没有深入地思考过这个问题,基本上是零敲碎打,随机性大。本着不能让你白来一趟的精神,好为人师的我会娓娓道来:

    \n\n

    第一阶段 认真构建完整的知识体系

    十几年前我投身软件行业的时候,光是讲解数据库原理、操作系统、TCP/IP、组网、算法等等基础知识的英文原版书摞起来就等身,认认真真看完,各种上手实践,入行后,读遍 C++ 各种经典著作,读遍各种协议原文,认认真真打基础。

    \n

    很多工程师都说自己平常就是在某些 IT 门户上看看推荐的博文或新闻,我说这属于典型的零敲碎打,不够刺激。

    \n

    聊到这时,我会举一个例子,为什么要阅读长篇小说,因为中短篇小说就像用针扎你,而长篇小说就像把你装进一个沙袋里吊起来,从四面八方用狼牙棒打你,酣畅淋漓。构建可用的知识体系,就得读书,书是有体系结构的,你关心不关心,现阶段你用到用不到,它都讲到了,从头到尾看几遍,针扎得透透的。

    \n

    何谓知识体系?

    \n

    几年前,前支付宝架构师姚建东曾经在我们公司做过技术人员如何规划自己的分享讲座,他是这么论述的:

    \n

    技术与技巧包括:

    \n
      \n
    • 计算机基础理论
        \n
      • 计算机模型:内存/IO/时钟/CPU……
      • \n
      • 算法
      • \n
      • 专项技术领域:
          \n
        • 数据挖掘
        • \n
        • 数据管理
        • \n
        • 智能推荐
        • \n
        • 搜索
        • \n
        • ……
        • \n
        • \n
        \n
      • \n
      \n
    • \n
    • 语言与工具

      \n
        \n
      • 语言与相关体系
      • \n
      • 开发工具,分析工具,代码管理工具
      • \n
      • HTML/CSS/JS/Ajax
      • \n
      • 常用框架与第三方类库
      • \n
      \n
    • \n
    • 调试与测试

      \n
        \n
      • 调试方法和哲学
      • \n
      • 定位问题
      • \n
      • BUG管理工具
      • \n
      • 单元测试
      • \n
      • 集成测试
      • \n
      • 性能测试
      • \n
      • 安全测试
      • \n
      • 兼容性测试与方法
      • \n
      • JS/Ajax测试与方法
      • \n
      • 服务层测试
      • \n
      • Web层测试
      • \n
      \n
    • \n
    • 网络与系统

      \n
        \n
      • TCP/IP协议与模型,HTTP/SMTP等协议
      • \n
      • Linux系统,网络分析工具,系统分析工具
      • \n
      • 容量,流量与负载均衡
      • \n
      • 应用部署、规范、规划
      • \n
      • 安全
      • \n
      • 监控与故障分析
      • \n
      • 磁盘与存储
      • \n
      • Shell
      • \n
      • DNS与域名
      • \n
      • 缓存,反向代理
      • \n
      • 图片服务器(海量小文件)
      • \n
      \n
    • \n
    • 需求挖掘与分析

      \n
        \n
      • 需求文档格式
      • \n
      • 需求访谈
      • \n
      • 需求分析方法,需求分析工具
      • \n
      • 领域知识与经验
      • \n
      \n
    • \n
    • 系统分析与设计
        \n
      • UML语言与模型
      • \n
      • 分析模式
      • \n
      • 设计模式,领域驱动
      • \n
      • 系统分析文档格式
      • \n
      • 系统设计文档格式
      • \n
      • 功能性需求与非功能性需求
      • \n
      \n
    • \n
    • 数据与系统
        \n
      • 数据库
      • \n
      • 可伸缩策略,扩展策略,备份,容灾,性能,安全,高可用……
      • \n
      • 数据设计与范式,SQL/NoSQL,Cache,分布式文件
      • \n
      \n
    • \n
    • 架构设计
        \n
      • 架构模式,典型互联网公司架构演进历史
      • \n
      • 架构原则,常用策略
      • \n
      • 架构设计方法
      • \n
      • 非功能性理解
          \n
        • 扩展性
        • \n
        • 伸缩性
        • \n
        • 稳定性
        • \n
        • 一致性
        • \n
        • 性能
        • \n
        • 吞吐量
        • \n
        \n
      • \n
      • 容量预测与规划
      • \n
      • 架构体系与相关技术
      • \n
      \n
    • \n
    • 过程与管理
        \n
      • 分析过程
      • \n
      • 研发过程
      • \n
      • 评审过程
      • \n
      • 测试过程
      • \n
      • 发布过程
      • \n
      • 回滚过程
      • \n
      • 文档管理
      • \n
      • 知识管理
      • \n
      • 项目管理
      • \n
      \n
    • \n
    \n

    以上其实就是一份从业基础知识清单,你可以按图索骥,阅读相关书籍。

    \n

    第二阶段 顺着一个Topic钻进去,锻炼自己的预研能力

    无论公司业务还是自己喜欢做的事,都可以抽象出通用性课题,然后以做论文的方式杀进去。这个事情得反复操练,有意识操练。

    \n

    做事方式为:

    \n
      \n
    1. 抽象出 Topic——如分布式锁,分布式并行计算引擎,防CSRF的FormToken自动生成框架,定时任务管理与调度平台,分布式跟踪,等等
    2. \n
    3. 向功课好的学生学习——有针对性地深入了解业界其他公司是如何分析问题和解决问题的,汇总各种方案,站在巨人的肩膀上
    4. \n
    5. 分析特定应用场景,技术选型
    6. \n
    7. 兼顾高可用性和可伸缩,做设计评审
    8. \n
    9. 做测试自证靠谱,梳理知识点,开技术分享会
    10. \n
    11. 上线商用,总结经验教训,开经验分享会
    12. \n
    \n

    其中一个重点是汇总和分享。05年时,应电信级统一消息业务需要,我去研究了 SIP 协议,做了各种试验,分析报文,写了一系列的幻灯片,做了公开分享,一时间还颇受欢迎:

    \n
      \n
    1. SIP_to_Freshman_by_zhengyun.ppt
    2. \n
    3. SIP之穿越NAT_by_zhengyun.ppt
    4. \n
    5. SIP体系架构讲义及消息交互演示_by_zhengyun.ppt
    6. \n
    7. SIP多方会话消息之实例讲解_by_zhengyun.ppt
    8. \n
    9. SIP安全框架之认证[NTLM和Kerberos]_by_zhengyun.ppt
    10. \n
    11. SIP消息之逐项讲解_by_zhengyun.ppt
    12. \n
    \n

    为什么要写出来、讲出来呢?
    因为有一个学习金字塔理论,如下图所示:

    \n

    \"\"

    \n

    我们读过的事情能够记住学习内容的10%,

    \n

    我们听过的事情能够记住20%,

    \n

    我们看过的事情能够记住30%,

    \n

    我们听过和看过的事情能够记住50%——如看影像/看展览/看演示/现场观摩,

    \n

    我们说过的事情能够记住70%——如参与讨论/发言,

    \n

    我们说过和做过的事情能够记住90%——如做报告,给别人讲,亲身体验,动手做。

    \n

    这也就是我在《窝窝研发过去几年做对了哪些事》中阐述的管理方法:我们从入职之后就有意识地训练大家,让大家能够公开陈述、清晰表达。所以,试用期内,新人必须做一次技术分享和一次技术评审,面对各方的 challenge;预研的中间和结尾都要有分享会;平时也要定期组织技术讲座。

    \n

    第三阶段 疯狂回答技术问题

    知识体系慢慢构建,与业务相关的抽象 Topic 也在探索中。
    但这还不够。

    \n

    因为你亲身接触到的世界太小,可能不足以构成挑战,你可能意识不到自己缺多少知识和技能,不利于你分析问题、提出问题和解决问题的能力培养。

    \n

    所以,要主动出击:

    \n

    疯狂回答问题。

    \n

    我曾经在入行的头几年里几乎把我关注的垂直领域(包括语言领域和业务领域)里的所有问题都回答了一遍。我对外宣扬知无不言言无不尽,放出邮件地址和 MSN(那时候 MSN 很高大上),很多网友都会发邮件或者加我好友,问各种开发疑难问题,平均每天都有几个,然后我把解决问题的过程写成微软 KB(KnowledgeBase) 文体发表在我的博客上。

    \n

    你想想看,工作中的问题你平均每隔几天才能遇到一个,而这么做,每天你都会遇到几个乃至于十几个,第一让你脑力激荡,第二接触到更多新知。

    \n

    05年到06年期间,我因工作需要学习了 JavaME(或古老的称呼 J2ME),早年间 Symbian 手机上的客户端开发。那段时间我天天扫中文论坛的帖子,力求回答所有问题,尤其是那些 BUG 或故障。对于那些暂时没有人解决的,如流媒体实时播放,如仿 OperaMini 二级菜单界面,都上下求索,最后放出思路以及源码。

    \n

    同时,我经常整理常见问题,梳理成册并发布。譬如我整理过的 J2ME 疑难问题:

    \n
      \n
    1. [J2ME Q&A]真机报告MontyThread -n的错误之解释
    2. \n
    3. [J2MEQ&A]WTK初始化WMAClient报错XXX has no IP address的解释
    4. \n
    5. [J2ME Q&A]untrusted domain is not configured问题回应
    6. \n
    7. [J2ME]“Cannot open socket for LIME events”错误解决
    8. \n
    9. 几个月后,我成为 J2ME 中文论坛超级版主。通过这个历程,我想告诉大家,回答网友问题,技巧得当的话,比如别老是重复回答新手问题,试着攻克那些疑难问题,或者离奇故障,绝对不会浪费你的时间。
    10. \n
    \n

    为什么?

    \n

    因为你要信奉:

    \n
    \n

    你学过的每一样东西,你遭受的每一次苦难,都会在你一生中的某个时候派上用场。
    ——佩内洛普·菲兹杰拉德 《离岸》

    \n
    \n
    \n

    Everything that you’ve learnt and all the hardships you’ve suffered will all come in handy at some point in your life.

    \n
    \n

    第四阶段 RCA/总结

    现在是你把经验教训变为财富的时刻了。

    \n

    什么是好的技术 Leader?

    \n

    随便一个业务需求或业务场景讲出来,你立刻把它抽象为几个模块/系统/Topic,然后侃侃而谈,业界都是怎么解决的,我们以前又是怎么分析怎么解决的,现在咱们这种情况下应该如何设计,可能会遇到什么问题,我们应该做哪些预防设计,blabla。

    \n

    怎么做到这一点?

    \n

    第一,写 RCA 报告。

    \n

    我以前说过,『窝窝从 2011 年开始,一直坚持每错必查、错了又错就整改、每错必写,用身体力行告诉每一个新员工直面错误、公开技术细节、分享给所有人,长此以往,每一次事故和线上漏测都会变为我们的财富。这就是我们的 RCA(Root Cause Analysis)制度,截止到目前已经收集整理了近两百个详尽的 RCA 报告。』

    \n

    RCA 报告格式为:

    \n
      \n
    1. 背景知识(Optional)
    2. \n
    3. 问题现象
    4. \n
    5. 影响范围
    6. \n
    7. 问题原因
    8. \n
    9. 问题分析过程(Optional)
    10. \n
    11. 解决办法
    12. \n
    13. 后续处理措施:如线上脏数据如何修复,如对用户造成的影响如何弥补等(Optional)
    14. \n
    15. 经验教训
    16. \n
    17. RCA类型:如代码问题、实施问题、配置问题、设计问题、测试问题
    18. \n
    \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 数据, 会报:

    \n
    <title>400 Bad Request</title>
    <h1>Bad Request</h1>
    <p>Failed to decode JSON object: No JSON object could be decoded</p>
    \n

    解决办法是在 js 的 ajax 方法中加上:contentType:"application/json; charset=utf-8",

    \n

    注:只需要在提交的数据类型为 JsonPOSTPUT 的方法上添加就行了,不用在提交 Form 表单 的地方添加,否则加上后 Form 表单 类型的 ajax 就无法提交了。

    \n"},{"title":"时间戳转换方法","url":"/2015/%E6%97%B6%E9%97%B4%E6%88%B3%E8%BD%AC%E6%8D%A2%E6%96%B9%E6%B3%95/","content":"
    datetime = datetime.datetime.now()
    timestamp = time.time()
    datetime_format = %Y-%m-%d %H:%M:%S

    # str -> datetime
    datetime.datetime.strptime(string, datetime_format)

    # datetime -> str
    datetime.datetime.strftime(datetime_format)
    type(a) -> datetime.datetime
    a.strftime(datetime_format)

    # datetime -> timestamp
    time.mktime(datetime.datetime.timetuple())

    # timestamp -> datetime
    datetime.datetime.fromtimestamp(timestamp)
    datetime.datetime.fromtimestamp(timestamp).strftime(datetime_format)

    # timestamp -> time
    time.localtime(timestamp)
    \n

    \"\"

    \n","categories":["Code"],"tags":["Python","Datetime","timestamp"]},{"title":"有趣的this.py源码","url":"/2016/%E6%9C%89%E8%B6%A3%E7%9A%84this-py%E6%BA%90%E7%A0%81/","content":"

    以前知道python中有个彩蛋,在Python shell下,输入

    \n
    import this
    \n

    会输出:

    \n
    The Zen of Python, by Tim Peters

    Beautiful is better than ugly.
    Explicit is better than implicit.
    Simple is better than complex.
    Complex is better than complicated.
    Flat is better than nested.
    Sparse is better than dense.
    Readability counts.
    Special cases aren't special enough to break the rules.
    Although practicality beats purity.
    Errors should never pass silently.
    Unless explicitly silenced.
    In the face of ambiguity, refuse the temptation to guess.
    There should be one-- and preferably only one --obvious way to do it.
    Although that way may not be obvious at first unless you're Dutch.
    Now is better than never.
    Although never is often better than *right* now.
    If the implementation is hard to explain, it's a bad idea.
    If the implementation is easy to explain, it may be a good idea.
    Namespaces are one honking great idea -- let's do more of those!
    \n

    当时认为this模块就是直接把上边字符串print出来而已。

    \n

    今天心血来潮,看了下this.py的源码

    \n
    s = """Gur Mra bs Clguba, ol Gvz Crgref

    Ornhgvshy vf orggre guna htyl.
    Rkcyvpvg vf orggre guna vzcyvpvg.
    Fvzcyr vf orggre guna pbzcyrk.
    Pbzcyrk vf orggre guna pbzcyvpngrq.
    Syng vf orggre guna arfgrq.
    Fcnefr vf orggre guna qrafr.
    Ernqnovyvgl pbhagf.
    Fcrpvny pnfrf nera'g fcrpvny rabhtu gb oernx gur ehyrf.
    Nygubhtu cenpgvpnyvgl orngf chevgl.
    Reebef fubhyq arire cnff fvyragyl.
    Hayrff rkcyvpvgyl fvyraprq.
    Va gur snpr bs nzovthvgl, ershfr gur grzcgngvba gb thrff.
    Gurer fubhyq or bar-- naq cersrenoyl bayl bar --boivbhf jnl gb qb vg.
    Nygubhtu gung jnl znl abg or boivbhf ng svefg hayrff lbh'er Qhgpu.
    Abj vf orggre guna arire.
    Nygubhtu arire vf bsgra orggre guna *evtug* abj.
    Vs gur vzcyrzragngvba vf uneq gb rkcynva, vg'f n onq vqrn.
    Vs gur vzcyrzragngvba vf rnfl gb rkcynva, vg znl or n tbbq vqrn.
    Anzrfcnprf ner bar ubaxvat terng vqrn -- yrg'f qb zber bs gubfr!"""

    d = {}
    for c in (65, 97):
    for i in range(26):
    d[chr(i+c)] = chr((i+13) % 26 + c)

    print "".join([d.get(c, c) for c in s])
    \n

    当时我就震惊了。。。

    \n

    乍一看,s保存的是个什么鬼😂,还以为是什么小语种,然后进行国际化后再输出呢,再往下看,才明白了原理。

    \n

    先把所有大小写字母经过一定算法转换后,将对照表保存在一个字典中,逐个遍历”加密”后的字符串,从字典中取出对应结果然后进行拼接后再输出。。。

    \n

    顺便也知道了chr(c)的用处。

    \n","categories":["Code"],"tags":["Python"]},{"title":"explore flask 环境","url":"/2015/%E7%8E%AF%E5%A2%83/","content":"

    环境

    \"\"

    \n

    版本控制

    选择一个版本控制系统并且使用它。我推荐Git。在我看来,Git是最近新项目最流行的选择。能够删除代码而不用担心造成不可逆的错误是很宝贵的。你可以让你的项目中大量被注释的代码快自由了,因为你现在可以删除它们并且在随后需要的时候恢复这些改变。另外,你将拥有完整的项目备份在GitHub,Bitbucket或者你自己的Gitolite服务上。

    \n\n

    避开版本控制

    我通常把一个文件放在版本控制之外因为两个原因之一。其中一个是杂乱的东西另一个是秘密的东西。例如编译后的.pyc文件和虚拟环境(如果你因为某些原因没有使用virtualenvwrapper)是杂乱的东西。它们不需要在版本控制中,因为它们可以各自通过.py文件和你的requirements.txt文件被重新创建。

    \n

    例如API密钥,应用密钥和数据库证书是秘密的东西。它们不应该在版本控制中因为它们的暴露将会造成很大的安全问题。

    \n

    调试

    调试模式

    Flask自带一个很好用的功能叫调试模式。你只需要在你的开发配置中设置debug = True就可以打开它。当调试模式打开,服务器将会在代码发生变化时重新加载并且带有堆栈跟踪和交互控制台。

    \n

    Flask-DebugToolbar

    Flask-DebugToolbar是另外一个很棒的工具用来调试你应用中的问题。在调试模式中,它在你的应用中的每一个页面上覆盖一个边栏。这个边栏给你关于SQL查询,日志,版本,模板,配置的信息和其他有趣的东西用来更容易的跟踪问题。

    \n

    总结

      \n
    • 使用virtualenv保持你的应用的依赖在一起。
    • \n
    • 使用virtualenvwrapper保持你的虚拟环境依赖在一起
    • \n
    • 保持一个或多个文本文件的追踪依赖。
    • \n
    • 使用版本控制系统。我推荐Git。
    • \n
    • 使用’.gitignore’来让杂乱的东西和秘密的东西置于版本控制之外。
    • \n
    • 调试模式可以给你关于开发中遇到的问题的信息。
    • \n
    • Flask-DebugToolbar扩展将给你更多的信息。
    • \n
    \n","categories":["exploreflask"],"tags":["flask"]},{"title":"简明 Python 编程规范","url":"/2016/%E7%AE%80%E6%98%8E-Python-%E7%BC%96%E7%A8%8B%E8%A7%84%E8%8C%83/","content":"

    原文地址:http://blog.csdn.net/gzlaiyonghao/article/details/6601123

    \n

    编码

      \n
    • 所有的 Python 脚本文件都应在文件头标上如下标识或其兼容格式的标识:
    • \n
    \n
    # -*- coding:utf-8 -*-
    \n
      \n
    • 设置编辑器,默认保存为 utf-8 格式。
    • \n
    \n

    注释

      \n
    • 业界普遍认同 Python 的注释分为两种的概念,一种是由 # 开头的“真正的”注释,另一种是 docstrings。前者表明为何选择当前实现以及这种实现的原理和难点,后者表明如何使用这个包、模块、类、函数(方法),甚至包括使用示例和单元测试。
    • \n
    • 坚持适当注释原则。对不存在技术难点的代码坚持不注释,对存在技术难点的代码必须注释。但与注释不同,推荐对每一个包、模块、类、函数(方法)写 docstrings,除非代码一目了然,非常简单。
    • \n
    \n

    格式

    缩进

      \n
    • Python 依赖缩进来确定代码块的层次,行首空白符主要有两种:tab 和空格,但严禁两者混用。
    • \n
    • 公司内部使用 4 个空格的 tab 进行缩进。
    • \n
    \n
    \n

    (此处修改为我自己的规范,原文是使用2个空格)

    \n
    \n

    空格

      \n
    • 空格在 Python 代码中是有意义的,因为 Python 的语法依赖于缩进,在行首的空格称为前导空格。在这一节不讨论前导空格相关的内容,只讨论非前导空格。非前导空格在 Python 代码中没有意义,但适当地加入非前导空格可以增进代码的可读性。
    • \n
    • 在二元算术、逻辑运算符前后加空格,如:
    • \n
    \n
    a = b + c
    \n
      \n
    • “:”用在行尾时前后皆不加空格,如分枝、循环、函数和类定义语言;用在非行尾时两端加空格,如 dict 对象的定义:
    • \n
    \n
    d = {'key' : 'value'}
    \n
      \n
    • 括号(含圆括号、方括号和花括号)前后不加空格,如:
    • \n
    \n
    do_something(arg1, arg2)
    \n

    而不是

    \n
    do_something( arg1, arg2 )
    \n
      \n
    • 逗号后面加一个空格,前面不加空格。
    • \n
    \n

    空行

      \n
    • 适当的空行有利于增加代码的可读性,加空行可以参考如下几个准则:
        \n
      • 在类、函数的定义间加空行;
      • \n
      • 在 import 不同种类的模块间加空行;
      • \n
      • 在函数中的逻辑段落间加空行,即把相关的代码紧凑写在一起,作为一个逻辑段落,段落间以空行分隔
      • \n
      \n
    • \n
    \n

    断行

      \n
    • 尽管现在的宽屏显示器已经可以单屏显示超过 256 列字符,但本规范仍然坚持行的最大长度不得超过 78 个字符的标准。折叠长行的方法有以下几种方法:

      \n
        \n
      • 为长变量名换一个短名,如:

        \n
        this._is.a.very.long.variable_name = this._is.another.long.variable_name
        \n
      • \n
      \n
    • \n
    \n
    - 应改为:\n\n
    variable_name1 = this._is.a.very.long.variable_name  
    variable_name2 = this._is.another.variable_name
    variable_name1 = variable_name2s
    \n\n\n- 在括号(包括圆括号、方括号和花括号)内换行,如:\n\n
    class Edit(Widget):  
    def __init__(self, parent, width,
    font = FONT, color = BLACK, pos = POS, style = 0): # 注意:多一层缩进
    pass
    \n\n\n或\n\n
    very_very_very_long_variable_name = Edit(parent,  
    width,
    font,
    color,
    pos) # 注意:多一层缩进
    do_sth_with(very_very_very_long_variable_name)
    \n\n\n- 如果行长到连第一个括号内的参数都放不下,则每个元素都单独占一行:\n\n
    very_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\n
    if color == WHITE or color == BLACK \\  
    or color == BLUE: # 注意 or 操作符在新行的行首而不是旧行的行尾,上一行的续行符不可省略
    do_something(color);
    else:
    do_something(DEFAULT_COLOR);
    \n

    命名

      \n
    • 一致的命名可以给开发人员减少许多麻烦,而恰如其分的命名则可以大幅提高代码的可读性,降低维护成本。
    • \n
    \n

    常量

      \n
    • 常量名所有字母大写,由下划线连接各个单词,如:
    • \n
    \n
    WHITE = 0xffffffff  
    THIS_IS_A_CONSTANT = 1
    \n

    变量

      \n
    • 变量名全部小写,由下划线连接各个单词,如:
    • \n
    \n
    color = WHITE  
    this_is_a_variable = 1
    \n
      \n
    • 不论是类成员变量还是全局变量,均不使用 m 或 g 前缀。私有类成员使用单一下划线前缀标识,多定义公开成员,少定义私有成员。
    • \n
    • 变量名不应带有类型信息,因为 Python 是动态类型语言。如 iValue、names_list、dict_obj 等都是不好的命名。
    • \n
    \n

    函数

      \n
    • 函数名的命名规则与变量名相同。
    • \n
    \n

      \n
    • 类名单词首字母大写,不使用下划线连接单词,也不加入 C、T 等前缀。如:
    • \n
    \n
    class ThisIsAClass(object):  
    passs
    \n

    模块

      \n
    • 模块名全部小写,对于包内使用的模块,可以加一个下划线前缀,如:
    • \n
    \n

    module.py
    _internal_module.py
    \n

      \n
    • 包的命名规范与模块相同。
    • \n
    \n

    缩写

      \n
    • 命名应当尽量使用全拼写的单词,缩写的情况有如下两种:

      \n
        \n
      • 常用的缩写,如 XML、ID等,在命名时也应只大写首字母,如:

        \n
        class XmlParser(object):pass
        \n
      • \n
      \n
    • \n
    \n
    - 命名中含有长单词,对某个单词进行缩写。这时应使用约定成俗的缩写方式,如去除元音、包含辅音的首字符等方式,例如:\n    - function 缩写为 fn\n    - text 缩写为 txt\n    - object 缩写为 obj\n    - count 缩写为 cnt\n    - number 缩写为 num,等。\n

    特定命名方式

      \n
    • 主要是指 __xxx__ 形式的系统保留字命名法。项目中也可以使用这种命名,它的意义在于这种形式的变量是只读的,这种形式的类成员函数尽量不要重载。如:
    • \n
    \n
    class Base(object):  
    def __init__(self, id, parent = None):
    self.__id__ = id
    self.__parent__ = parent
    def __message__(self, msgid):
    # …略
    \n

    其中 __id____parent____message__ 都采用了系统保留字命名法。

    \n

    语句

    import

      \n
    • import 语句有以下几个原则需要遵守:

      \n
        \n
      • import 的次序,先 import Python 内置模块,再 import 第三方模块,最后 import 自己开发的项目中的其它模块;这几种模块中用空行分隔开来。
      • \n
      • 一条 import 语句 import 一个模块。
      • \n
      • 当从模块中 import 多个对象且超过一行时,使用如下断行法(此语法 py2.5 以上版本才支持):

        \n
        from module import (obj1, obj2, obj3, obj4,  
        obj5, obj6)
        \n
      • \n
      \n
    • \n
    \n
    - 不要使用 from module import *,除非是 import 常量定义模块或其它你确保不会出现命名空间冲突的模块。\n

    赋值

      \n
    • 对于赋值语句,主要是不要做无谓的对齐,如:
    • \n
    \n
    a        = 1                  # 这是一个行注释  
    variable = 2 # 另一个行注释
    fn = callback_function # 还是行注释
    \n

    没有必要做这种对齐,原因有两点:一是这种对齐会打乱编程时的注意力,大脑要同时处理两件事(编程和对齐);二是以后阅读和维护都很困难,因为人眼的横向视野很窄,把三个字段看成一行很困难,而且维护时要增加一个更长的变量名也会破坏对齐。直接这样写为佳:

    \n
    a = 1 # 这是一个行注释  
    variable = 2 # 另一个行注释
    fn = callback_function # 还是行注释
    \n

    分枝和循环

      \n
    • 对于分枝和循环,有如下几点需要注意的:

      \n
        \n
      • 不要写成一行,如:

        \n
        if not flg: pass
        \n
      • \n
      \n
    • \n
    \n
    和\n\n
    for i in xrange(10): print i
    \n\n\n都不是好代码,应写成\n\n
    if not flg:  
    pass
    for i in xrange(10):
    print i
    \n\n\n注:本文档中出现写成一行的例子是因为排版的原因,不得作为编码中不断行的依据。\n
      \n
    • 条件表达式的编写应该足够 pythonic,如以下形式的条件表达式是拙劣的:
    • \n
    \n
    if len(alist) != 0: do_something()  
    if alist != []: do_something()
    if s != "": do_something()
    if var != None: do_something()
    if var != False: do_something()
    \n

    上面的语句应该写成:

    \n

    if seq: do_somethin() # 注意,这里命名也更改了
    if var: do_something()
    \n
      \n
    • 用得着的时候多使用循环语句的 else 分句,以简化代码。
    • \n
    \n

    已有代码

      \n
    • 对于项目中已有的代码,可能因为历史遗留原因不符合本规范,应当看作可以容忍的特例,允许存在;但不应在新的代码中延续旧的风格。
    • \n
    • 对于第三方模块,可能不符合本规范,也应看作可以容忍的特例,允许存在;但不应在新的代码中使用第三方模块的风格。
    • \n
    • tab 与空格混用的缩进是’’’不可容忍’’’的,在运行项目时应使用 -t 或 -tt 选项排查这种可能性存在。出现混用的情况时,如果是公司开发的基础类库代码,应当通知类库维护人员修改;第三方模块则可以通过提交 patch 等方式敦促开发者修正问题。
    • \n
    \n

    已有风格

      \n
    • 开发人员往往在加入项目之前已经形成自有的编码风格,加入项目后应以本规范为准编写代码。特别是匈牙利命名法,因为带有类型信息,并不适合 Python 编程,不应在 Python 项目中应用。
    • \n
    \n","categories":["转载"],"tags":["Python"]},{"title":"纪念很久没有写代码到深夜 && 几个提高效率的oh-my-zsh插件","url":"/2017/%E7%BA%AA%E5%BF%B5%E5%BE%88%E4%B9%85%E6%B2%A1%E6%9C%89%E5%86%99%E4%BB%A3%E7%A0%81%E5%88%B0%E6%B7%B1%E5%A4%9C-%E5%87%A0%E4%B8%AA%E6%8F%90%E9%AB%98%E6%95%88%E7%8E%87%E7%9A%84oh-my-zsh%E6%8F%92%E4%BB%B6/","content":"

    昨晚写代码时遇到一个坑,导致12点半左右才合上电脑。这个坑是自己挖出来的,大致原因是在使用 sqlalchemy 读取一个数据前,给这个数据进行了操作,导致每次读出来的值都不准。之前没想到是因为前边的代码操作了数据,恰好我在这之前为了验证一些逻辑,手动改了下表数据,所以我一度怀疑是 sqlalchemy 或者 mysql 的缓存导致,或者有事务没有提交导致,然后各种查资料,尝试关闭 mysql 缓存啥的,都没有解决问题,但是后来发现用不同参数调用时,有时又能得到正确的数据,使我不禁开始怀疑人生。

    \n

    眼看到了0点,我静下心来,一行一行检查代码运行路径,最终捉住了这只虫子。。。在我印象中,自从去年7、8月份后,就没有写代码到这么晚了,因为之前每次写代码都会兴奋,导致休息不好,所以就改掉了深夜写代码的习惯。

    \n

    今天白天在打开终端时,我的 oh-my-zsh 例行提示我是否要检查更新,我进行了更新工作后,饶有兴趣的查了查 oh-my-zsh 的常用插件,自己也收入囊肿几个。在此做下记录:

    \n

    先说下如何配置插件,打开 ~/.zshrc 里边有个

    \n

    plugins=(...) 编辑括号中的内容就可以了

    \n

    d

    这个插件可以记录我本次窗口进入过的目录历史记录,当在几个目录之间来回穿梭时,可以输入 d 回车,按照提示的数字直接进入之前进入过的目录。

    \n

    sublime

    之前在命令行下,为了快速编辑一个文件,我通常使用 vi, 或者做复杂编辑的时候使用 atom,其实我更喜欢用 sublime 一些,但是一直找不到如何让 终端 调起它的方法,今天终于知道到了。常用命令如下

    \n
    st          # 直接打开sublime
    st file_a # 用sublime打开文件 file
    st dir_a # 用sublime打开目录 dir
    stt # 在sublime打开当前目录,相当于 st .
    \n

    extract

    我觉得这个插件真的解决了我的痛点,之前每次解压文件,都需要先去网上查下命令,比如解 gz.tar 需要用什么命令 解压 tar 需要什么命令,解压 zip 需要什么命令,现在好了,需要解压文件时直接 x file_name 就完成了。

    \n

    z

    作用和 autojump 相同,autojump 是使用 j 作为启动键,z 是用 z 作为启动键,但是查阅资料后解释 z 的速度更快一些,z 是使用 shell 直接编写的,而 autojump 则是用 Python 编写(又黑 Python )。。。

    \n

    web-search

    这个是用来在终端中启用搜索的命令,比如 输入 google Python 会自动用默认浏览器打开 google 并用 Python 作为关键字进行查询。同时也支持 baidu、bing。

    \n

    我现在的插件列表如下:

    \n

    plugins=(git d sublime extract z web-search)

    \n

    git 的 Aliases 见: https://github.com/robbyrussell/oh-my-zsh/wiki/Plugin:git

    \n"},{"title":"explore flask 组织你的项目","url":"/2015/%E7%BB%84%E7%BB%87%E4%BD%A0%E7%9A%84%E9%A1%B9%E7%9B%AE/","content":"

    组织你的项目

    \"\"

    \n

    Flask将你的应用的组织工作有你来决定。这也是我像初学者一样喜欢Flask的原因之一,但是这确实意味着你必须对如何构建你的代码做一番思考。你可以把你整个应用放在一个文件中,或者把它分散在多个包中。这里有一些你可以遵循的组织模式,可以更轻松的开发和部署。

    \n\n

    定义

    让我们来定义一些在本章中会遇到的术语

    \n

    版本库 - 这是放置你的项目的基础文件夹。这个术语传统上指的是版本控制系统,你应该使用版本控制系统。当我在本章中提到你的存放库,我将说的是你的项目的跟目录。当在你的应用中工作时,你可能将不会离开这个目录。

    \n

    - 这个指的是一个包含你的应用代码的Python包。我将会在本章谈论更多的关于设置你的作为一个包,但是现在只需要知道包是版本库的一个子目录。

    \n

    模块 - 一个模块是一个单独的Python文件可以被其他Python文件所导入。一个包本质上是多个模块被包裹在一起。

    \n
    注意
    \n

    组织模式

    单模块

    你会遇到很多Flask示例将所有的代码放在一个文件中,常常是app.py。这对于快速项目是很好的(比如一个用来教学的项目),你只需要在这个文件里提供一些路由并且你已经获得少于几百行的应用代码了。

    \n
    app.py
    config.py
    requirements.txt
    static/
    templates/
    \n

    应用逻辑将放在清单的app.py中。

    \n

    当你工作的项目稍微有些复杂时,一个单独的模块会导致混乱。你将需要为模型和表单定义类,并且他们将与你的路由和配置代码混合起来。所有的这些会阻碍开发。为了解决这个问题,我们能够把我们应用的不同的组件分解出来进行分组形成相互连接的模块 – 一个包。

    \n
    config.py
    requirements.txt
    run.py
    instance/
    \tconfig.py
    yourapp/
    \t__init__.py
    \tviews.py
    \tmodels.py
    \tforms.py
    \tstatic/
    \ttemplates/
    \n

    在这个列表中展示的这种结构允许你将应用中不同的组件按照符合逻辑的方式进行分组。定义模型的类一起放在models.py中,路由定义放在views.py中,表单定义放在forms.py中(我们稍后有整章来介绍表单)。

    \n

    下边的表提供一个基本的组件概述,你会在大多数的Flask应用中见到。你可能最终在你的版本库中有很多其它文件,但是这些是大多数Flask应用中最普遍的。

    \n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n
    文件描述
    run.py这个文件被调用启动开发服务器。它从你的包中获得应用的副本并且运行它。这个不能用在生产中,但是它将在开发中有很多的用途。
    requirements.txt这个文件列出所有你的应用所依赖的Python包。你可能为生产和开发依赖准备了各自的文件。
    config.py这个文件包含了大多数你的应用所需要的配置变量。
    /instance/config.py这个文件包含不应该在版本控制中的配置变量。这里包括的东西比如API密钥和数据库RUIs包含的密码。这个也包含你的应用特定情况下的特殊的变量。比如你可能在config.py中有DEBUG = False,但是设置DEBUG = True在 instance/config.py在你作为开发的本地设备中。因为这个文件会在config.py后读取,这将会覆盖它并且设置DEBUG = True
    /yourapp/这是一个包含你应用程序的包。
    /yourapp/__init__.py这个文件初始化你的应用并且汇集把所有不同的组件汇集在一起。
    /yourapp/views.py这是路由被定义的地方。它可能切分为它自己的包(yourapp/views)与有联系的视图组合在一起成为一个模块。
    /yourapp/models.py这是定义你的应用模型的地方。这个可能以views.py相同的方式分成多个模块。
    /yourapp/static/这个文件包含公开的CSS,JavaScript,图片和其他你想通过应用程序公开的文件。这些默认是可以通过 yourapp.com/static/ 来访问的。
    /yourapp/templates/这是将要放置你的应用Jinja2模板的地方。
    \n

    蓝图

    有时你可能会发现你有很多相关的路由。如果你像我一样,你的第一个想法是将views.py切分成一个包并且将这些视图分组到一个模块中。这种情况下,可能是时候考虑将你的应用放在蓝图中了。

    \n

    蓝图本质上是有点自包含方式定义你的应用的组件。他们作为你应用程序中的应用。你可能为管理员控制台、前后端和用户仪表盘有不同的蓝图。这让你通过组件来分组视图、静态文件和模板,同时来人让你分享模型、表单和其他你的应用程序在这些组件之间的部分。我们马上将会谈论使用蓝图组织你的应用。

    \n

    总结

      \n
    • 为你的应用使用单一的模块,对于快速项目来说是很好的。
    • \n
    • 为你的项目使用包,对于项目中的视图、模型、表单和其他组件来说是很好的。
    • \n
    • 蓝图是用几个不同的组件来组织项目很好的方式。
    • \n
    \n

    【完】

    ","categories":["exploreflask"],"tags":["flask"]},{"title":"翻出CSDN博客有感","url":"/2016/%E7%BF%BB%E5%87%BACSDN%E5%8D%9A%E5%AE%A2%E6%9C%89%E6%84%9F/","content":"

    今天有同事说自己的CSDN博客被盗了,我才隐约想起我以前好像也在CSDN写过东西。找了好久才找到当年的博客,都是那时候学C++时候的笔记,如果我到现在还在坚持用C++的话,也许也已经能和「轮子哥」谈笑风生了吧。

    \n

    \"\"

    \n

    C++让我懂了很多直接学习动态语言和其他高级语言(比如Java,我并没有黑Java)所接触不到的和更底层的东西,比如指针、内存动态分配、构造函数和析构函数的作用等等。
    那时候还亲自动手写过各种数据结构和算法的C++实现,也走过很多很多的弯路。

    \n

    翻到这个东西还能证明一点,我已经符合至少三年开发经验的要求了😂

    \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":"
      \n
    • 推模式:被关注者产生一条news,会给所有的关注者每人生成一条feed数据。

      \n
        \n
      • 优点:查询速度快,性能高
      • \n
      • 缺点:产生数据条目多,写入量大

        \n
      • \n
      • 最严重缺点:这种模式类似朋友圈,只有关注时开始,才给关注者产生feed数据,之前的被关注者发布的news是收不到feed的(其实也可以收到,不过相当麻烦,假如之前被关注者已经发布了很多news了,需要逐个为之前的news生成feed)

        \n
      • \n
      \n
    • \n
    \n\n
    \n
      \n
    • 拉模式:被关注者产生一条news,只产生一条和被关注者相关的feed数据,其他用户在看自己关注的人feed流时,逐个查询他所关注人的feed数据,然后展示结果。

      \n
        \n
      • 优点:写入量小,关注后即可看到关注者之前产生的feed数据
      • \n
      • 缺点:查询时性能低
      • \n
      \n
    • \n
    \n
    \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":"

    视图和路由的高级模式

    \"\"

    \n

    视图装饰器

    Python装饰器是用来改变其他函数的函数。当装饰函数被调用,这个装饰器被调用替代。然后装饰器能够才去行动,修改参数,停止执行或者调用原函数。我们能使用装饰器来包装视图来运行他们执行前的代码。

    \n
    @decorator_function
    def decorated():
    \tpass
    \n

    如果你浏览过Flask的教程,你可能很熟悉这个代码块中的语法。`@app.route`Flask应用中是用来匹配URL到视图函数的装饰器。

    \n

    来看一些其他你能够在你的Flask应用中使用的装饰器。

    \n\n

    认证

    Flask-Login扩展使可以很容易的实现一个登录系统。出了处理用户认证的细节,Flask-Login给了我们一个装饰器用来限制某些视图给已经认证的用户:@login_required

    \n
    # app.py

    from flask import render_template
    from flask.ext.login import login_required, current_user

    @app.route('/')
    def index():
    \treturn render_template("index.html")

    @app.route('/dashboard')
    @login_required
    def account():
    \treturn render_template("account.html")
    \n
    警告

    @app.route应该永远是最远的视图装饰器。

    \n
    \n

    只有被认证的用户将能够访问/dashboard路由。我们能配置Flask-Login让没有认证的用户跳转到登录页面,返回一个HTTP 401状态或者任何其他我们希望他们做的。

    \n
    注意

    官方文档阅读更多关于Flask-Login的使用。

    \n
    \n

    缓存

    想象一篇提及到我们应用的文章刚刚发表在CNN和其他新闻站点。我们每秒钟获得成千上万的请求。我们的主页为每个请求前往数据库多次,所以这一切注意力都放慢下来到爬行。我们如何让速度快速加快,因此所有这些访问者就不会错过我们的站点。

    \n

    这里有很多好的回答,但是这个部分是关于缓存的,所以我们将要谈谈关于缓存的东西。明确来说,我们将要使用Flask-Cache扩展。这个扩展提供给我们一个装饰器,我们可以用在我们的主页视图上用来在一段时间内缓存响应。

    \n

    Flask-Cache 能够被配置和很多不同的缓存后端一起工作。一个流行的选择是Redis,这个我们可以简答设置和使用。假定Flask-Cache已经被配置完成,这段代码块展示我们的装饰器视图是什么样子的。

    \n
    # app.py

    from flask.ext.cache import Cache
    from flask import Flask

    app = Flask()

    # We'd normally include configuration settings in this call
    cache = Cache(app)

    @app.route('/')
    @cache.cached(timeout=60)
    def index():
    \t[...] # Make a few database calls to get the information we need
    \treturn render_template(
    \t\t'index.html',
    \t\tlatest_posts=latest_posts,
    \t\trecent_users=recent_users,
    \t\trecent_photos=recent_photos
    \t)
    \n

    现在这个函数将会每60秒只运行一次,这时候缓存过期。这个响应将会被保存在我们的缓存中并且为任何有障碍的请求从这里获取响应。

    \n
    注意

    Flask-Cache 也让我们memoize函数或者缓存用确定参数调用的函数的结果。我们甚至能够缓存计算昂贵的Jinja2模板片段。

    \n
    \n

    自定义装饰器

    在这部分,让我们想象我们有一个应用来让我们的用户每个月付费,如果一个用户的账户到期了,我们将会跳转他们到结账页面并且告诉他们去升级。

    \n
    # myapp/util.py

    from functools import wraps
    from datetime import datetime

    from flask import flash, redirect, url_for

    from flask.ext.login import current_user

    def check_expired(func):
    \t@wraps(func)
    \tdef decorated_function(*args, **kwargs):
    \t\tif datetime.utcnow() > current_user.account_expires:
    \t\t\tflash("Your account has expired. Update your billing info.")
    \t\t\treturn redirect(url_for('account_billing'))
    \t\treturn func(*args, **kwargs)

    \treturn decorated_function
    \n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n
    行数注释
    10当一个函数被@check_expried装饰,check_expried()被调用并且被装饰的函数被作为参数传递。
    11@warps是一个装饰器用来做一些簿记,使被装饰的函数()显示为func()的文档和调试的目的。这使这个函数的行为更正常一点。
    12。。。
    16。。。
    \n

    当我们把装饰器叠在一起时,最上边的装饰器将会第一个运行,然后调用下一行的函数:视图函数或者下一个装饰器之一。装饰器语法只是一点点语法糖。

    \n
    # This code:
    @foo
    @bar
    def one():
    \tpass

    r1 = one()

    # is the same as this code:
    def two():
    \tpass

    two = foo(bar(two))
    r2 = two()

    r1 == r2 # True
    \n

    这个代码块使用我们自定义装饰器和来自Flask-Login扩展的@login_required装饰器展示一个例子。我们能使用多个装饰器通过把他们叠在一起。

    \n
    # myapp/views.py

    from flask import render_template

    from flask.ext.login import login_required

    from . import app
    from .util import check_expired

    @app.route('/use_app')
    @login_required
    @check_expired
    def use_app():
    \t"""Use our amazing app."""
    \t# [...]
    \treturn render_template('use_app.html')

    @app.route('/account/billing')
    @login_required
    def account_billing():
    \t"""Update your billing info."""
    \t# [...]
    \treturn render_template('account/billing.html')
    \n

    现在当一个用户尝试访问 /user_appcheck_expired()将会在运行这个视图函数前确定他们的账户没有过期。

    \n
    注意

    Python docs了解更多关于warps()函数的作用。

    \n
    \n

    URL转换器

    内置转换器

    当你在Flask里定义一个路由,你能够指定它的一部分转换成Python变量并且传递给视图函数。

    \n
    @app.route('/user/<username>')
    def profile(username):
    \tpass
    \n

    无论。。URL标签<username>将会被传递到视图作为username参数。你也能够指定一个转换器在变量被传到视图前过滤它。

    \n
    @app.route('/user/id/<int:user_id>')
    def profile(user_id):
    \tpass
    \n

    在这个代码块,这个URL http://myapp.com/user/id/Q29kZUxlc3NvbiEh 将会返回404代码-未找到。这是因为这个URL的部分被支持变成整型实际上是一个字符串。

    \n

    我们还可以有第二个视图来查找字符串。这会被/user/id/Q29kZUxlc3NvbiEh/调用,与此同时第一个会被/user/id/124调用。

    \n

    这个表展示Flask的内置URL转换器。

    \n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n
    名称描述
    string。。。
    int。。。
    float。。。
    path。。。
    \n

    自定义转换器

    我们同样可以自定义转换器来满足我们度需要。在Reddit上(一个流行的连接分享站点),用户创建和主持主题讨论型社区和链接分享。一些例子是/r/python/r/flask,被表示为URL的路径:分别是reddit.com/r/pythonreddit.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文件,加上如下几行:

    \n

    set fileencodings=utf-8,ucs-bom,gb18030,gbk,gb2312,cp936

    \n

    set termencoding=utf-8

    \n

    set encoding=utf-8

    \n","categories":["VIM"],"tags":["Linux","VIM"]},{"title":"解决Pycharm中总是不小心拖拽代码的问题","url":"/2017/%E8%A7%A3%E5%86%B3Pycharm%E4%B8%AD%E6%80%BB%E6%98%AF%E4%B8%8D%E5%B0%8F%E5%BF%83%E6%8B%96%E6%8B%BD%E4%BB%A3%E7%A0%81%E7%9A%84%E9%97%AE%E9%A2%98/","content":"

    我一直想转 VIM 党,但转了好多次了都没转成功,后来就放弃了,给了一个安慰自己的说法:「某些事情,键盘就是没有鼠标快。」所以依旧在使用 Pycharm 作为我的主要开发工具。

    \n

    但是。。

    \n

    因为我是鼠标党或者触摸板党,所以经常不小心在用鼠标的时候拖拽代码,尤其是升级完 Sierra 后(其实我也不太清楚是因为换了Magic Mouse2 以后还是升级导致的),就经常不小心拖拽代码,将代码弄乱,每次都要按 command + z 撤销,然后还要反复检查是不是搞乱了,大大影响我的工作效率,今天突然想到,设置中是不是能关闭代码拖拽的功能,于是通过关键词 drag 找了找,果然,没有找到。

    \n

    然而我并没有放弃,在 Editor –> General 中看到一项叫 Enable Drag’n’Drop functionality in Editor ,把这个项前边的勾勾去掉后,发现奇迹般的貌似成功了,好吧,运气不错。。

    \n

    开门红~

    \n
    \n

    2017-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处理不加空格的模板。

    \n

    操作:

    \n
    app.jinja_env.variable_start_string = '{{ '
    app.jinja_env.variable_end_string = ' }}'
    \n

    就酱~

    \n

    我这个项目中还使用了flask-bootstrap作为模板,不幸的是,flask-bootstrap使用的大括号都没加空格,导致页面渲染时出现问题。所以我将flask-bootstrap源码进行了修改,安装时,只要用我的数据源安装即可git+https://github.com/Panmax/flask-bootstrap.git

    \n","categories":["源码"],"tags":["源码","flask","Jinja2","Vue.js"]},{"title":"记录第一次玩阿里云","url":"/2015/%E8%AE%B0%E5%BD%95%E7%AC%AC%E4%B8%80%E6%AC%A1%E7%8E%A9%E9%98%BF%E9%87%8C%E4%BA%91/","content":"

    看到阿里云的美国硅谷区的ECS正在高活动,而且最近正好想学习学习Linux服务器相关的只是,所以就购入了一部。

    \n

    我选择的是1G内存,1G CPU,按流量计费。

    \n

    支付宝付完款后,居然没有自动进入完成付款页面。。。

    \n

    首先进入系统后,我先更改了root密码,因为初始化设置root密码时候要求有大小写。命令:sudo passwd root,然后按照要求输入两遍密码就好了。

    \n\n

    为了不直接使用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!来进行保存。

    \n

    执行< /etc/shells grep zsh后发现ubuntu没有自带zsh,所以又进行了zsh的安装:sudo apt-get install zsh,之前要需要先安装git:sudo apt-get install git

    \n

    设置登录时就使用zshchsh -s /bin/zsh jiapan

    \n

    然后为了不折腾zsh,直接安装了oh-my-zsh:
    sh -c "$(wget https://raw.github.com/robbyrussell/oh-my-zsh/master/tools/install.sh -O -)"

    \n

    弱弱的说一句,硅谷区下载国外的资源真心快。。。

    \n

    按照池建强的教程,进行了一些zsh简单的配置:http://macshuo.com/?p=676

    \n

    本来想直接安装virtualenvwrapper结果发现,python原生不带pip,所以进行pip的安装:

    \n
      \n
    1. 先从官网把安装源文件下载下来:curl "https://bootstrap.pypa.io/get-pip.py" -o "get-pip.py"
    2. \n
    3. 安装 sudo python get-pip.py
    4. \n
    \n

    发现curl也没装。。。所以先安装curl。

    \n
      \n
    1. sudo apt-get update
    2. \n
    3. sudo apt-get install curl
    4. \n
    \n

    终于可以安装virtualenvwrapper了:sudo pip install virtualenvwrapper

    \n

    安装完成后,将下边内容放在~/.bashrc

    \n
    # where to store our virtual envs
    export WORKON_HOME=$HOME/virtenvs
    # where projects will reside
    export PROJECT_HOME=$HOME/Projects-Active
    # where is the virtualenvwrapper.sh
    source $HOME/.local/bin/virtualenvwrapper.sh
    \n

    然后执行source ~/.zshrc

    \n

    安装完成!然后就可以创建虚拟环境搞Python开发了~

    \n

    今天就到这里。。。

    \n","categories":["Linux"],"tags":["Linux"]},{"title":"explore flask 配置","url":"/2015/%E9%85%8D%E7%BD%AE/","content":"

    配置

    原文地址:https://exploreflask.com/configuration.html
    \"\"

    \n

    当你正在学习Flask时,配置看起来很简单。你只需要在config.py中定义一些变量所以工作就完成了。当你不得不为了生产环境的应用管理配置时,这简单就开始消失了。你可能需要保护你的密钥或者在不同的环境下使用不同的配置(例如开发和生产环境)。在这一章我们将介绍一些Flask先进的功能来更简单的管理配置。

    \n\n

    简单的情况

    一个简单的应用可能不需要任何复杂的功能。你可能只需要放置config.py在你代码库的根目录下并且在app.py或者yourapp/__init__.py中加载它。

    \n

    这个config.py问卷需要每行包含一个变量赋值。当你的app初始化后,这些在config.py中的变量用于配置Flask并且它们增加通过app.config字典的访问方式。例如app.config["DEBUG"]

    \n
    # app.py or app/__init__.py
    from flask import Flask

    app = Flask(__name__)
    app.config.from_object('config')

    # 现在我们能够通过app.config[\"变量名\"]来访问配置变量。
    \n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n
    变量描述
    DEBUGtodo…
    SECRET_KEYtodo…
    BCRYPT_LEVELtodo…
    \n
    !警告\n请确定在生产环境下`DEBUG`设置为`False`。不然将会允许用户在你的服务器上运行任何Python代码。\n

    实例文件夹

    有时你需要定义包含敏感信息的配置变量。我们想将这些变量从config.py分离出来并且将他们保存在代码库之外。你可能要隐藏保密的东西比如数据库密码和API密钥或者给机器定义详细的变量。为了让这容易,Flask给我们提供了一个叫实例文件夹的功能。实例文件夹是代码库根目录的子目录并且为这个应用实例包含了一个特殊的配置文件。我们不想在版本控制中提交它。

    \n
    config.py
    requirements.txt
    run.py
    instance/
    \tconfig.py
    yourapp/
    \t__init__.py
    \tmodels.py
    \tviews.py
    \ttemplates/
    \tstatic/
    \n

    使用实例文件夹

    为了加载来自实例文件夹的配置变量,我们使用app.config.from_pyfile()。当我们创建我们的应用并用Flask()调用时,如果我们设置instance_relative_config=Trueapp.config.from_pyfile()将会通过instance/directory加载指定的文件。

    \n
    # app.py or app/__init__.py

    app = Flask(__name__, instance_relative_config=True)
    app.config.from_object('config')
    app.config.from_pyfile('config.py')
    \n

    密钥

    实例文件夹的私有本质为定义密钥而不想暴露在版本库中提供了很好的候选。这些可能包含你应用的密钥或者第三方API的密钥。如果你的应用是开源的这尤其重要或者也许会在未来某一时刻开源。我们通常想让其他用户或者贡献者使用他们自己的密钥。

    \n
    # instance/config.py

    SECRET_KEY = 'Sm9obiBTY2hyb20ga2lja3MgYXNz'
    STRIPE_API_KEY = 'SmFjb2IgS2FwbGFuLU1vc3MgaXMgYSBoZXJv'
    SQLALCHEMY_DATABASE_URI= \\
    \"postgresql://user:TWljaGHFgiBCYXJ0b3N6a2lld2ljeiEh@localhost/databasename\"
    \n

    较小的环境基础配置

    如果你的生产环境和开发环境之间的不同很小,你可能想使用你的实例文件夹处理配置的改变。定义在instance/config.py文件中的变量能够覆盖config.py中的变量。你只需要在app.config.from_object()之后调用app.config.from_pyfile()。利用这个方法是改变你的应用在不同机器配置的一种方式。

    \n
    # config.py

    DEBUG = False
    SQLALCHEMY_ECHO = False


    # instance/config.py
    DEBUG = True
    SQLALCHEMY_ECHO = True
    \n

    在生产中,我们将离开列表中的值,在instance/config.py之外,并且它将会回落到config.py定义的值。

    \n

    配置基于环境变量

    实例文件夹不应该在版本控制中。这意味着你将无法跟踪你实例配置的变化。这可能对于一两个值来说不是问题,但是如果你在不同环境中(生产、升级、开发等)有微调的配置,你不想冒险失去这些。

    \n

    Flask给我们基于环境变量的值去选择一个配置文件来加载的能力。这意味着我们能有多个配置文件在我们的代码库中并且总是加载正确的那个。每次我们有几个不同的配置文件,我们能够移动他们到他们自己的config目录中。

    \n
    requirements.txt
    run.py
    config/
    \t__init__.py # Empty, just here to tell Python that it's a package.
    \tdefault.py
    \tproduction.py
    \tdevelopment.py
    \tstaging.py
    instance/
    \tconfig.py
    yourapp/
    \t__init__.py
    \tmodels.py
    \tviews.py
    \tstatic/
    \ttemplates/
    \n

    在这个列表中我们有少量不同的配置文件。

    \n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n
    文件介绍
    config/default.py
    config/development.py
    config/production.py
    config/staging.py
    \n

    为了决定加载哪个配置文件,我们将调用app.config.from_envvar()

    \n
    # yourapp/__init__.py

    app = Flask(__name__, instance_relative_config=True)

    # Load the default configuration
    app.config.from_object('config.default')

    # Load the configuration from the instance folder
    app.config.from_pyfile('config.py')

    # Load the file specified by the APP_CONFIG_FILE environment variable
    # Variables defined here will override those in the default configuration
    app.config.from_envvar('APP_CONFIG_FILE')
    \n

    环境变量的值应该是配置文件的绝对路径。

    \n

    我们如何设置这个环境变量,取决于我们的应用正在运行的平台。如果我们运行在一个普通的Linux服务器上,我们能够设置一个shell脚本来设置我们的环境变量并且运行run.py

    \n
    # start.sh

    APP_CONFIG_FILE=/var/www/yourapp/config/production.py
    python run.py
    \n

    start.sh在每个环境中是唯一的,所以它应该离开版本控制。在Herok中,我们想要使用Heroku工具设置环境变量。相同的思路应用于其他PaaS平台上。

    \n

    总结

      \n
    • 一个简单的应用可能只需要一个配置文件:config.py
    • \n
    • 实例文件夹能够帮助我们隐藏保密的配置值。
    • \n
    • 实例文件夹能够用于应用的配置之后为了特定的环境。
    • \n
    • 我们应该使用环境变量并且为更复杂的基于环境的配置使用app,config.from_envvar()
    • \n
    \n","categories":["exploreflask"],"tags":["flask"]},{"title":"随机从一个数组中取出n个元素打乱顺序构成新的数组","url":"/2016/%E9%9A%8F%E6%9C%BA%E4%BB%8E%E4%B8%80%E4%B8%AA%E6%95%B0%E7%BB%84%E4%B8%AD%E5%8F%96%E5%87%BAn%E4%B8%AA%E5%85%83%E7%B4%A0%E5%B9%B6%E6%89%93%E4%B9%B1%E9%A1%BA%E5%BA%8F%E6%9E%84%E6%88%90%E6%96%B0%E7%9A%84%E6%95%B0%E7%BB%84/","content":"

    今天要求实现一个功能是从被推荐的用户中,随机取出n个用户,并打乱顺序返回。

    \n

    看了看random模块刚好有这种功能的实现,所以就直接拿来用了。

    \n

    所有被推荐用户的列表为:recommend_users,已经确定的是,这个列表的长度一定大于n。

    \n

    我们需要将结果保存在recommend_user_list中。

    \n

    首先,从这个列表中随机取出n个元素:

    \n
    import random
    recommend_user_list = random.sample(recommend_users, n)
    \n

    这个方法是从recommend_users列表中按顺序随机取出n个元素,因为我们需要打乱顺序,所以还需要调用另一个方法。

    \n
    # 将这个数组打乱顺序
    random.shuffle(recommend_user_list)
    \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

    update at 2017-04-12

    \n
    \n

    逻辑结构:指数据对象中数据元素之间的相互关系

    \n
    1. 集合结构\n2. 线性结构\n3. 树形结构\n4. 图形结构\n

    物理结构:指数据的逻辑结构在计算机中的存储形式

    \n
    1. 顺序存储结构\n2. 链式存储结构\n

    逻辑结构是面相问题的,物理结构是面相计算机的,基本目标就是将数据及其逻辑关系存储到计算机的内存中。

    \n
    \n
    \n

    update at 2017-04-13

    \n
    \n

    算法具有五个基本特性:输入、输出、有穷性、确定性和可行性

    \n

    好的算法应该具有正确性、可读性、健壮性、高效率和低存储量的特征

    \n

    判断一个算法的效率时,函数中的常数和其他次要项常常可以忽略,而更应该关注主项(最高阶项)的阶数

    \n
    \n
    \n

    update at 2017-04-17

    \n
    \n

    推到大 O 阶:

    \n
      \n
    1. 用常数 1 取代运行时间中的所有加法常数。
    2. \n
    3. 在修改后的运行次数函数中,只保留最高阶项。
    4. \n
    5. 如果最高阶项存在且不是 1,则去除与这个项相乘的常数。
    6. \n
    \n

    得到的结果就是大 O 阶。

    \n
    \n
    \n

    update at 2017-04-18

    \n
    \n

    线性表顺序存储结构需要三个属性:

    \n
      \n
    1. 存储空间的起始位置
    2. \n
    3. 线性表的最大存储容量
    4. \n
    5. 线性表的当前长度
    6. \n
    \n

    线性表顺序存储结构的优缺点:

    \n

    优点:

    \n
      \n
    • 无需为表示表中元素之间的逻辑关系而增加额外的存储空间
    • \n
    • 可以快速地存取表中任一位置的元素
    • \n
    \n

    缺点:

    \n
      \n
    • 插入和删除操作需要移动大量元素
    • \n
    • 当线性表长度变化较大时,难以确定存储空间的容量
    • \n
    • 造成存储空间的碎片
    • \n
    \n
    \n
    \n

    update at 2017-04-19

    \n
    \n

    链式结构

    \n

    为了表示每个数据元素 ai 与其直接后继数据元素 ai+1 之间的逻辑关系,对数据元素 ai 来说,除了存储其本身的信息之外,还需存储一个指示其直接后继的信息(即直接后继的存储位置)

    \n

    把存储数据元素信息的域称为数据域,把存储直接后继位置的域成为指针域。指针域中存储的信息称做指针或链。这两部分信息组成数据元素 ai 的存储映像,称为结点(Node)

    \n

    链表中第一个节点的存储位置叫做头指针

    \n

    头指针

    \n
      \n
    • 头指针是指链表指向第一个节点的指针,若链表有头结点,则是指向投结点的指针
    • \n
    • 头指针具有标识作用,所以常用头指针冠以链表的名字
    • \n
    • 无论链表是否为空,头指针均不为空。头指针是链表的必要元素
    • \n
    \n

    头结点

    \n
      \n
    • 头结点是为了操作的统一和方便而设立的,放在第一元素的结点之前,其数据域一般无意义(也可以存放链表的长度)
    • \n
    • 有了头结点,对在第一元素结点前插入结点和删除第一节点,其操作与其他节点的操作就统一了
    • \n
    • 头结点不一定是链表必须要素
    • \n
    \n
    \n
    \n

    update at 2017-04-20

    \n
    \n

    3.11 顺序结构与单链表结构优缺点

    存储分配方式

    \n
      \n
    • 顺序存储结构用一段连续的存储单元依次存储线性表的数据元素
    • \n
    • 单链表采用链式存储结构,用一组任意的存储单元存放线性表的元素
    • \n
    \n

    时间性能

    \n

    查找

    \n
      \n
    • 顺序存储结构O(1)
    • \n
    • 单链表O(n)
    • \n
    \n

    插入和删除

    \n
      \n
    • 顺序存储结构需要平均移动表长一半的元素,时间为O(n)
    • \n
    • 单链表在找出某位置的指针后,插入和删除时间为O(1)
    • \n
    \n

    空间性能

    \n
      \n
    • 顺序存储结构需预分配存储空间,分大了浪费,分小了容易发生上溢
    • \n
    • 单链表不需要分配存储空间,只要有就可以分配,元素个数也不受限制
    • \n
    \n

    结论

    \n
      \n
    • 若线性表需要频繁查找,很少进行插入和删除操作时,宜采用顺序存储结构。若需要频繁插入和删除时,宜采用单链表结构
    • \n
    • 当线性表中的元素个数变化较大或者根本不知道有多大时,最好用单链表结构,这样可以不需要考虑存储空间的大小问题。而如果事先知道线性表的大致长度,用顺序存储结构效率会高很多
    • \n
    \n

    3.14 双向链表

    线性表的双向链表存储结构

    \n
    typedef struct DulNode
    {
    ElemType data;
    struct DuLNode *prior;
    struct DuLNode *next;
    } DulNode, *DuLinkList;
    \n

    双向链表插入元素

    \n

    \"\"

    \n
    s -> prior = p
    s -> next = p -> next
    p -> next -> prior = s
    p -> next = s
    \n

    双向链表删除元素

    \n

    \"\"

    \n
    p -> prior -> next = p -> next;
    p -> next -> prior = p -> prior
    free(p);
    \n

    3.15 回顾总结

    线性表的两种结构

    \n

    \"\"

    \n

    4.2 栈的定义

    \n

    栈(stack)是限定仅在表尾进行插入和删除操作的线性表。

    \n
    \n

    把允许插入和删除的一端称为栈顶(top),另一端称为栈底(bottom)

    \n

    不含任何数据元素的栈称为空栈,栈又称为后进先出(Last In First Out)的线性表,简称 FIFO 结构。

    \n

    栈的插入操作叫作进栈,也称压栈、入栈。

    \n

    栈的删除操作叫做出栈。

    \n

    4.9 栈的应用 – 四则运算表达式求值

    中缀表达式转后缀表达式:

    \n

    如:9+(3-1)3+10/2 -> 9 3 1 - 3 + 10 2 / +

    \n

    规则:从左到右遍历中缀表达式的每个数字和符号,若是数字就输出,即成为后缀表达式的一部分;若是符号,则判断其与栈顶符号的优先级,是有括号或优先级低于栈顶符号(乘除优先加减)则栈顶元素依次出栈并输出,并将当前符号进栈,一直到最终输出后缀表达式为止。

    \n

    4.10 队列的定义

    \n

    队列(queue)是只允许在一段进行插入操作,而在另一端进行删除操作的线性表。

    \n
    \n

    队列是一种先进先出(First In First Out)的线性表,简称 FIFO。允许插入的一端称为队尾,允许删除的一端称为队头。

    \n

    4.12.2 循环队列定义

    循环队列满时没我们有两种办法来判断:

    \n
      \n
    • 办法一是设置一个标志变量 flag,当 front == rear,且 flag == 0 时为队列空,当 front == rear,且 flag = 1 时为队列满。
    • \n
    • 办法二是当前队列空时,条件就是 front = rear,当队列满时,我们修改其条件,保留一个元素空间。也就是说队列满时,数组中还有一个空闲单元。如图所示,我们就认为此队列已经满了。
    • \n
    \n

    \"\"

    \n

    第二种方法,队列满的条件是 (rear+1) % QueueSize == front

    \n

    计算队列长度公式:(rear-front+Queue) % QueueSize

    \n

    4.14 总结回顾

    对于栈来说,如果是两个相同数据类型的栈,则可以用数组的两端作栈底的方法来让两个栈共享数据,这就可以最大化地利用数组的空间。

    \n

    对于队列来说,为了避免数组插入和删除时需要移动数据,于是就引入了循环队列,使得队头和队尾可以在数组中循环变化。解决了移动数据的时间损耗,使得本来插入和删除时间是 O(n) 的时间复杂度变成了 O(1)。

    \n

    他们也都可以通过链式存数结构来实现。

    \n

    5.2 串的定义

    串(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)

    \n

    这种模式刚开始没有问题,后来新鲜增加了可以筛选性别的需求。因为队列中所有性别都是混在一起的,所以每次从队列中取出数据后,需要把Photo实体取出来,然后进行性别过滤,把过滤出来的结果再返回给客户端。如果筛选结果后发现数量不足(通常是20),就重新从库中重新查询,拿出前2000张,过滤掉我要的性别(因为我们用的leancloud平台,他们不支持关联查询,我们的photo不记录性别,需要先取出后再通过user才能知道性别),再过滤掉我看过的,能找到就返回,找不到就算了。假如我把筛选改为女,然后我看啊看,看啊看,早晚我会把队列中的女性照片看完。因为我们只查询前2000张,所以后边的照片我根本看不到,除非等着有女性用户新发照片。而且看的照片越多,查询速度越慢。

    \n

    下边讲一下优化的方法

    \n\n

    这个优化是基于假设用户很少切换性别的基础上进行的:

    \n
      \n
    1. 我为照片表中的数据新增了sex列,为每张照片标记了性别(和发布者性别相同,实为下策略,不过没办法。。。)
    2. \n
    3. 每个用户队列中只保存他所选择性别的照片(全部、男性、女性)
    4. \n
    5. cache中为每个用户保存上次选择的性别 cache:feed:last:sex:u_id,并且记录上次查询到最后那张照片的ID cache:feed:last:photo:p_id
    6. \n
    7. 如果用户再次请求,并且性别不变,那么使用上次查询到的那个照片id继续往后查询m张,因为照片表中有了性别,所以查询效率提升很多。如果中间满足条件的张数大于n,停止查询,并记录最后张ID。
    8. \n
    9. 如果用户切换性别,重新开始查询,更新用户最后一次查询的性别,记录最后查询到的照片ID。从头重新查询。
    10. \n
    11. 在有用户创建新照片以后,需要发通知给offline(redis的发布/订阅实现),之前只发送照片id,现在改为发送照片id和照片性别。
    12. \n
    13. offline收到通知后,将这张照片插入到符合性别和不进行性别过滤的那些用户队列里。
    14. \n
    \n

    这样做之后性能提升了很多很多~

    \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 块多。打算迁移完之后改成两个容器实例。

    \n

    但是在迁移中发现一个问题,当过一会不访问服务后,再次访问时会出现 MySQL server has gone away 的错误,在 SAE 提供的数据库中用 SHOW VARIABLES; 检查了下,SAE 给数据库配置的 wait_timeout 是 300 秒,我之前阿里云上的数据库没有改配置,所以默认为 8 小时,而且 SAE 数据库 的这个值是不允许修改的,所以既然无法改变环境,就来适应环境吧

    \n

    尝试了很多解决办法,比如 配置SQLALCHEMY_COMMIT_ON_TEARDOWN=True 和 在每次请求完成后关闭 db 的 session 都没有解决问题,再次阅读文档时看到了这个配置:SQLALCHEMY_POOL_RECYCLE,作用是设置多少秒后回收连接,在使用 MySQL 时是必须设置的。如果不提供值,默认为 2 小时,而我之前的数据库默认 wait_timeout 为 8 小时,所以一直没有出过问题。我在我的配置文件中,将这个值设置为 280 秒(小于 300 秒),最终解决了问题。

    \n

    最后帖一下SQLALCHEMY_POOL_RECYCLE参数的原文解释:

    \n
    \n

    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"},{"title":"程序员需要知道的缩写和专业名词","url":"/2018/Awsome-Abbreviation/","content":"
    \n

    程序员的世界里充斥着很多的专业名词和英文缩写,我打算对一些常见的词汇进行一个汇总,同时会在 GitHub 上进行同步:https://github.com/Panmax/Awsome-Programmer-Abbreviation,欢迎 PR。

    \n
    \n

    英文缩写

    API

    应用程序接口(英语:Application Programming Interface,简称:API),又称为应用编程接口,就是软件系统不同组成部分衔接的约定。由于近年来软件的规模日益庞大,常常需要把复杂的系统划分成小的组成部分,编程接口的设计十分重要。程序设计的实践中,编程接口的设计首先要使软件系统的职责得到合理划分。良好的接口设计可以降低系统各部分的相互依赖,提高组成单元的内聚性,降低组成单元间的耦合程度,从而提高系统的维护性和扩展性。

    \n

    ACID

    ACID,是指数据库管理系统(DBMS)在写入或更新资料的过程中,为保证事务(transaction)是正确可靠的,所必须具备的四个特性:原子性(atomicity,或称不可分割性)、一致性(consistency)、隔离性(isolation,又称独立性)、持久性(durability)。

    \n

    AJAX

    AJAX即“Asynchronous JavaScript and XML”(异步的 JavaScript 与 XML 技术),指的是一套综合了多项技术的浏览器端网页开发技术。

    \n

    CAS

      \n
    1. 比较并交换(compare and swap, CAS),是原子操作的一种,可用于在多线程编程中实现不被打断的数据交换操作,从而避免多线程同时改写某一数据时由于执行顺序不确定性以及中断的不可预知性产生的数据不一致问题。 该操作通过将内存中的值与指定数据进行比较,当数值一样时将内存中的数据替换为新的值。
    2. \n
    3. 集中式认证服务(英语:Central Authentication Service,缩写CAS)是一种针对万维网的单点登录协议。它的目的是允许一个用户访问多个应用程序,而只需提供一次凭证(如用户名和密码)。它还允许web应用程序在没有获得用户的安全凭据(如密码)的情况下对用户进行身份验证。“CAS”也指实现了该协议的软件包。
    4. \n
    \n

    JPA

    JPA 是 Java Persistence API 的简称,中文名 Java 持久层 API,是 JDK 5.0 注解或 XML 描述对象-关系表的映射关系,并将运行期的实体对象持久化到数据库中。

    \n

    JSON

    JSON(JavaScript Object Notation)是一种轻量级的数据交换语言,以文字为基础,且易于让人阅读。尽管 JSON 是 Javascript 的一个子集,但JSON是独立于语言的文本格式,并且采用了类似于 C语言 家族的一些习惯。

    \n

    POJO

    POJO(Plain Ordinary Java Object)简单的 Java 对象,实际就是普通 Java Beans。使用 POJO 名称是为了避免和 EJB 混淆起来,而且简称比较直接。其中有一些属性及其 getter setter 方法的类,没有业务逻辑,有时可以作为VO(Value Object) 或 DTO(Data Transform Object) 来使用。当然,如果你有一个简单的运算属性也是可以的,但不允许有业务方法,也不能携带有 connection 之类的方法。

    \n

    DSL

    领域专用语言(Domain Specific Language/DSL),其基本思想是「求专不求全」,不像通用目的语言那样目标范围涵盖一切软件问题,而是专门针对某一特定问题的计算机语言。

    \n

    GC

    在计算机科学中,垃圾回收(英语:Garbage Collection,缩写为GC)是一种自动的内存管理机制。当一个电脑上的动态内存不再需要时,就应该予以释放,以让出内存,这种内存资源管理,称为垃圾回收。垃圾回收器可以让程序员减轻许多负担,也减少程序员犯错的机会。垃圾回收最早起源于LISP语言。目前许多语言如 Smalltalk、Java、C# 和 D 语言都支持垃圾回收器。

    \n

    DML

    数据操纵语言(Data Manipulation Language, DML)是 SQL 语言中,负责对数据库对象运行数据访问工作的指令集,以 INSERT、UPDATE、DELETE 三种指令为核心,分别代表插入、更新与删除,是开发以数据为中心的应用程序必定会使用到的指令,因此有很多开发人员都把加上SQL的SELECT语句的四大指令以“CRUD”来称呼。

    \n

    DDL

    数据定义语言(Data Definition Language,DDL)是 SQL 语言集中负责数据结构定义与数据库对象定义的语言,由 CREATE、ALTER 与 DROP 三个语法所组成,最早是由Codasyl(Conference on Data Systems Languages)数据模型开始,现在被纳入 SQL 指令中作为其中一个子集。

    \n

    DI

    Dependency Injection,依赖注入。在软件工程中,依赖注入是种实现控制反转用于解决依赖性设计模式。一个依赖关系指的是可被利用的一种对象(即服务提供端) 。依赖注入是将所依赖的传递给将使用的从属对象(即客户端)。该服务是将会变成客户端的状态的一部分。 传递服务给客户端,而非允许客户端来建立或寻找服务,是本设计模式的基本要求。

    \n

    DNS

    域名系统(英文:Domain Name System)是互联网的一项服务。它作为将域名和IP地址相互映射的一个分布式数据库,能够使人更方便地访问互联网。DNS使用TCP和UDP端口53。当前,对于每一级域名长度的限制是63个字符,域名总长度则不能超过253个字符。

    \n

    GUI

    图形用户界面(Graphical User Interface)是指采用图形方式显示的计算机操作用户界面。与早期计算机使用的命令行界面相比,图形界面对于用户来说在视觉上更易于接受。

    \n

    HTTP

    超文本传输协议(英文:HyperText Transfer ProtocolP)是一种用于分布式、协作式和超媒体信息系统的应用层协议。HTTP是万维网的数据通信的基础。

    \n

    IOC

    控制反转(Inversion of Control,缩写为IoC),是面向对象编程中的一种设计原则,可以用来减低计算机代码之间的耦合度。其中最常见的方式叫做依赖注入(Dependency Injection,简称DI),还有一种方式叫“依赖查找”(Dependency Lookup)。通过控制反转,对象在被创建的时候,由一个调控系统内所有对象的外界实体,将其所依赖的对象的引用传递给它。也可以说,依赖被注入到对象中。

    \n

    JWT

    JSON Web Token(JWT)是一个非常轻巧的规范。这个规范允许我们使用JWT在用户和服务器之间传递安全可靠的信息,特别适用于分布式站点的单点登录(SSO)场景。

    \n

    LDAP

    轻型目录存取协定(英文:Lightweight Directory Access Protocol)是一个开放的,中立的,工业标准的应用协议,通过IP协议提供访问控制和维护分布式信息的目录信息。

    \n

    MVC

    MVC模式(Model–view–controller)是软件工程中的一种软件架构模式,把软件系统分为三个基本部分:模型(Model)、视图(View)和控制器(Controller)。MVC 模式的目的是实现一种动态的程序设计,使后续对程序的修改和扩展简化,并且使程序某一部分的重复利用成为可能。除此之外,此模式通过对复杂度的简化,使程序结构更加直观。

    \n

    MVP

    Model-view-presenter,简称MVP,是电脑软件设计工程中一种对针对MVC模式,再审议后所延伸提出的一种软件设计模式。被广范用于便捷自动化单元测试和在呈现逻辑中改良分离关注点(separation of concerns)。

    \n

    MVVM

    MVVM(Model–view–viewmodel)是一种软件架构模式,有助于将图形用户界面的开发与业务逻辑或后端逻辑(数据模型)的开发分离开来,这是通过置标语言或 GUI 代码实现的。

    \n

    OLAP

    联机分析处理(英语:On-Line Analytical Processing),是一套以多维度方式分析数据,而能弹性地提供积存(英语:Roll-up)、下钻(英语:Drill-down)、和透视分析(英语:pivot)等操作,呈现集成性决策信息的方法,多用于决策支持系统、商务智能或数据仓库。其主要的功能,在于方便大规模数据分析及统计计算,对决策提供参考和支持。与之相区别的是联机交易处理(OLTP)。

    \n

    SQL

    SQL(结构化查询语言)是一种特定目的程序语言,用于管理关系数据库管理系统(RDBMS),或在关系流数据管理系统(RDSMS)中进行流处理。

    \n

    SPA

    单页 Web 应用(single page web application),就是只有一张 Web 页面的应用,是加载单个 HTML 页面并在用户与应用程序交互时动态更新该页面的 Web 应用程序。

    \n

    SOA

    面向服务的体系结构(英语:service-oriented architecture)并不特指一种技术,而是一种分散式运算的软件设计方法。软件的部分组件(呼叫者),可以透过网络上的通用协定呼叫另一个应用软件元件执行、运作,让呼叫者获得服务。SOA原则上采用开放标准、与软件资源进行交互并采用表示的标准方式。因此应能跨越厂商、产品与技术。一项服务应视为一个独立的功能单元,可以远端存取并独立执行与更新,例如在线上线查询信用卡账单。

    \n

    SOAP

    SOAP(原为Simple Object Access Protocol的首字母缩写,即简单对象访问协议)是交换数据的一种协议规范,使用在计算机网络Web服务(web service)中,交换带结构信息。SOAP为了简化网页服务器(Web Server)从XML数据库中提取数据时,节省去格式化页面时间,以及不同应用程序之间按照HTTP通信协议,遵从XML格式执行资料互换,使其抽象于语言实现、平台和硬件。

    \n

    NoSQL

    NoSQL 是对不同于传统的关系数据库的数据库管理系统的统称。

    \n

    XML

    可扩展标记语言(英语:eXtensible Markup Language,简称:XML),是一种标记语言。标记指计算机所能理解的信息符号,通过此种标记,计算机之间可以处理包含各种信息的文章等。如何定义这些标记,既可以选择国际通用的标记语言,比如HTML,也可以使用像XML这样由相关人士自由决定的标记语言,这就是语言的可扩展性。XML是从标准通用标记语言(SGML)中简化修改出来的。它主要用到的有可扩展标记语言、可扩展样式语言(XSL)、XBRL和XPath等。

    \n
    \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

    B/S结构

    浏览器-服务器(Browser/Server)结构,与C/S结构不同,其客户端不需要安装专门的软件,只需要浏览器即可,浏览器通过Web服务器与数据库进行交互,可以方便的在不同平台下工作;服务器端可采用高性能计算机,并安装Oracle、Sybase、Informix等大型数据库。B/S结构简化了客户端的工作,它是随着Internet技术兴起而产生的,对C/S技术的改进,但该结构下服务器端的工作较重,对服务器的性能要求更高。

    \n

    C/S结构

    主从式架构 (英语:Client–server model) 也称客户端-服务器(Client/Server)架构、C/S架构,是一种网络架构,它把客户端 (Client) (通常是一个采用图形用户界面的程序)与服务器 (Server) 区分开来。每一个客户端软件的实例都可以向一个服务器或应用程序服务器发出请求。有很多不同类型的服务器,例如文件服务器、游戏服务器等。

    \n

    Web服务

    根据W3C的定义,Web服务(Web service)应当是一个软件系统,用以支持网络间不同机器的互动操作。网络服务通常是许多应用程序接口(API)所组成的,它们透过网络,例如国际互联网(Internet)的远程服务器端,执行客户所提交服务的请求。

    \n"},{"title":"《掌控习惯》——读后行动","url":"/2022/Atomic-Habits-action/","content":"

    今天是六一儿童节,祝各位大宝宝小宝宝儿童节快乐🌸

    \n

    上周读完了 《掌控习惯》 这本书,里边总结了四个定律来培养好习惯或者戒除不良习惯,我列出这四个定律和每个定律给出的几个主要方案,在每个方案下写上自己可以将此应用在哪些地方,同时会写一些自己的感想。

    \n

    第一定律:让它显而易见

    填写习惯「积分卡」;记下你当前的习惯并留意他们

    +早上不赖床
    =洗脸刷牙上厕所
    +洗漱期间听红楼梦、播客
    -上厕所时候玩游戏
    +上厕所时候学英语
    +手冲一杯很淡的美式,可以提神、让自己多喝水
    +使用 Things 管理自己的待办事项
    -工作或者看书的空档没有思路时会刷会手机
    -外边走路时会不自主的拿出手机,虽然也不知道要做什么
    =边走路变听播客
    +读书
    =带娃
    +背 Anki
    =看邮件
    -看 Telegram、Twitter
    +每周一次慢跑
    +晚上在 10:30 前做好睡眠准备

    \n

    应用执行意图:「我将于【时间】 在【地点】【行为】。」

      \n
    • 我将于早晚洗漱期间听播客。
    • \n
    • 我将于上下班的地铁上,根据当时的状态(如地铁拥挤情况、自身状况)选择阅读、听课或者听播客。
    • \n
    • 我每天 8:30 前出门,下地铁后步行到公司,下班步行到地铁。
    • \n
    • 我将于每天中午 12:00-14:00 其他人午休这段安静的时间练习写作
    • \n
    \n

    应用习惯叠加:「继【当前习惯】之后,我将会养成【新习惯】。」

    这个我没有想出与我自己相匹配的场景,先从书中抄几条吧(同时会应用在自己身上),等自己有了灵感再补充。

    \n
      \n
    • 当我想买超过 200 元的东西时,我会等 24 小时后再买。
    • \n
    • 电话铃响时,我会深吸一口气,微笑着接电话。
    • \n
    • 每当买一件新物品时,我会将一些旧物品送人会丢弃。
    • \n
    \n

    设计你的环境,让好习惯的提示清晰明了。

    在地铁上读书时我会戴上 AirPods Pro,播放我长期在听的那些钢琴曲,可能是已经听的太长时间吧(2年?),每当听到这些音乐后我很容易就能进入阅读状态,同时 AirPods Pro 的降噪效果也能给我提供一个不那么嘈杂的环境,更容易让我沉浸在阅读中。

    \n

    这里附上我的歌单

    \n

    第一定律反用:让它脱离视线

    降低出现频率。把习惯的提示清除出你所在的环境。

    工作时将手机屏幕扣在桌面上,晚上睡觉前将手机放在卧室充电,十点后就不再玩手机和其他点子设备(Kindle除外),这样可以避免睡前刷手机影响睡眠。

    \n

    第二定律:让它有吸引力

    利用喜好绑定。用你喜好的行为强化你需要的动作

      \n
    • 我要在每天冲完咖啡回工位后冥想一分钟(最近一段时间没有冥想了,要重新培养起来);
    • \n
    • 待补充…
    • \n
    \n

    加入把你喜好的行为视为正常行为的文化群体

    这也是为什么家长喜欢让自己的孩子和那些更优秀的孩子在一块玩的原因,人会相互影响,尤其是那些和我们亲近的人。

    \n

    这也是为什么有时候我们学习一样东西,自学的效率没有报一个班和大家一起学高的原因之一吧(另一个原因是能得到专业的指导),好多人一起学可以创造一种氛围,让你觉得这个事情也并没有那么难。

    \n

    同样和自己志趣相投的一群人一起工作更能保持充足的热情,书中提到「没有什么比群体归属感更能维持一个人做事的动力了」。

    \n

    通过这一节我也知道了我们为什么会在焦虑、无所事事时喜欢刷朋友圈、抖音、淘宝的原因:「当我们不确定改如何做时,我们都会期待得到团体的指导」。我们想看看其他人在做什么,想看看其他人在玩什么,想看看其他人在买什么。

    \n

    我们会从众,希望能被这个社会所接纳,哪怕整个社会都在做的可能是一件不正确的事,我们为了得到认可同样也会选择做这件事。比如最近一段时间的每天一次核酸监测,再过几年再回头来看,我不认为这是正确的。「我们宁愿跟众人一起犯错,也不愿特立独行坚持真理。」

    \n

    创设一种激励仪式。在实施低频行动之前先做一件让你特别喜好的事

    第二定律反用:让它缺乏吸引力

    重新梳理你的思路。罗列出戒除坏习惯带来的益处

      \n
    • 戒除吃饭时喜欢配辣酱的习惯,这可以让我吃的更健康,可以控制食量,也能避免饭后和大量的水,更近一步晚上不至于总上洗手间能有更好的睡眠。恢复正常办公,回到公司后我要把公司冰箱里我的那瓶辣酱扔掉。
    • \n
    • 戒除刷手机的习惯后我会有很多时间做其他更有意义的事情
    • \n
    • 戒除拖延的时间后也压缩出更多的时间做其他事情
    • \n
    • 戒除看到感兴趣的商品脑子一热就下单的习惯后,可以节省一些钱还能让自己生活的空间更简洁。
    • \n
    • 戒除暴饮暴食的习惯后,我可以更好的做好体重管理、健康管理,也不至于总因为吃的过撑而懊恼。
    • \n
    • 晚上不再喝大量饮料(包括牛奶)或吃西瓜类水份糖分过高的水果,这样对健康有利,同时不会再半夜醒来上厕所,提升睡眠质量。
    • \n
    \n

    第三定律:让它简便易行

    减小阻力。减少培养好习惯的步骤

    我现在只在电脑上安装了 Anki,手机上的 Anki 是付费的,价格还不低,就一直没装,我准备今天就购买并安装上手机版。我现在每天做知识回顾的时候都需要使用电脑,无法随时随地的回顾知识,多少有些阻力。

    \n

    备好环境。创造一种有利与未来行为的环境

    我的书包里时刻装着一本书和 Kindle,出门乘坐交通工具或者等人的时候,可以随时拿出来进行阅读,同时书包里还有铅笔、荧光笔,可以随时让我做标记使用。

    \n

    再有,比如我要求自己在周六下午慢跑一次,我在白天或者提前一天就把跑步需要的衣服鞋子准备好,去跑步的概率会更大一些,因为到了那个时间我只需把衣服换好就可以出门跑步,不需要再去想着需要先找衣服、换衣服才能出门。

    \n

    把握好决定性时刻。优化可以产生重大影响的小选择

      \n
    • 路过便利店,没有必需品要买就不要想着进去转一圈
    • \n
    • 如果外出吃饭想吃健康餐,就选择去专门提供健康餐的店
    • \n
    • 待补充…
    • \n
    \n

    利用两分钟准则。在能够锁定你未来行为的技术和物品上有所犹如??

    我现在刚刚开始尝试练习写作,写作一定是一个对未来非常好的投资,每当我需要写点什么的时候,即使自己不想写我也会要求自己坐在电脑前,打开写作工具只写一段就好。

    \n

    想到另一个用途:当我非常想做点杀时间的事情,比如打局游戏、刷会短视频,我会跟自己说先看会书吧,就看两分钟就行。如果两分钟过去心里没那么浮躁可以读下去了就会继续读,如果还是读不下去就按照之前的计划想做什么做点什么。

    \n

    第三定律反用:让它难以施行

    增大阻力。增加实行坏习惯的步骤

    这个方法可以利用在避免浪费太多时间在刷手机上,我将自己容易沉迷的软件收在一个目录中,而不是直接平铺在桌面上,这样在每次打开手机时就不会直接看到它们,同时将能关闭的推送关闭,很多不必要的软件如果没有推送我们是不会主动想到去打开它的,更进一步,我们可以尝试卸载 APP,比如我手机中现在就没有抖音、快手这钟既浪费我时间又会降低我心智的软件。

    \n

    如果我们把手机的面部识别功能关闭,是不是能较少我们划手机的次数?有时候我们打开手机是个无意识行为,看一眼手机随手往上一滑就解锁了,然后人们会顺着这个动作不自主的启动后边一系列的动作。前段时间人们在公共场合都需要戴口罩,在使用面部识别时会比较麻烦,这应该多少也会减少玩手机的次数吧,然而后来 Apple 支持了带口罩识别,「破坏」了这个隐性的好处。我因为有 Apple Watch,所以自始至终都可以在佩戴口罩的情况下解锁 😂

    \n

    利用承诺机制。锁定未来会有利于你的选择项

    我在两周前发了个朋友圈,表示自己又又又要开始减肥了,算是一种对公共的承诺,虽然没有奖惩措施,但也这会给我提供动力,好让我在约定时间内再次在公共面前交上答卷。这样能带来的好处是我能让自己恢复到正常体重,而且能让其他人看到我是一个言必行、行必果,做事靠谱的人。

    \n

    \n

    第四定律:让它令人愉悦

    利用增强法。完成一套习惯后立即奖励自己

    Apple Watch 会在我每次完成计划的运动量后亮出三环的烟花,每月完成运动天数后会奖励一个奖牌。

    \n

    让「无所事事」变得愉快。当避免坏习惯时,设计一种让由此带来的好处显而易见的好处而显而易见的方式

      \n
    • 早上步行到公司的路上可以听播客;
    • \n
    • 在跑步过程中听点有意思的东西;
    • \n
    \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 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 服务订阅清单 | 贾攀的流水账 + + + + + + + + + + + + + + + + +
    +
    + +
    +
    + + +
    + + + +

    贾攀的流水账

    + +
    +

    Panmax's Blog

    +
    + + +
    + + + + + + + + +
    + +
    + +
    +
    + + +
    + + 0% +
    + + +
    +
    +
    + + + + +
    + + + + + +
    +
    + +

    服务订阅清单 +

    + + + +
    + + + + +
    +
    +

    以下是我所订阅的付费服务清单,统计下我一年在这些服务上的花销。

    +
    + +

    Apple Music 家庭版,6人共享:15 / 6 * 12 = 36/年(司机)

    +

    iCloud 2T 家庭版 6人共享:68 / 6 * 12 = 144/年(司机)

    +

    Office 365 家庭版:45/年(乘客,不知道总价)

    +

    YouTube Premium 印区-家庭版:45/年(乘客,不知道总价)

    +

    Surge For Mac x 2 :280/年(乘客)

    +

    Surge For iOS :120/年(乘客)

    +

    主梯子服务(V2Ray+国内中转):420/年

    +

    备用梯子服务(SS + V2Ray + 大厂背景):400/年

    +

    BandwagonHost VPS:1228/年:

    +
      +
    • 1C+1G+20G:$37.59
    • +
    • 2C+1G+22G:$46.99
    • +
    • 1C+512M+10G:$28.19
    • +
    • 2C+2G+40G x 2:$29.88 * 2 = $59.76 (这两个是 dc8 的绝版机器,非常值,18年双11购入)
    • +
    +

    域名8个,平均100/个:800/年

    +

    腾讯云 1C+2G+50G:1000/年

    +

    Mac 剪切板工具 Paste:73/年

    +

    阿里虚拟云主机:58/年

    +

    百度网盘超级会员:263/年

    +

    京东Plus + 爱奇艺会员:98/年

    +

    ProcessOn 一次性买了4年226:57/年

    +

    XMind 2020:388/年

    +
    +

    合计 5397

    再加上购买各种一次性付费的软件或者杂七杂八的费用(比如七牛CDN、云片的短信),一年也有1500的开销,所以这么看来我一年在软件上的花销在 ¥6500+

    + +
    + + + +
    + + + + + + + +
    + +
    + + + +
    + + + + + + + + +
    +
    + +
    + +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tags/2016/index.html b/tags/2016/index.html new file mode 100644 index 0000000000..31a7f2a32b --- /dev/null +++ b/tags/2016/index.html @@ -0,0 +1,442 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Tag: 2016 | 贾攀的流水账 + + + + + + + + + + + + + + + +
    +
    + +
    +
    + + +
    + + + +

    贾攀的流水账

    + +
    +

    Panmax's Blog

    +
    + + +
    + + + + + + + + +
    + +
    + +
    +
    + + +
    + + 0% +
    + + +
    +
    +
    + + +
    + + + + + +
    +
    +
    +

    2016 + Tag +

    +
    + + +
    + 2015 +
    + + + +
    +
    + + + + + + + + +
    + + + + +
    + + + + + + + + +
    +
    + +
    + +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tags/AWS/index.html b/tags/AWS/index.html new file mode 100644 index 0000000000..082210c6b5 --- /dev/null +++ b/tags/AWS/index.html @@ -0,0 +1,442 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Tag: AWS | 贾攀的流水账 + + + + + + + + + + + + + + + +
    +
    + +
    +
    + + +
    + + + +

    贾攀的流水账

    + +
    +

    Panmax's Blog

    +
    + + +
    + + + + + + + + +
    + +
    + +
    +
    + + +
    + + 0% +
    + + +
    +
    +
    + + +
    + + + + + +
    +
    +
    +

    AWS + Tag +

    +
    + + +
    + 2016 +
    + + + +
    +
    + + + + + + + + +
    + + + + +
    + + + + + + + + +
    +
    + +
    + +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tags/BigData-Graph/index.html b/tags/BigData-Graph/index.html new file mode 100644 index 0000000000..9436ae3cfa --- /dev/null +++ b/tags/BigData-Graph/index.html @@ -0,0 +1,442 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Tag: BigData Graph | 贾攀的流水账 + + + + + + + + + + + + + + + +
    +
    + +
    +
    + + +
    + + + +

    贾攀的流水账

    + +
    +

    Panmax's Blog

    +
    + + +
    + + + + + + + + +
    + +
    + +
    +
    + + +
    + + 0% +
    + + +
    +
    +
    + + +
    + + + + + +
    +
    +
    +

    BigData Graph + Tag +

    +
    + + +
    + 2017 +
    + + + +
    +
    + + + + + + + + +
    + + + + +
    + + + + + + + + +
    +
    + +
    + +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tags/BigData/index.html b/tags/BigData/index.html new file mode 100644 index 0000000000..7ba7182d81 --- /dev/null +++ b/tags/BigData/index.html @@ -0,0 +1,442 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Tag: BigData | 贾攀的流水账 + + + + + + + + + + + + + + + +
    +
    + +
    +
    + + +
    + + + +

    贾攀的流水账

    + +
    +

    Panmax's Blog

    +
    + + +
    + + + + + + + + +
    + +
    + +
    +
    + + +
    + + 0% +
    + + +
    +
    +
    + + +
    + + + + + +
    +
    +
    +

    BigData + Tag +

    +
    + + +
    + 2017 +
    + + + +
    +
    + + + + + + + + +
    + + + + +
    + + + + + + + + +
    +
    + +
    + +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tags/CDN/index.html b/tags/CDN/index.html new file mode 100644 index 0000000000..f977860f58 --- /dev/null +++ b/tags/CDN/index.html @@ -0,0 +1,442 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Tag: CDN | 贾攀的流水账 + + + + + + + + + + + + + + + +
    +
    + +
    +
    + + +
    + + + +

    贾攀的流水账

    + +
    +

    Panmax's Blog

    +
    + + +
    + + + + + + + + +
    + +
    + +
    +
    + + +
    + + 0% +
    + + +
    +
    +
    + + +
    + + + + + +
    +
    +
    +

    CDN + Tag +

    +
    + + +
    + 2015 +
    + + + +
    +
    + + + + + + + + +
    + + + + +
    + + + + + + + + +
    +
    + +
    + +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tags/CSDN/index.html b/tags/CSDN/index.html new file mode 100644 index 0000000000..9d857ebb36 --- /dev/null +++ b/tags/CSDN/index.html @@ -0,0 +1,442 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Tag: CSDN | 贾攀的流水账 + + + + + + + + + + + + + + + +
    +
    + +
    +
    + + +
    + + + +

    贾攀的流水账

    + +
    +

    Panmax's Blog

    +
    + + +
    + + + + + + + + +
    + +
    + +
    +
    + + +
    + + 0% +
    + + +
    +
    +
    + + +
    + + + + + +
    +
    +
    +

    CSDN + Tag +

    +
    + + +
    + 2016 +
    + + + +
    +
    + + + + + + + + +
    + + + + +
    + + + + + + + + +
    +
    + +
    + +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tags/Datetime/index.html b/tags/Datetime/index.html new file mode 100644 index 0000000000..71d231000e --- /dev/null +++ b/tags/Datetime/index.html @@ -0,0 +1,462 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Tag: Datetime | 贾攀的流水账 + + + + + + + + + + + + + + + +
    +
    + +
    +
    + + +
    + + + +

    贾攀的流水账

    + +
    +

    Panmax's Blog

    +
    + + +
    + + + + + + + + +
    + +
    + +
    +
    + + +
    + + 0% +
    + + +
    +
    +
    + + +
    + + + + + +
    +
    +
    +

    Datetime + Tag +

    +
    + + +
    + 2015 +
    + + + + + +
    +
    + + + + + + + + +
    + + + + +
    + + + + + + + + +
    +
    + +
    + +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tags/EC2/index.html b/tags/EC2/index.html new file mode 100644 index 0000000000..c27248cc81 --- /dev/null +++ b/tags/EC2/index.html @@ -0,0 +1,442 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Tag: EC2 | 贾攀的流水账 + + + + + + + + + + + + + + + +
    +
    + +
    +
    + + +
    + + + +

    贾攀的流水账

    + +
    +

    Panmax's Blog

    +
    + + +
    + + + + + + + + +
    + +
    + +
    +
    + + +
    + + 0% +
    + + +
    +
    +
    + + +
    + + + + + +
    +
    +
    +

    EC2 + Tag +

    +
    + + +
    + 2016 +
    + + + +
    +
    + + + + + + + + +
    + + + + +
    + + + + + + + + +
    +
    + +
    + +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tags/Effective-Python/index.html b/tags/Effective-Python/index.html new file mode 100644 index 0000000000..4f44c55a60 --- /dev/null +++ b/tags/Effective-Python/index.html @@ -0,0 +1,442 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Tag: Effective Python | 贾攀的流水账 + + + + + + + + + + + + + + + +
    +
    + +
    +
    + + +
    + + + +

    贾攀的流水账

    + +
    +

    Panmax's Blog

    +
    + + +
    + + + + + + + + +
    + +
    + +
    +
    + + +
    + + 0% +
    + + +
    +
    +
    + + +
    + + + + + +
    +
    +
    +

    Effective Python + Tag +

    +
    + + +
    + 2016 +
    + + + +
    +
    + + + + + + + + +
    + + + + +
    + + + + + + + + +
    +
    + +
    + +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tags/GitHub/index.html b/tags/GitHub/index.html new file mode 100644 index 0000000000..d5d6497262 --- /dev/null +++ b/tags/GitHub/index.html @@ -0,0 +1,462 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Tag: GitHub | 贾攀的流水账 + + + + + + + + + + + + + + + +
    +
    + +
    +
    + + +
    + + + +

    贾攀的流水账

    + +
    +

    Panmax's Blog

    +
    + + +
    + + + + + + + + +
    + +
    + +
    +
    + + +
    + + 0% +
    + + +
    +
    +
    + + +
    + + + + + +
    +
    +
    +

    GitHub + Tag +

    +
    + + +
    + 2016 +
    + + + + + +
    +
    + + + + + + + + +
    + + + + +
    + + + + + + + + +
    +
    + +
    + +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tags/IDE/index.html b/tags/IDE/index.html new file mode 100644 index 0000000000..ff6835cd5b --- /dev/null +++ b/tags/IDE/index.html @@ -0,0 +1,442 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Tag: IDE | 贾攀的流水账 + + + + + + + + + + + + + + + +
    +
    + +
    +
    + + +
    + + + +

    贾攀的流水账

    + +
    +

    Panmax's Blog

    +
    + + +
    + + + + + + + + +
    + +
    + +
    +
    + + +
    + + 0% +
    + + +
    +
    +
    + + +
    + + + + + +
    +
    +
    +

    IDE + Tag +

    +
    + + +
    + 2017 +
    + + + +
    +
    + + + + + + + + +
    + + + + +
    + + + + + + + + +
    +
    + +
    + +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tags/JS/index.html b/tags/JS/index.html new file mode 100644 index 0000000000..859c198a7a --- /dev/null +++ b/tags/JS/index.html @@ -0,0 +1,462 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Tag: JS | 贾攀的流水账 + + + + + + + + + + + + + + + +
    +
    + +
    +
    + + +
    + + + +

    贾攀的流水账

    + +
    +

    Panmax's Blog

    +
    + + +
    + + + + + + + + +
    + +
    + +
    +
    + + +
    + + 0% +
    + + +
    +
    +
    + + +
    + + + + + +
    +
    +
    +

    JS + Tag +

    +
    + + +
    + 2016 +
    + + + + + +
    +
    + + + + + + + + +
    + + + + +
    + + + + + + + + +
    +
    + +
    + +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tags/JavaScript/index.html b/tags/JavaScript/index.html new file mode 100644 index 0000000000..bd3e24d853 --- /dev/null +++ b/tags/JavaScript/index.html @@ -0,0 +1,442 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Tag: JavaScript | 贾攀的流水账 + + + + + + + + + + + + + + + +
    +
    + +
    +
    + + +
    + + + +

    贾攀的流水账

    + +
    +

    Panmax's Blog

    +
    + + +
    + + + + + + + + +
    + +
    + +
    +
    + + +
    + + 0% +
    + + +
    +
    +
    + + +
    + + + + + +
    +
    +
    +

    JavaScript + Tag +

    +
    + + +
    + 2016 +
    + + + +
    +
    + + + + + + + + +
    + + + + +
    + + + + + + + + +
    +
    + +
    + +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tags/Jinja2/index.html b/tags/Jinja2/index.html new file mode 100644 index 0000000000..f881225801 --- /dev/null +++ b/tags/Jinja2/index.html @@ -0,0 +1,442 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Tag: Jinja2 | 贾攀的流水账 + + + + + + + + + + + + + + + +
    +
    + +
    +
    + + +
    + + + +

    贾攀的流水账

    + +
    +

    Panmax's Blog

    +
    + + +
    + + + + + + + + +
    + +
    + +
    +
    + + +
    + + 0% +
    + + +
    +
    +
    + + +
    + + + + + +
    +
    +
    +

    Jinja2 + Tag +

    +
    + + +
    + 2016 +
    + + + +
    +
    + + + + + + + + +
    + + + + +
    + + + + + + + + +
    +
    + +
    + +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tags/Linux/index.html b/tags/Linux/index.html new file mode 100644 index 0000000000..69130819cb --- /dev/null +++ b/tags/Linux/index.html @@ -0,0 +1,505 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Tag: Linux | 贾攀的流水账 + + + + + + + + + + + + + + + +
    +
    + +
    +
    + + +
    + + + +

    贾攀的流水账

    + +
    +

    Panmax's Blog

    +
    + + +
    + + + + + + + + +
    + +
    + +
    +
    + + +
    + + 0% +
    + + +
    +
    +
    + + +
    + + + + + +
    +
    +
    +

    Linux + Tag +

    +
    + + +
    + 2016 +
    + + + + + + +
    + 2015 +
    + + + +
    +
    + + + + + + + + +
    + + + + +
    + + + + + + + + +
    +
    + +
    + +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tags/MySQL/index.html b/tags/MySQL/index.html new file mode 100644 index 0000000000..3e92f45b3c --- /dev/null +++ b/tags/MySQL/index.html @@ -0,0 +1,442 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Tag: MySQL | 贾攀的流水账 + + + + + + + + + + + + + + + +
    +
    + +
    +
    + + +
    + + + +

    贾攀的流水账

    + +
    +

    Panmax's Blog

    +
    + + +
    + + + + + + + + +
    + +
    + +
    +
    + + +
    + + 0% +
    + + +
    +
    +
    + + +
    + + + + + +
    +
    +
    +

    MySQL + Tag +

    +
    + + +
    + 2016 +
    + + + +
    +
    + + + + + + + + +
    + + + + +
    + + + + + + + + +
    +
    + +
    + +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tags/MySql/index.html b/tags/MySql/index.html new file mode 100644 index 0000000000..7c63162d18 --- /dev/null +++ b/tags/MySql/index.html @@ -0,0 +1,442 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Tag: MySql | 贾攀的流水账 + + + + + + + + + + + + + + + +
    +
    + +
    +
    + + +
    + + + +

    贾攀的流水账

    + +
    +

    Panmax's Blog

    +
    + + +
    + + + + + + + + +
    + +
    + +
    +
    + + +
    + + 0% +
    + + +
    +
    +
    + + +
    + + + + + +
    +
    +
    +

    MySql + Tag +

    +
    + + +
    + 2016 +
    + + + +
    +
    + + + + + + + + +
    + + + + +
    + + + + + + + + +
    +
    + +
    + +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tags/Python/index.html b/tags/Python/index.html new file mode 100644 index 0000000000..62096414ad --- /dev/null +++ b/tags/Python/index.html @@ -0,0 +1,625 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Tag: Python | 贾攀的流水账 + + + + + + + + + + + + + + + +
    +
    + +
    +
    + + +
    + + + +

    贾攀的流水账

    + +
    +

    Panmax's Blog

    +
    + + +
    + + + + + + + + +
    + +
    + +
    +
    + + +
    + + 0% +
    + + +
    +
    +
    + + +
    + + + + + +
    +
    +
    +

    Python + Tag +

    +
    + + +
    + 2016 +
    + + + + + + + + + + + + + + + + + + + + + +
    +
    + + + + + + + + + +
    + + + + +
    + + + + + + + + +
    +
    + +
    + +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tags/Python/page/2/index.html b/tags/Python/page/2/index.html new file mode 100644 index 0000000000..40d721a306 --- /dev/null +++ b/tags/Python/page/2/index.html @@ -0,0 +1,608 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Tag: Python | 贾攀的流水账 + + + + + + + + + + + + + + + +
    +
    + +
    +
    + + +
    + + + +

    贾攀的流水账

    + +
    +

    Panmax's Blog

    +
    + + +
    + + + + + + + + +
    + +
    + +
    +
    + + +
    + + 0% +
    + + +
    +
    +
    + + +
    + + + + + +
    +
    +
    +

    Python + Tag +

    +
    + + +
    + 2016 +
    + + + + + + + + + + + + + + +
    + 2015 +
    + + + + + +
    +
    + + + + + + + + + +
    + + + + +
    + + + + + + + + +
    +
    + +
    + +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tags/React-Native/index.html b/tags/React-Native/index.html new file mode 100644 index 0000000000..63b0ddd6e5 --- /dev/null +++ b/tags/React-Native/index.html @@ -0,0 +1,442 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Tag: React Native | 贾攀的流水账 + + + + + + + + + + + + + + + +
    +
    + +
    +
    + + +
    + + + +

    贾攀的流水账

    + +
    +

    Panmax's Blog

    +
    + + +
    + + + + + + + + +
    + +
    + +
    +
    + + +
    + + 0% +
    + + +
    +
    +
    + + +
    + + + + + +
    +
    +
    +

    React Native + Tag +

    +
    + + +
    + 2016 +
    + + + +
    +
    + + + + + + + + +
    + + + + +
    + + + + + + + + +
    +
    + +
    + +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tags/VIM/index.html b/tags/VIM/index.html new file mode 100644 index 0000000000..6596e00743 --- /dev/null +++ b/tags/VIM/index.html @@ -0,0 +1,442 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Tag: VIM | 贾攀的流水账 + + + + + + + + + + + + + + + +
    +
    + +
    +
    + + +
    + + + +

    贾攀的流水账

    + +
    +

    Panmax's Blog

    +
    + + +
    + + + + + + + + +
    + +
    + +
    +
    + + +
    + + 0% +
    + + +
    +
    +
    + + +
    + + + + + +
    +
    +
    +

    VIM + Tag +

    +
    + + +
    + 2016 +
    + + + +
    +
    + + + + + + + + +
    + + + + +
    + + + + + + + + +
    +
    + +
    + +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tags/Vue-js/index.html b/tags/Vue-js/index.html new file mode 100644 index 0000000000..1dd56e3715 --- /dev/null +++ b/tags/Vue-js/index.html @@ -0,0 +1,442 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Tag: Vue.js | 贾攀的流水账 + + + + + + + + + + + + + + + +
    +
    + +
    +
    + + +
    + + + +

    贾攀的流水账

    + +
    +

    Panmax's Blog

    +
    + + +
    + + + + + + + + +
    + +
    + +
    +
    + + +
    + + 0% +
    + + +
    +
    +
    + + +
    + + + + + +
    +
    +
    +

    Vue.js + Tag +

    +
    + + +
    + 2016 +
    + + + +
    +
    + + + + + + + + +
    + + + + +
    + + + + + + + + +
    +
    + +
    + +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tags/Web/index.html b/tags/Web/index.html new file mode 100644 index 0000000000..b5050173a8 --- /dev/null +++ b/tags/Web/index.html @@ -0,0 +1,442 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Tag: Web | 贾攀的流水账 + + + + + + + + + + + + + + + +
    +
    + +
    +
    + + +
    + + + +

    贾攀的流水账

    + +
    +

    Panmax's Blog

    +
    + + +
    + + + + + + + + +
    + +
    + +
    +
    + + +
    + + 0% +
    + + +
    +
    +
    + + +
    + + + + + +
    +
    +
    +

    Web + Tag +

    +
    + + +
    + 2016 +
    + + + +
    +
    + + + + + + + + +
    + + + + +
    + + + + + + + + +
    +
    + +
    + +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tags/bootstrap/index.html b/tags/bootstrap/index.html new file mode 100644 index 0000000000..593a213e2f --- /dev/null +++ b/tags/bootstrap/index.html @@ -0,0 +1,442 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Tag: bootstrap | 贾攀的流水账 + + + + + + + + + + + + + + + +
    +
    + +
    +
    + + +
    + + + +

    贾攀的流水账

    + +
    +

    Panmax's Blog

    +
    + + +
    + + + + + + + + +
    + +
    + +
    +
    + + +
    + + 0% +
    + + +
    +
    +
    + + +
    + + + + + +
    +
    +
    +

    bootstrap + Tag +

    +
    + + +
    + 2015 +
    + + + +
    +
    + + + + + + + + +
    + + + + +
    + + + + + + + + +
    +
    + +
    + +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tags/cdn/index.html b/tags/cdn/index.html new file mode 100644 index 0000000000..a7116b9f1f --- /dev/null +++ b/tags/cdn/index.html @@ -0,0 +1,442 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Tag: cdn | 贾攀的流水账 + + + + + + + + + + + + + + + +
    +
    + +
    +
    + + +
    + + + +

    贾攀的流水账

    + +
    +

    Panmax's Blog

    +
    + + +
    + + + + + + + + +
    + +
    + +
    +
    + + +
    + + 0% +
    + + +
    +
    +
    + + +
    + + + + + +
    +
    +
    +

    cdn + Tag +

    +
    + + +
    + 2021 +
    + + + +
    +
    + + + + + + + + +
    + + + + +
    + + + + + + + + +
    +
    + +
    + +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tags/decode/index.html b/tags/decode/index.html new file mode 100644 index 0000000000..1142de6e86 --- /dev/null +++ b/tags/decode/index.html @@ -0,0 +1,442 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Tag: decode | 贾攀的流水账 + + + + + + + + + + + + + + + +
    +
    + +
    +
    + + +
    + + + +

    贾攀的流水账

    + +
    +

    Panmax's Blog

    +
    + + +
    + + + + + + + + +
    + +
    + +
    +
    + + +
    + + 0% +
    + + +
    +
    +
    + + +
    + + + + + +
    +
    +
    +

    decode + Tag +

    +
    + + +
    + 2016 +
    + + + +
    +
    + + + + + + + + +
    + + + + +
    + + + + + + + + +
    +
    + +
    + +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tags/encode/index.html b/tags/encode/index.html new file mode 100644 index 0000000000..2d0c2f2ca5 --- /dev/null +++ b/tags/encode/index.html @@ -0,0 +1,442 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Tag: encode | 贾攀的流水账 + + + + + + + + + + + + + + + +
    +
    + +
    +
    + + +
    + + + +

    贾攀的流水账

    + +
    +

    Panmax's Blog

    +
    + + +
    + + + + + + + + +
    + +
    + +
    +
    + + +
    + + 0% +
    + + +
    +
    +
    + + +
    + + + + + +
    +
    +
    +

    encode + Tag +

    +
    + + +
    + 2016 +
    + + + +
    +
    + + + + + + + + +
    + + + + +
    + + + + + + + + +
    +
    + +
    + +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git "a/tags/feed\346\265\201/index.html" "b/tags/feed\346\265\201/index.html" new file mode 100644 index 0000000000..4c73c11be8 --- /dev/null +++ "b/tags/feed\346\265\201/index.html" @@ -0,0 +1,442 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Tag: feed流 | 贾攀的流水账 + + + + + + + + + + + + + + + +
    +
    + +
    +
    + + +
    + + + +

    贾攀的流水账

    + +
    +

    Panmax's Blog

    +
    + + +
    + + + + + + + + +
    + +
    + +
    +
    + + +
    + + 0% +
    + + +
    +
    +
    + + +
    + + + + + +
    +
    +
    +

    feed流 + Tag +

    +
    + + +
    + 2015 +
    + + + +
    +
    + + + + + + + + +
    + + + + +
    + + + + + + + + +
    +
    + +
    + +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tags/flag/index.html b/tags/flag/index.html new file mode 100644 index 0000000000..4c314c0608 --- /dev/null +++ b/tags/flag/index.html @@ -0,0 +1,442 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Tag: flag | 贾攀的流水账 + + + + + + + + + + + + + + + +
    +
    + +
    +
    + + +
    + + + +

    贾攀的流水账

    + +
    +

    Panmax's Blog

    +
    + + +
    + + + + + + + + +
    + +
    + +
    +
    + + +
    + + 0% +
    + + +
    +
    +
    + + +
    + + + + + +
    +
    +
    +

    flag + Tag +

    +
    + + +
    + 2019 +
    + + + +
    +
    + + + + + + + + +
    + + + + +
    + + + + + + + + +
    +
    + +
    + +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tags/flask/index.html b/tags/flask/index.html new file mode 100644 index 0000000000..e8dd349299 --- /dev/null +++ b/tags/flask/index.html @@ -0,0 +1,565 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Tag: flask | 贾攀的流水账 + + + + + + + + + + + + + + + +
    +
    + +
    +
    + + +
    + + + +

    贾攀的流水账

    + +
    +

    Panmax's Blog

    +
    + + +
    + + + + + + + + +
    + +
    + +
    +
    + + +
    + + 0% +
    + + +
    +
    +
    + + +
    + + + + + +
    +
    +
    +

    flask + Tag +

    +
    + + +
    + 2016 +
    + + + + +
    + 2015 +
    + + + + + + + + + + + +
    +
    + + + + + + + + +
    + + + + +
    + + + + + + + + +
    +
    + +
    + +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tags/git/index.html b/tags/git/index.html new file mode 100644 index 0000000000..8e833250b3 --- /dev/null +++ b/tags/git/index.html @@ -0,0 +1,442 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Tag: git | 贾攀的流水账 + + + + + + + + + + + + + + + +
    +
    + +
    +
    + + +
    + + + +

    贾攀的流水账

    + +
    +

    Panmax's Blog

    +
    + + +
    + + + + + + + + +
    + +
    + +
    +
    + + +
    + + 0% +
    + + +
    +
    +
    + + +
    + + + + + +
    +
    +
    +

    git + Tag +

    +
    + + +
    + 2021 +
    + + + +
    +
    + + + + + + + + +
    + + + + +
    + + + + + + + + +
    +
    + +
    + +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tags/go/index.html b/tags/go/index.html new file mode 100644 index 0000000000..66b51d1495 --- /dev/null +++ b/tags/go/index.html @@ -0,0 +1,465 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Tag: go | 贾攀的流水账 + + + + + + + + + + + + + + + +
    +
    + +
    +
    + + +
    + + + +

    贾攀的流水账

    + +
    +

    Panmax's Blog

    +
    + + +
    + + + + + + + + +
    + +
    + +
    +
    + + +
    + + 0% +
    + + +
    +
    +
    + + +
    + + + + + +
    +
    +
    +

    go + Tag +

    +
    + + +
    + 2021 +
    + + +
    + 2020 +
    + + + +
    +
    + + + + + + + + +
    + + + + +
    + + + + + + + + +
    +
    + +
    + +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tags/https/index.html b/tags/https/index.html new file mode 100644 index 0000000000..dbf5e8c064 --- /dev/null +++ b/tags/https/index.html @@ -0,0 +1,442 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Tag: https | 贾攀的流水账 + + + + + + + + + + + + + + + +
    +
    + +
    +
    + + +
    + + + +

    贾攀的流水账

    + +
    +

    Panmax's Blog

    +
    + + +
    + + + + + + + + +
    + +
    + +
    +
    + + +
    + + 0% +
    + + +
    +
    +
    + + +
    + + + + + +
    +
    +
    +

    https + Tag +

    +
    + + +
    + 2019 +
    + + + +
    +
    + + + + + + + + +
    + + + + +
    + + + + + + + + +
    +
    + +
    + +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tags/index.html b/tags/index.html new file mode 100644 index 0000000000..deed6f878b --- /dev/null +++ b/tags/index.html @@ -0,0 +1,442 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + tags | 贾攀的流水账 + + + + + + + + + + + + + + + + +
    +
    + +
    +
    + + +
    + + + +

    贾攀的流水账

    + +
    +

    Panmax's Blog

    +
    + + +
    + + + + + + + + +
    + +
    + +
    +
    + + +
    + + 0% +
    + + +
    +
    +
    + + + + + + +
    + + + +
    + + + + + + + + +
    +
    + +
    + +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tags/jQuery/index.html b/tags/jQuery/index.html new file mode 100644 index 0000000000..1cc773e7c7 --- /dev/null +++ b/tags/jQuery/index.html @@ -0,0 +1,442 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Tag: jQuery | 贾攀的流水账 + + + + + + + + + + + + + + + +
    +
    + +
    +
    + + +
    + + + +

    贾攀的流水账

    + +
    +

    Panmax's Blog

    +
    + + +
    + + + + + + + + +
    + +
    + +
    +
    + + +
    + + 0% +
    + + +
    +
    +
    + + +
    + + + + + +
    +
    +
    +

    jQuery + Tag +

    +
    + + +
    + 2016 +
    + + + +
    +
    + + + + + + + + +
    + + + + +
    + + + + + + + + +
    +
    + +
    + +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tags/neo4j/index.html b/tags/neo4j/index.html new file mode 100644 index 0000000000..89662a8327 --- /dev/null +++ b/tags/neo4j/index.html @@ -0,0 +1,625 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Tag: neo4j | 贾攀的流水账 + + + + + + + + + + + + + + + +
    +
    + +
    +
    + + +
    + + + +

    贾攀的流水账

    + +
    +

    Panmax's Blog

    +
    + + +
    + + + + + + + + +
    + +
    + +
    +
    + + +
    + + 0% +
    + + +
    +
    +
    + + +
    + + + + + +
    +
    +
    +

    neo4j + Tag +

    +
    + + +
    + 2018 +
    + + + + + + + + + + + + + + + + + + + + + +
    +
    + + + + + + + + + +
    + + + + +
    + + + + + + + + +
    +
    + +
    + +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tags/neo4j/page/2/index.html b/tags/neo4j/page/2/index.html new file mode 100644 index 0000000000..bd13229184 --- /dev/null +++ b/tags/neo4j/page/2/index.html @@ -0,0 +1,568 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Tag: neo4j | 贾攀的流水账 + + + + + + + + + + + + + + + +
    +
    + +
    +
    + + +
    + + + +

    贾攀的流水账

    + +
    +

    Panmax's Blog

    +
    + + +
    + + + + + + + + +
    + +
    + +
    +
    + + +
    + + 0% +
    + + +
    +
    +
    + + +
    + + + + + +
    +
    +
    +

    neo4j + Tag +

    +
    + + +
    + 2018 +
    + + + + + + + + + + + + +
    + 2017 +
    + + + +
    +
    + + + + + + + + + +
    + + + + +
    + + + + + + + + +
    +
    + +
    + +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tags/reading/index.html b/tags/reading/index.html new file mode 100644 index 0000000000..c207b490e8 --- /dev/null +++ b/tags/reading/index.html @@ -0,0 +1,442 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Tag: reading | 贾攀的流水账 + + + + + + + + + + + + + + + +
    +
    + +
    +
    + + +
    + + + +

    贾攀的流水账

    + +
    +

    Panmax's Blog

    +
    + + +
    + + + + + + + + +
    + +
    + +
    +
    + + +
    + + 0% +
    + + +
    +
    +
    + + +
    + + + + + +
    +
    +
    +

    reading + Tag +

    +
    + + +
    + 2017 +
    + + + +
    +
    + + + + + + + + +
    + + + + +
    + + + + + + + + +
    +
    + +
    + +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tags/redis/index.html b/tags/redis/index.html new file mode 100644 index 0000000000..8bfdb533a3 --- /dev/null +++ b/tags/redis/index.html @@ -0,0 +1,442 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Tag: redis | 贾攀的流水账 + + + + + + + + + + + + + + + +
    +
    + +
    +
    + + +
    + + + +

    贾攀的流水账

    + +
    +

    Panmax's Blog

    +
    + + +
    + + + + + + + + +
    + +
    + +
    +
    + + +
    + + 0% +
    + + +
    +
    +
    + + +
    + + + + + +
    +
    +
    +

    redis + Tag +

    +
    + + +
    + 2015 +
    + + + +
    +
    + + + + + + + + +
    + + + + +
    + + + + + + + + +
    +
    + +
    + +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tags/timestamp/index.html b/tags/timestamp/index.html new file mode 100644 index 0000000000..6f25c898be --- /dev/null +++ b/tags/timestamp/index.html @@ -0,0 +1,442 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Tag: timestamp | 贾攀的流水账 + + + + + + + + + + + + + + + +
    +
    + +
    +
    + + +
    + + + +

    贾攀的流水账

    + +
    +

    Panmax's Blog

    +
    + + +
    + + + + + + + + +
    + +
    + +
    +
    + + +
    + + 0% +
    + + +
    +
    +
    + + +
    + + + + + +
    +
    +
    +

    timestamp + Tag +

    +
    + + +
    + 2015 +
    + + + +
    +
    + + + + + + + + +
    + + + + +
    + + + + + + + + +
    +
    + +
    + +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tags/udp/index.html b/tags/udp/index.html new file mode 100644 index 0000000000..6ed6e0dfa4 --- /dev/null +++ b/tags/udp/index.html @@ -0,0 +1,442 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Tag: udp | 贾攀的流水账 + + + + + + + + + + + + + + + +
    +
    + +
    +
    + + +
    + + + +

    贾攀的流水账

    + +
    +

    Panmax's Blog

    +
    + + +
    + + + + + + + + +
    + +
    + +
    +
    + + +
    + + 0% +
    + + +
    +
    +
    + + +
    + + + + + +
    +
    +
    +

    udp + Tag +

    +
    + + +
    + 2021 +
    + + + +
    +
    + + + + + + + + +
    + + + + +
    + + + + + + + + +
    +
    + +
    + +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git "a/tags/\344\272\232\351\251\254\351\200\212\344\272\221\346\234\215\345\212\241/index.html" "b/tags/\344\272\232\351\251\254\351\200\212\344\272\221\346\234\215\345\212\241/index.html" new file mode 100644 index 0000000000..619fe723a0 --- /dev/null +++ "b/tags/\344\272\232\351\251\254\351\200\212\344\272\221\346\234\215\345\212\241/index.html" @@ -0,0 +1,442 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Tag: 亚马逊云服务 | 贾攀的流水账 + + + + + + + + + + + + + + + +
    +
    + +
    +
    + + +
    + + + +

    贾攀的流水账

    + +
    +

    Panmax's Blog

    +
    + + +
    + + + + + + + + +
    + +
    + +
    +
    + + +
    + + 0% +
    + + +
    +
    +
    + + +
    + + + + + +
    +
    +
    +

    亚马逊云服务 + Tag +

    +
    + + +
    + 2016 +
    + + + +
    +
    + + + + + + + + +
    + + + + +
    + + + + + + + + +
    +
    + +
    + +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git "a/tags/\345\205\263\351\224\256\345\255\227\345\217\202\346\225\260/index.html" "b/tags/\345\205\263\351\224\256\345\255\227\345\217\202\346\225\260/index.html" new file mode 100644 index 0000000000..1e5d8b7327 --- /dev/null +++ "b/tags/\345\205\263\351\224\256\345\255\227\345\217\202\346\225\260/index.html" @@ -0,0 +1,442 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Tag: 关键字参数 | 贾攀的流水账 + + + + + + + + + + + + + + + +
    +
    + +
    +
    + + +
    + + + +

    贾攀的流水账

    + +
    +

    Panmax's Blog

    +
    + + +
    + + + + + + + + +
    + +
    + +
    +
    + + +
    + + 0% +
    + + +
    +
    +
    + + +
    + + + + + +
    +
    +
    +

    关键字参数 + Tag +

    +
    + + +
    + 2016 +
    + + + +
    +
    + + + + + + + + +
    + + + + +
    + + + + + + + + +
    +
    + +
    + +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git "a/tags/\345\220\214\346\255\245/index.html" "b/tags/\345\220\214\346\255\245/index.html" new file mode 100644 index 0000000000..58a5668291 --- /dev/null +++ "b/tags/\345\220\214\346\255\245/index.html" @@ -0,0 +1,442 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Tag: 同步 | 贾攀的流水账 + + + + + + + + + + + + + + + +
    +
    + +
    +
    + + +
    + + + +

    贾攀的流水账

    + +
    +

    Panmax's Blog

    +
    + + +
    + + + + + + + + +
    + +
    + +
    +
    + + +
    + + 0% +
    + + +
    +
    +
    + + +
    + + + + + +
    +
    +
    +

    同步 + Tag +

    +
    + + +
    + 2019 +
    + + + +
    +
    + + + + + + + + +
    + + + + +
    + + + + + + + + +
    +
    + +
    + +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git "a/tags/\345\235\221/index.html" "b/tags/\345\235\221/index.html" new file mode 100644 index 0000000000..c7cf941476 --- /dev/null +++ "b/tags/\345\235\221/index.html" @@ -0,0 +1,442 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Tag: 坑 | 贾攀的流水账 + + + + + + + + + + + + + + + +
    +
    + +
    +
    + + +
    + + + +

    贾攀的流水账

    + +
    +

    Panmax's Blog

    +
    + + +
    + + + + + + + + +
    + +
    + +
    +
    + + +
    + + 0% +
    + + +
    +
    +
    + + +
    + + + + + +
    +
    +
    +

    坑 + Tag +

    +
    + + +
    + 2015 +
    + + + +
    +
    + + + + + + + + +
    + + + + +
    + + + + + + + + +
    +
    + +
    + +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git "a/tags/\345\274\202\346\255\245/index.html" "b/tags/\345\274\202\346\255\245/index.html" new file mode 100644 index 0000000000..34bd0a697c --- /dev/null +++ "b/tags/\345\274\202\346\255\245/index.html" @@ -0,0 +1,442 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Tag: 异步 | 贾攀的流水账 + + + + + + + + + + + + + + + +
    +
    + +
    +
    + + +
    + + + +

    贾攀的流水账

    + +
    +

    Panmax's Blog

    +
    + + +
    + + + + + + + + +
    + +
    + +
    +
    + + +
    + + 0% +
    + + +
    +
    +
    + + +
    + + + + + +
    +
    +
    +

    异步 + Tag +

    +
    + + +
    + 2019 +
    + + + +
    +
    + + + + + + + + +
    + + + + +
    + + + + + + + + +
    +
    + +
    + +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git "a/tags/\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204\350\256\276\350\256\241\346\250\241\345\274\217/index.html" "b/tags/\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204\350\256\276\350\256\241\346\250\241\345\274\217/index.html" new file mode 100644 index 0000000000..a39e23f8ee --- /dev/null +++ "b/tags/\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204\350\256\276\350\256\241\346\250\241\345\274\217/index.html" @@ -0,0 +1,502 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Tag: 微服务架构设计模式 | 贾攀的流水账 + + + + + + + + + + + + + + + +
    +
    + +
    +
    + + +
    + + + +

    贾攀的流水账

    + +
    +

    Panmax's Blog

    +
    + + +
    + + + + + + + + +
    + +
    + +
    +
    + + +
    + + 0% +
    + + +
    +
    +
    + + +
    + + + + + +
    +
    +
    +

    微服务架构设计模式 + Tag +

    +
    + + +
    + 2022 +
    + + + + + + + + + +
    +
    + + + + + + + + +
    + + + + +
    + + + + + + + + +
    +
    + +
    + +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git "a/tags/\346\210\220\351\225\277/index.html" "b/tags/\346\210\220\351\225\277/index.html" new file mode 100644 index 0000000000..1ec48d1a39 --- /dev/null +++ "b/tags/\346\210\220\351\225\277/index.html" @@ -0,0 +1,442 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Tag: 成长 | 贾攀的流水账 + + + + + + + + + + + + + + + +
    +
    + +
    +
    + + +
    + + + +

    贾攀的流水账

    + +
    +

    Panmax's Blog

    +
    + + +
    + + + + + + + + +
    + +
    + +
    +
    + + +
    + + 0% +
    + + +
    +
    +
    + + +
    + + + + + +
    +
    +
    +

    成长 + Tag +

    +
    + + +
    + 2015 +
    + + + +
    +
    + + + + + + + + +
    + + + + +
    + + + + + + + + +
    +
    + +
    + +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git "a/tags/\346\216\245\345\217\243/index.html" "b/tags/\346\216\245\345\217\243/index.html" new file mode 100644 index 0000000000..71aa2aec40 --- /dev/null +++ "b/tags/\346\216\245\345\217\243/index.html" @@ -0,0 +1,442 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Tag: 接口 | 贾攀的流水账 + + + + + + + + + + + + + + + +
    +
    + +
    +
    + + +
    + + + +

    贾攀的流水账

    + +
    +

    Panmax's Blog

    +
    + + +
    + + + + + + + + +
    + +
    + +
    +
    + + +
    + + 0% +
    + + +
    +
    +
    + + +
    + + + + + +
    +
    +
    +

    接口 + Tag +

    +
    + + +
    + 2016 +
    + + + +
    +
    + + + + + + + + +
    + + + + +
    + + + + + + + + +
    +
    + +
    + +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git "a/tags/\346\221\230\346\212\204/index.html" "b/tags/\346\221\230\346\212\204/index.html" new file mode 100644 index 0000000000..67ce53e7d0 --- /dev/null +++ "b/tags/\346\221\230\346\212\204/index.html" @@ -0,0 +1,462 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Tag: 摘抄 | 贾攀的流水账 + + + + + + + + + + + + + + + +
    +
    + +
    +
    + + +
    + + + +

    贾攀的流水账

    + +
    +

    Panmax's Blog

    +
    + + +
    + + + + + + + + +
    + +
    + +
    +
    + + +
    + + 0% +
    + + +
    +
    +
    + + +
    + + + + + +
    +
    +
    +

    摘抄 + Tag +

    +
    + + +
    + 2021 +
    + + + + + +
    +
    + + + + + + + + +
    + + + + +
    + + + + + + + + +
    +
    + +
    + +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git "a/tags/\346\234\211\350\266\243/index.html" "b/tags/\346\234\211\350\266\243/index.html" new file mode 100644 index 0000000000..20962eb75e --- /dev/null +++ "b/tags/\346\234\211\350\266\243/index.html" @@ -0,0 +1,442 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Tag: 有趣 | 贾攀的流水账 + + + + + + + + + + + + + + + +
    +
    + +
    +
    + + +
    + + + +

    贾攀的流水账

    + +
    +

    Panmax's Blog

    +
    + + +
    + + + + + + + + +
    + +
    + +
    +
    + + +
    + + 0% +
    + + +
    +
    +
    + + +
    + + + + + +
    +
    +
    +

    有趣 + Tag +

    +
    + + +
    + 2016 +
    + + + +
    +
    + + + + + + + + +
    + + + + +
    + + + + + + + + +
    +
    + +
    + +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git "a/tags/\346\272\220\347\240\201/index.html" "b/tags/\346\272\220\347\240\201/index.html" new file mode 100644 index 0000000000..22e33fb579 --- /dev/null +++ "b/tags/\346\272\220\347\240\201/index.html" @@ -0,0 +1,502 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Tag: 源码 | 贾攀的流水账 + + + + + + + + + + + + + + + +
    +
    + +
    +
    + + +
    + + + +

    贾攀的流水账

    + +
    +

    Panmax's Blog

    +
    + + +
    + + + + + + + + +
    + +
    + +
    +
    + + +
    + + 0% +
    + + +
    +
    +
    + + +
    + + + + + +
    +
    +
    +

    源码 + Tag +

    +
    + + +
    + 2016 +
    + + + + + + + + + +
    +
    + + + + + + + + +
    + + + + +
    + + + + + + + + +
    +
    + +
    + +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git "a/tags/\347\237\245\344\271\216/index.html" "b/tags/\347\237\245\344\271\216/index.html" new file mode 100644 index 0000000000..84bfe65093 --- /dev/null +++ "b/tags/\347\237\245\344\271\216/index.html" @@ -0,0 +1,442 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Tag: 知乎 | 贾攀的流水账 + + + + + + + + + + + + + + + +
    +
    + +
    +
    + + +
    + + + +

    贾攀的流水账

    + +
    +

    Panmax's Blog

    +
    + + +
    + + + + + + + + +
    + +
    + +
    +
    + + +
    + + 0% +
    + + +
    +
    +
    + + +
    + + + + + +
    +
    +
    +

    知乎 + Tag +

    +
    + + +
    + 2015 +
    + + + +
    +
    + + + + + + + + +
    + + + + +
    + + + + + + + + +
    +
    + +
    + +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git "a/tags/\347\237\245\350\257\206\344\275\223\347\263\273/index.html" "b/tags/\347\237\245\350\257\206\344\275\223\347\263\273/index.html" new file mode 100644 index 0000000000..7b9a423c91 --- /dev/null +++ "b/tags/\347\237\245\350\257\206\344\275\223\347\263\273/index.html" @@ -0,0 +1,442 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Tag: 知识体系 | 贾攀的流水账 + + + + + + + + + + + + + + + +
    +
    + +
    +
    + + +
    + + + +

    贾攀的流水账

    + +
    +

    Panmax's Blog

    +
    + + +
    + + + + + + + + +
    + +
    + +
    +
    + + +
    + + 0% +
    + + +
    +
    +
    + + +
    + + + + + +
    +
    +
    +

    知识体系 + Tag +

    +
    + + +
    + 2015 +
    + + + +
    +
    + + + + + + + + +
    + + + + +
    + + + + + + + + +
    +
    + +
    + +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git "a/tags/\347\254\224\350\256\260/index.html" "b/tags/\347\254\224\350\256\260/index.html" new file mode 100644 index 0000000000..cc651c05c5 --- /dev/null +++ "b/tags/\347\254\224\350\256\260/index.html" @@ -0,0 +1,442 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Tag: 笔记 | 贾攀的流水账 + + + + + + + + + + + + + + + +
    +
    + +
    +
    + + +
    + + + +

    贾攀的流水账

    + +
    +

    Panmax's Blog

    +
    + + +
    + + + + + + + + +
    + +
    + +
    +
    + + +
    + + 0% +
    + + +
    +
    +
    + + +
    + + + + + +
    +
    +
    +

    笔记 + Tag +

    +
    + + +
    + 2018 +
    + + + +
    +
    + + + + + + + + +
    + + + + +
    + + + + + + + + +
    +
    + +
    + +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git "a/tags/\347\274\226\347\240\201/index.html" "b/tags/\347\274\226\347\240\201/index.html" new file mode 100644 index 0000000000..a867c38143 --- /dev/null +++ "b/tags/\347\274\226\347\240\201/index.html" @@ -0,0 +1,442 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Tag: 编码 | 贾攀的流水账 + + + + + + + + + + + + + + + +
    +
    + +
    +
    + + +
    + + + +

    贾攀的流水账

    + +
    +

    Panmax's Blog

    +
    + + +
    + + + + + + + + +
    + +
    + +
    +
    + + +
    + + 0% +
    + + +
    +
    +
    + + +
    + + + + + +
    +
    +
    +

    编码 + Tag +

    +
    + + +
    + 2016 +
    + + + +
    +
    + + + + + + + + +
    + + + + +
    + + + + + + + + +
    +
    + +
    + +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git "a/tags/\350\257\273\344\271\246/index.html" "b/tags/\350\257\273\344\271\246/index.html" new file mode 100644 index 0000000000..c0d3457751 --- /dev/null +++ "b/tags/\350\257\273\344\271\246/index.html" @@ -0,0 +1,442 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Tag: 读书 | 贾攀的流水账 + + + + + + + + + + + + + + + +
    +
    + +
    +
    + + +
    + + + +

    贾攀的流水账

    + +
    +

    Panmax's Blog

    +
    + + +
    + + + + + + + + +
    + +
    + +
    +
    + + +
    + + 0% +
    + + +
    +
    +
    + + +
    + + + + + +
    +
    +
    +

    读书 + Tag +

    +
    + + +
    + 2017 +
    + + + +
    +
    + + + + + + + + +
    + + + + +
    + + + + + + + + +
    +
    + +
    + +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git "a/tags/\350\257\273\344\271\246\347\254\224\350\256\260/index.html" "b/tags/\350\257\273\344\271\246\347\254\224\350\256\260/index.html" new file mode 100644 index 0000000000..de4751e22d --- /dev/null +++ "b/tags/\350\257\273\344\271\246\347\254\224\350\256\260/index.html" @@ -0,0 +1,525 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Tag: 读书笔记 | 贾攀的流水账 + + + + + + + + + + + + + + + +
    +
    + +
    +
    + + +
    + + + +

    贾攀的流水账

    + +
    +

    Panmax's Blog

    +
    + + +
    + + + + + + + + +
    + +
    + +
    +
    + + +
    + + 0% +
    + + +
    +
    +
    + + +
    + + + + + +
    +
    +
    +

    读书笔记 + Tag +

    +
    + + +
    + 2022 +
    + + + + + + + + +
    + 2016 +
    + + + +
    +
    + + + + + + + + +
    + + + + +
    + + + + + + + + +
    +
    + +
    + +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git "a/tags/\351\230\205\350\257\273/index.html" "b/tags/\351\230\205\350\257\273/index.html" new file mode 100644 index 0000000000..06f44f6443 --- /dev/null +++ "b/tags/\351\230\205\350\257\273/index.html" @@ -0,0 +1,462 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Tag: 阅读 | 贾攀的流水账 + + + + + + + + + + + + + + + +
    +
    + +
    +
    + + +
    + + + +

    贾攀的流水账

    + +
    +

    Panmax's Blog

    +
    + + +
    + + + + + + + + +
    + +
    + +
    +
    + + +
    + + 0% +
    + + +
    +
    +
    + + +
    + + + + + +
    +
    +
    +

    阅读 + Tag +

    +
    + + +
    + 2020 +
    + + + + + +
    +
    + + + + + + + + +
    + + + + +
    + + + + + + + + +
    +
    + +
    + +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git "a/tags/\351\230\256\344\270\200\345\263\260/index.html" "b/tags/\351\230\256\344\270\200\345\263\260/index.html" new file mode 100644 index 0000000000..28600ac303 --- /dev/null +++ "b/tags/\351\230\256\344\270\200\345\263\260/index.html" @@ -0,0 +1,442 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Tag: 阮一峰 | 贾攀的流水账 + + + + + + + + + + + + + + + +
    +
    + +
    +
    + + +
    + + + +

    贾攀的流水账

    + +
    +

    Panmax's Blog

    +
    + + +
    + + + + + + + + +
    + +
    + +
    +
    + + +
    + + 0% +
    + + +
    +
    +
    + + +
    + + + + + +
    +
    +
    +

    阮一峰 + Tag +

    +
    + + +
    + 2015 +
    + + + +
    +
    + + + + + + + + +
    + + + + +
    + + + + + + + + +
    +
    + +
    + +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git "a/tags/\351\230\273\345\241\236/index.html" "b/tags/\351\230\273\345\241\236/index.html" new file mode 100644 index 0000000000..a880da3b92 --- /dev/null +++ "b/tags/\351\230\273\345\241\236/index.html" @@ -0,0 +1,442 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Tag: 阻塞 | 贾攀的流水账 + + + + + + + + + + + + + + + +
    +
    + +
    +
    + + +
    + + + +

    贾攀的流水账

    + +
    +

    Panmax's Blog

    +
    + + +
    + + + + + + + + +
    + +
    + +
    +
    + + +
    + + 0% +
    + + +
    +
    +
    + + +
    + + + + + +
    +
    +
    +

    阻塞 + Tag +

    +
    + + +
    + 2019 +
    + + + +
    +
    + + + + + + + + +
    + + + + +
    + + + + + + + + +
    +
    + +
    + +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git "a/tags/\351\235\236\351\230\273\345\241\236/index.html" "b/tags/\351\235\236\351\230\273\345\241\236/index.html" new file mode 100644 index 0000000000..b28b2cc5a0 --- /dev/null +++ "b/tags/\351\235\236\351\230\273\345\241\236/index.html" @@ -0,0 +1,442 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Tag: 非阻塞 | 贾攀的流水账 + + + + + + + + + + + + + + + +
    +
    + +
    +
    + + +
    + + + +

    贾攀的流水账

    + +
    +

    Panmax's Blog

    +
    + + +
    + + + + + + + + +
    + +
    + +
    +
    + + +
    + + 0% +
    + + +
    +
    +
    + + +
    + + + + + +
    +
    +
    +

    非阻塞 + Tag +

    +
    + + +
    + 2019 +
    + + + +
    +
    + + + + + + + + +
    + + + + +
    + + + + + + + + +
    +
    + +
    + +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git "a/tags/\351\255\224\351\225\234/index.html" "b/tags/\351\255\224\351\225\234/index.html" new file mode 100644 index 0000000000..1107f2ba42 --- /dev/null +++ "b/tags/\351\255\224\351\225\234/index.html" @@ -0,0 +1,505 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Tag: 魔镜 | 贾攀的流水账 + + + + + + + + + + + + + + + +
    +
    + +
    +
    + + +
    + + + +

    贾攀的流水账

    + +
    +

    Panmax's Blog

    +
    + + +
    + + + + + + + + +
    + +
    + +
    +
    + + +
    + + 0% +
    + + +
    +
    +
    + + +
    + + + + + +
    +
    +
    +

    魔镜 + Tag +

    +
    + + +
    + 2016 +
    + + + + + + +
    + 2015 +
    + + + +
    +
    + + + + + + + + +
    + + + + +
    + + + + + + + + +
    +
    + +
    + +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/think/index.html b/think/index.html new file mode 100644 index 0000000000..415b83f011 --- /dev/null +++ b/think/index.html @@ -0,0 +1,496 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 浴室沉思 | 贾攀的流水账 + + + + + + + + + + + + + + + + +
    +
    + +
    +
    + + +
    + + + +

    贾攀的流水账

    + +
    +

    Panmax's Blog

    +
    + + +
    + + + + + + + + +
    + +
    + +
    +
    + + +
    + + 0% +
    + + +
    +
    +
    + + + + +
    + + + + + +
    +
    + +

    浴室沉思 +

    + + + +
    + + + + +
    +
    +

    以下想法多数来自我洗澡时(每个人在洗澡时都是哲学家)、骑车时、躺着睡不着时从脑子里蹦出来的,还有些是听到别人说觉得有些道理。

    +

    现在没有频繁发朋友圈的习惯了,但是这些观点又想分享出去,每个水一篇文章也没有必要,索性一起汇总在这里。

    +
    +
    +

    人生是一场长跑,别天天想着争分夺秒。

    +
    +

    悲观者往往正确,乐观者往往成功。

    +
    +

    勇敢是:当你还未开始就已知道自己会输,可你依然要去做,而且无论如何都要把它坚持到底。你很少能赢,但有时也会。——《杀死一只知更鸟》

    +
    +

    人类的一切智慧是包含在这四个字里面的:’等待’和’希望’ ——《基督山伯爵》

    +
    +

    大厂只是在贩卖一种叫做‘精英幻想’的毒品。

    +
    +

    我们每个人都是小丑,一生当中就在玩这五个球:家庭、工作、健康、朋友和灵魂。五个球当中只有工作这个球是橡胶做的,砸下去还会弹起来,其他四个球是玻璃做的,砸碎了再也不会复原。

    +
    +

    半师半友半知己,半慕半尊半倾心。

    +
    +

    你只是没得到而已,不要搞得像失去了一样。

    +
    +

    千万不要相信读书无用论。你现在用不到,是因为你的职业素养还没到那个层级,社会地位还没到那个高度。——from net 2022年07月22日

    +
    +

    爱情里面要是搀杂了和它本身无关的算计,那就不是真的爱情。——《李尔王》2022年07月22日

    +
    +

    很多人认为宝玉滥情,但实际上宝玉是博爱,他会觉得人世间每一个活着和死去的生命都应该被纪念,都应该被好好照顾。

    +
    +

    人人都讨厌小集体,但人人都在搞小集体。人讨厌小集体是因为小集体里没有自己。

    +
    +

    人生的领悟不是在知识里,人生的领悟其实是在生命的经验当中。(蒋勋)——2022年06月02日

    +
    +

    每当我失眠,或在半夜醒来,回忆过去时,往往记起的只是那些令我痛苦和难堪的事情。我们的不好的回忆,似乎都是由于我们做出了错误的决定导致,然而,用现在的眼光来评判过去的我们,是不公平的。在那个时刻,我们用有限的信息,和当时的心智做出了最好的决定,结果不是我们能控制的。所以,我们用不着后悔,更不应该自责。

    +
    +

    爱过的人,是做不了朋友的,因为一见面就会心软,一拥抱就会沦陷,多看一眼,就想重新拥有。(张爱玲)——2021年12月25日

    +
    +

    人性会在持续的颓废时发出示警,却容易被无效的努力所欺骗。——2021年12月7日

    +
    +

    幸于始者怠于终,善其辞者嗜其利(《红楼梦》第五十六回) ——2021年11月27日

    +
    +

    我觉得国内大厂情节比较严重和我国的高考制度有一定的关系,学生们在上学时就被灌输了上大学后就完事大吉。同样毕业后自然而然的被继续灌输进了大厂就万事大吉。殊不知人生的路还有很长,有很多更重要的事情等着我们去做 ——2021年11月4日

    +
    +

    回顾一下我的教育经历:一流的初中、二流的高中、三流的大学 ——2021年11月4日

    +
    +

    宝玉觉得每个生命的存在都有他的意义和价值 ——2021年10月15日

    +
    +

    我的被子在等我睡觉 ——2021年9月6日

    +
    +

    少用反问句 ——2021年8月23日

    +
    +

    王夫人身上有一种原配情结,她觉得女人就该正正经经,像赵姨娘那样的丫头出来做妾,就是娼妇、狐狸精 ——2021年8月18日

    +
    +

    写作是一种深度思考 ——2021年8月14日

    +
    +

    数据太多没有用,数据关联起来才叫信息,信息的因果关系叫公式 ——2021年8月14日

    +
    +

    判断语言是否有前途的4个方面:学习门槛、社区活跃、大公司支撑、杀手级应用 ——2021年8月14日

    +
    +

    富过三代,才懂吃穿 ——2021年8月12日

    +
    +

    幸运的人一生都被童年治愈,不幸的人一生都在治愈童年 ——2021年8月11日

    +
    +

    古人说的三从四德中的「三从」指的是未嫁从父、出嫁从夫、夫死从子 ——2021年8月9日

    + +
    + + + +
    + + + + + + + +
    + +
    + + + +
    + + + + + + + + +
    +
    + +
    + +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +