Giter Site home page Giter Site logo

blog's People

Contributors

aplini avatar bingoohuang avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

blog's Issues

与人方便的各式各样的Fiddler

Fiddle原是小提琴🎻的意思。
image
图片来自500px

抓包工具Fiddler

最早用的是它了,著名的抓包小工具----Fiddler,下面这个样子,当初IE上开发,没有那么好用的Dev Tools。可惜MAC上没有。
image
MAC上的是Charles
image

在线编辑、展示、分享、交流你的JavaScript 代码JsFiddle

Js在线跑跑脚本,看看样式,最早的应该是JsFiddle。长下面这个样子。
image
后来,又有了各式各样的版本,比如RunJS、JSBIN、CODEPEN等。随便找一个用用即可。

正则表达式在线测试regexpal

看下图,最欣赏的是,它把正则表达式进行了解释了,告诉你,你的正则的各个部分是个什么意思。
image

正则表达式,要注意ReDoS问题

SQL Fiddle

image
可以支持MySQL5.6, Oracle 11g R2, PostgreSQL 9.6/9.4, SQLite(WebSQL/SQL.js), MS SQL Server 2017,真是超级方便。写完创建表和插入表数据后,就可以在后边写查询语句了,然后执行后,就可以在下面看到结果,并且这时候URL也完成了更新,可以拷贝这个URL,分享当前的SQL给小伙伴们。超级方便。

Cron Fiddle

Graphically, the cron syntax for Quarz is (source):

+-------------------- second (0 - 59)
|  +----------------- minute (0 - 59)
|  |  +-------------- hour (0 - 23)
|  |  |  +----------- day of month (1 - 31)
|  |  |  |  +-------- month (1 - 12)
|  |  |  |  |  +----- day of week (0 - 6) (Sunday=0 or 7)
|  |  |  |  |  |  +-- year [optional]
|  |  |  |  |  |  |
*  *  *  *  *  *  * command to be executed 

crontab.guru: — (disclaimer: I am not related to that page at all, only that I find it very useful). This page uses UNIX style of cron that does not have seconds in it, while Spring does as the first field:
image

Cron Expression Generator & Explainer - Quartz — cron formatter, allowing seconds also.
image

Date & Time Fiddle

currentmillis获取当前的时间毫秒数及其换算。
image

timeanddate日期上的加加减减那些事。
image

Stylus Compiler

image

JSON 在线编辑工具

image

在线文件转换神器

2018年10月24日,汉能给我们发了一些Hogon相关的文档,里面有几封.msg结尾的文件,我们电脑没有安装相关软件,无法打开,然后我就想能不能找个线上的Viewer呢,然后就输入“outlook msg online viewer”谷歌一下,第二项就是它:
image

然后再看看下面的介绍,支持1200多种不同格式的文件转换,牛B💯不要不要。
image

Text Tables Generator

纯文本化的表格
image

自己挖坑自己填3:会时光倒流的有效期

问题描述

最近开了一个新的丽云SpringBoot工程,开始做新的行业SAAS了。结果开发期间,发现日期时间存到阿里云RDS中,会发生偏移,例如存储2018-05-08 00:00:00,再查询出来,会变成2018-05-07 11:00:00,造成数据错误。

2018-05-08 10:48:07.869 [http-nio-7689-exec-9] DEBUG tcode[19000001] eql.cn.raiyee.liyun.service.back.dao.MemberCardDaoImpl.eql.addMemberCard.prepare:116 - insert into t_member_card(MEMBER_CARD_ID, CARD_NO, USER_ID, REAL_NAME, CARD_TYPE_NAME, CARD_NAME, CARD_ID, `EFFECTIVE`, `EXPIRED`, EXPIRED_VALUE, EXPIRED_UNIT, INITIAL_BALANCE, AVAIL_BALANCE, `REMARK`, `STATE`, LATEST_ACTIVATE_DAY, FROM_MEMBER_CARD_ID)values(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
2018-05-08 10:48:07.870 [http-nio-7689-exec-9] DEBUG tcode[19000001] eql.cn.raiyee.liyun.service.back.dao.MemberCardDaoImpl.eql.addMemberCard.params:99 - [148484250576441344, F05, 144902165450399744, 冯小雨, 储值卡, 100元无折扣, 147018595541049344, 2018-05-08T00:00:00.000+08:00, 2018-06-08T00:00:00.000+08:00, 1, 月, 100.0, 100.0, null, 已激活, null, null]
2018-05-08 10:48:07.871 [http-nio-7689-exec-9] DEBUG tcode[19000001] eql.cn.raiyee.liyun.service.back.dao.MemberCardDaoImpl.eql.addMemberCard.eval:106 - insert into t_member_card(MEMBER_CARD_ID, CARD_NO, USER_ID, REAL_NAME, CARD_TYPE_NAME, CARD_NAME, CARD_ID, `EFFECTIVE`, `EXPIRED`, EXPIRED_VALUE, EXPIRED_UNIT, INITIAL_BALANCE, AVAIL_BALANCE, `REMARK`, `STATE`, LATEST_ACTIVATE_DAY, FROM_MEMBER_CARD_ID)values('148484250576441344', 'F05', '144902165450399744', '冯小雨', '储值卡', '100元无折扣', '147018595541049344', '2018-05-08T00:00:00.000+08:00', '2018-06-08T00:00:00.000+08:00', 1, '', 100.0, 100.0, NULL, '已激活', NULL, NULL)
2018-05-08 10:48:07.894 [http-nio-7689-exec-9] DEBUG tcode[19000001] eql.cn.raiyee.liyun.service.back.dao.MemberCardDaoImpl.eql.addMemberCard.result:14 - 1

image
图:2018-05-08 00:00:00的时间存储后,偏移到了2018-05-07 11:00:00,发生时光倒流。

问题分析

我们之前同样使用阿里云的RDS,而且还是同一个实例,也同样使用eql来访问数据库,没有出现问题。怀疑是mysql驱动升级了的原因。
马上使用mvn dependency:tree分析mysql依赖,发现mysql版本确实不一样。切换会5.1.42的版本,问题解决。
image

继续深挖

理论上,mysql-connector-java:jar:6.0.3的版本,应该兼容低版本才是。可能是某些参数需要调整。所以,继续切回6.0.3版本,尝试添加时区的参数设置。只要添加serverTimezone=Asia/Shanghai就可以解决问题了。其中一篇博客mysql-connector-java 6.x 时区设置讲得很详细。
image
图:使用谷歌搜索相关文章

url=jdbc:mysql://test.go.easy-hi.com:33306/?useUnicode=true&&characterEncoding=UTF-8&connectTimeout=30000&socketTimeout=30000&autoReconnect=true&serverTimezone=Asia/Shanghai

最后方案确定

为了保持线上配置不动,最终权衡后,还是切换5版本驱动。

据说能开启上帝视角,赶紧入坑LISP

看了一篇微信文章推送,《请问你为什么学习LISP》。之前也看过左耳朵耗子:什么是函数式编程?。什么“没有状态就没有伤害”,“并行执行无伤害”,“Copy-Paste 重构代码无伤害”,“函数的执行没有顺序上的问题”,“MapReduce”等卖点,都没有真正打动我。直到看到一句话:学会了Lisp之后你有很大概率开启上帝视角,能站在一个相当高的高度去审视别的语言,这不是在吹牛,这是真的真的相当高。

妈蛋蛋,就冲着这“上帝视角”,我准备“入坑”了。

单元测试的原则:AIR、BCDE、FIRST、3R、3A、SOCKS、Right-BICEP(右臂二头肌)

今天随便翻了一下《阿里巴巴JAVA开发规范》,看到单元测试中提到了“AIR”原则,而我之前了解的是“FIRST”原则,不管什么原则都念叨一下总是有所裨益的,我都在这里归集一下吧。

AIR原则

好的单元测试必须遵守 AIR 原则。
单元测试在线上运行时,感觉像空气 (AIR) 一样并不存在,但在测试质量的保障上,
却是非常关键的。好的单元测试宏观上来说,具有自动化、独立性、可重复执行的特点。

  • A: Automatic (自动化)

单元测试应该是全自动执行的,并且非交互式的。测试用例通常是被定期执行的,执行过程必须完全自动化才有意义。输出结果需要人工检查的测试不是一个好的单元测试。单元测试中不准使用System.out来进行人肉验证,必须使用assert来验证。

  • I: Independent (独立性)

保持单元测试的独立性。为了保证单元测试稳定可靠且便于维护,单元测试用例之间决不能互相调用,也不能依赖执行的先后次序。 反例:method2需要依赖method1的执行,将执行结果作为method2的输入。

  • R: Repeatable (可重复)

单元测试是可以重复执行的,不能受到外界环境的影响。 说明:单元测试通常会被放到持续集成中,每次有代码check in时单元测试都会被执行。如果单测对外部环境(网络、服务、中间件等)有依赖,容易导致持续集成机制的不可用。 正例:为了不受外界环境影响,要求设计代码时就把SUT的依赖改成注入,在测试时用spring 这样的DI框架注入一个本地(内存)实现或者Mock实现。

FIRST 原则

image

  • Fast快速

测试如果跑得不够快,就不会让人想常常跑,不常跑的测试最后也就失去的它意义了。
所以实务上会使用mock工具,mock其他依赖的物件或环境,来加速测试的执行。

  • Independent独立

测试要相互独立,一个测试不会依赖其他测试,如果互相依赖的话,一个测试的失败会影响其他测试也跟着失败,那么在找问题点的时候将会变得更困难。

  • Repeatable 可重复

测试应该要可以在任何环境中重复执行。减少因环境因素而产生测试失败的问题。

  • Self-Validating 自我验证

测试程式应该要输出布林值。不管是测试成功或失败。
简单来说就是可以在测试报告很清楚的看到红灯(测试失败)或绿灯(测试成功)。

  • Timely及时

撰写测试要及时,最好是在写产品程式前先写(TDD的概念)。

BCDE 原则

编写单元测试代码遵守BCDE原则,以保证被测试模块的交付质量。

  • Border,边界值测试,包括循环、 特殊取,边界值测试包括循环、 特殊取特殊时间点、数据顺序等。
  • Correct,正确的输入,并得到预期结果。 ,正确的输入并得到预期结果。
  • Design,与设计文档相结合,来编写单元测试。 ,与设计文档相结合来编写单元测试。
  • Error,强制错误信息输入(如:非法数据、异常流程业务允许等),并得 ,强制错误信息输入(如:非法数据、异常流程业务允许等),并得到预期结果。

测试边界原则 - CORRECT

  • Conformance - 一致性 (值是否符合预期的格式?)
  • Ordering - 有序性 (一组值的顺序是否符合预期?)
  • Range - 区间性 (值是否在一个合理的最大值和最小值的范围内?)
  • Reference - 引用性 / 耦合性 (代码是否引用了一些不受其直接控制的外部因素,由这些外部因素所引入的前置条件或后置条件所造成的影响是否符合预期?)
  • Existence - 存在性 (值是否存在(例如:非 null,非零,存在于某个集合中等)? )
  • Cardinality - 基数性 : 计数性 (是否恰好有足够的值?(重点关注 0-1-n 原则,问题往往发生在这三个边界上。))
  • Time - 时间性(绝对时间及相对时间)- (所有事情是否按顺序发生?是否在正确的时间?是否及时?)

3R原则

为了提高开发人员的代码质量,编写高质量的单元测试,要遵守3R(Responsible, Reliable, Repeative)原则,具体含义如下:

  • Responsible: 谁开发谁负责测试,在哪里开发就在哪里测试。

开发在完成一个方法,或者一个类之后,就要及时得进行单元测试;不能在对应方法或类的调用处进行测试,比如两个模块A、B,A是基础模块,为模块B提供服务,那么所有A模块的单元测试case都应该在A模块的内部进行测试。

  • Reliable: 测试case要可靠,并且是值得信赖的,对于底层的任何改动都要能够及时感知。

为了使得测试用例尽量可靠,就要减少mock的使用(对于第三方的调用可以使用mock),对每层代码的测试都要完全依赖于下层,不能mock下层逻辑。因此引入递进集成的概念,比如测试DAO时要连接真实的数据库,测试Service时要使用真实的DAO、DB, 测试Controller层的代码,要使用真实的Service、DAO、DB,以此类推。这样就可以最大限度的提高case的可靠性。

  • Repeative: 所有单元测试用例都要能够重复运行。能够重复运行就能够进行回归测试、覆盖率统计等等。

必须要做到case间完全解耦,没有任何的依赖,这包括和数据库的依赖以及第三方的依赖。case解耦可以通过准备测试数据、mock第三方调用来解决。

推荐的测试结构:AAA (Arrange - Act - Assert),或GWT (Given - When - Then)

3A原则原本是单元测试用例编写时应该遵循的基本原则

  • Arrange: 初始化测试对象或者准备测试数据

arrange 初始化测试数据,就是造数据,这里的数据有我们输入的数据,也有目标接口所涉及的资源,比如hr系统中的用户信息,我们必须先有几条人员的详细信息才能去测获取人员信息的接口(当然只是正常的流程,我们有时候还需要清掉数据以便测试资源为空的情况);

  • Act : 通过不同的参数来调用接口,并拿到返回

act 调用接口,传入输入数据;

  • Assert: 必须做断言,否则用例就没有任何意义了

assert 断言, 对返回的资源信息进行断言,比如获取用户信息的接口返回了用户信息之后,我们要判断返回的用户是不是我们想要的那个用户,我们获取的是李雷的信息,接口如果返回韩梅梅,那么接口的逻辑就是不对的;

提升可测试性的袜子模型-SOCKS,或者SOCK

  • Simple:一个一百行的程式和一个一千行的程式哪一个比较好测?应该是前者比较容易测试。同理,一个模组化做得很好的程式,和一坨义大利面相比,前者比较容易理解,当然也比较容易测试。
  • Observable:俗话说「可观察才可测量」,如果程式的内在结构或是外在行为不容易观测,那么这个程式就不容易测试。
  • Control:能够控制软体的行为,也会让测试变得容易许多。
  • Knowledge:对于待测程式的相关知识越充足,也会提升待测程式的可测试性。例如,假设乡民们要测试isPrime(int value)这个判断一个整数是否为质数的函数。如果乡民们连质数的定义都不了解,当然也无法帮isPrime()写测试案例(无法定义出expected result啊)。所以,提供越清楚、正确的说明文件或是任何有关待测程式的知识,便可提高程式的可测试性。
  • Stability: 功能稳定的系统才方便单元测试。

理想的单元测试金字塔与现实中的冰激凌蛋筒

image
Testing pyramid

image
Testing ice cream cone

image
Testing hourglass

奕起嗨并发串馆问题分析

最近一个月内多次接到用户报告,说会员在登录页面上,显示的是另外一家瑜伽馆的名字和图片,也有在订课时发生类似情况,我们称之为“串馆”。
image
image
图:2017年10月25日报告,登录时,串馆情况

image

图:10月23日报告,从拉姆瑜伽公众号进入的,馆名是优美甜缘的,课表是蝉悦瑜伽的,这三家都是付了费的,都是南方的。

image
之前由于串馆问题只发生在南方,因为南方商户较多,并发冲突的可能性更大。但是之前从日志中没有找到相关的问题,有可能是商户多日志量太大了。所以把怀疑的重点放在了Nginx根据TCODE查找TID的LUA脚本上,以及JAVA端的多租户连接池上。
LUA的一个排查重点,就是没有加local关键字的全局变量,JAVA多租户连接池的排查重点是多线程取连接问题。所有代码都走过一遍后,仍然没有发现有效线索。业务代码涉及TCODE代码很少,也没有看出可能的问题。
因为一直没有报告北方商户出现串馆问题,所以也怀疑了是不是可能Nginx本身的BUG,然后对比了南北的Nginx的版本号,发现确实不一样。北方openresty/1.11.2.1南方是openresty/1.9.15.1,本想将南方的Nginx升级成北方一样的版本号,看看是否仍然有串馆问题。
今天突然北方也报告了串馆,立马跟踪日志,很快张存鑫同学从日志中看到了一个问题时间点(12:50)的异常,然后进行关联跟踪分析之后,果然发现串馆的问题应该就是这个异常导致的。

image
image
问题分析清楚后,只需要再仔细思考一下这段代码逻辑,直接重构了,删除了一些无用代码后,改成以下这样,然后上线,经过一段时间的观察后,确认困扰了将近半个月的偶发性串馆问题自此修复。
image

问题总结:对于商户的路由逻辑,应该由Nginx统一通过HTTP头传递给YOGA应用,YOGA应用的过滤器中,应该从HTTP头中取出商户路由并设置到本地线程变量,避免使用会话保存等其他形式。另外,应该避免因为异常导致商户路由设置代码被跳过。

服务最好成双成对

image
刚刚看到一篇文章,讲运维的,开篇就是这么一张图,然后配了文字是“服务最好成双成对”,哈哈,这两个养眼美女,我会盯上看一会,如果走了其中一个美女呢,我依然会盯上看几眼,但是美女走光了呢,靠,没得看了,我也只能走了。

如果没有成双成对呢,那就是单身狗了,技术上叫做SPOF(Single Point of Failure)。SPOF就是独苗,就是单身汪,挂了就没了。

想到以前我们上初中时(92年左右)的一个情景,那会学校里面没有自来水,吃水都是靠一口井来解决。然后大热天,实在太热了,大家想到一个办法,用吊水的盐水瓶系上长长的绳子,趴在井口上去吊凉凉的井水喝。喝的实在太爽,很快几个男孩子都去这么干。可是很危险啊,学校肯定不允许啊,万一掉到井里面去了咋办,那口井感觉很深的样子。所以大家都是偷偷地去吊井水,还是有一次被校长发现了。校长喝令几个男生站成一排,很严厉地再次讲了这么做的危险。然后一个一个地问,你家几个娃,哦,两个男娃,你走吧,你家呢,啊,就一个独苗,对不起,这时候校长就往后退半步,两脚稍微分开一些,身子稍微下层一点,伸出双手,合在一起吹吹神仙气,然后张开手臂,运起力气,吧唧一声,一记响亮的夹耳光狠狠地打了下去,然后被打的独苗虽然脸被打得红红的,也狠狠地憋住不哭,但是也禁不住已经两眼泪水汪汪了。那可是令人恐惧,令人印象深刻的校长打人专利-夹耳光。

所以,从初中我就知道,不是独苗还有这好处,因为我有一个哥哥,所以就免于因为吊井水而被夹耳光的痛苦(虽然还有其他时候被夹耳光)。

回到技术上,服务成双成对,是最基本的生产环境的集群配置了。当然,为了配合这个成双成对,还得引入前面的负载均衡技术,引入VIP虚拟IP技术。成双成对仅仅是最初的开始。

门户清理1之RDS数据清理

RDS磁盘空间告警信息:

【阿里云】华东2(上海)的云数据库RDS版实例ali-hd2-hi-rds-t01,磁盘使用率平均值超过80,请登录云监控关注
【阿里云】华北2(北京)的云数据库RDS版实例ali-hb2-hi-rds-t01,磁盘使用率平均值超过80,请登录云监控关注

持续收到了一周了,看来得进行门户清理了。

首先看看监控图

image
图:南方中心RDS磁盘空间
image
图:北方中心RDS磁盘空间

然后就是清理了

  1. 在生产环境上做清理,得胆大心细,先做好备份,删之前做好核查,很容易删的心惊肉跳。
  2. 释放不再使用的database,一下子干掉150多个废弃商户的database。
  3. drop不再使用的表。
drop table databasechangelog;
drop table databasechangeloglock;
-- tt_d_funcright和tt_f_role_right商户库不需要,中心库需要,注意只删除商户库。
drop table tt_d_funcright;
drop table tt_f_role_right;
drop table tt_d_wx_msg_template;
drop table tt_l_mbrcard_times;
drop table tr_f_db_sp; 
drop table ts_f_stat;

truncate table tt_f_file;

drop table tt_f_schedule_rule_bak01;
drop table tt_f_schedule_rule_bak02;
drop table tt_l_mbrcard_chg_bak;
drop table tt_f_card_rule3;
drop table tt_f_card_rule4;
drop table tt_f_wx_msg_article;
drop table tt_d_wx_msg_handler;
  1. 查看数据量大的表,进行清理。
-- 查询行数大于1万的表
SELECT TABLE_SCHEMA,TABLE_NAME,TABLE_ROWS,TABLE_COMMENT FROM INFORMATION_SCHEMA.TABLES  where TABLE_SCHEMA not in ( 'information_schema', 'mysql', 'performance_schema' ) and table_rows > 10000 order by table_rows desc

image
图:行数大于1万的表

delete from tt_f_schedule_rule where schedule_id in (select schedule_id from tt_f_schedule where start_time < '2018-01-01 00:00:00');
delete from tt_l_remark where operate_time < '2018-01-01 00:00:00';
delete FROM tt_f_card_rule  where state = 0;

最后的效果

心惊胆战的drop表,胆战心惊的drop库(包括手工drop,程序drop),干了一天下来,看下效果,磁盘空间还是降了一些的。
image
图:北方RDS磁盘空间
image
图:南方RDS磁盘空间

码农应知应会1:延迟的不同数据级别

为了降低对Redis的频繁读取,也提升效率(包含序列化/反序列化),我打算在系统中实现二级缓存,L1用Guava Cache来做,L2用Redis来做(为了同步),然后翻阅了一些文章,从Latency Numbers Every Programmer Should Know发现如下数据,暂且参考记录一下。

Latency Comparison Numbers (~2012)
----------------------------------
L1 cache reference                           0.5 ns
Branch mispredict                            5   ns
L2 cache reference                           7   ns                      14x L1 cache
Mutex lock/unlock                           25   ns
Main memory reference                      100   ns                      20x L2 cache, 200x L1 cache
Compress 1K bytes with Zippy             3,000   ns        3 us
Send 1K bytes over 1 Gbps network       10,000   ns       10 us
Read 4K randomly from SSD*             150,000   ns      150 us          ~1GB/sec SSD
Read 1 MB sequentially from memory     250,000   ns      250 us
Round trip within same datacenter      500,000   ns      500 us
Read 1 MB sequentially from SSD*     1,000,000   ns    1,000 us    1 ms  ~1GB/sec SSD, 4X memory
Disk seek                           10,000,000   ns   10,000 us   10 ms  20x datacenter roundtrip
Read 1 MB sequentially from disk    20,000,000   ns   20,000 us   20 ms  80x memory, 20X SSD
Send packet CA->Netherlands->CA    150,000,000   ns  150,000 us  150 ms

Notes
-----
1 ns = 10^-9 seconds
1 us = 10^-6 seconds = 1,000 ns
1 ms = 10^-3 seconds = 1,000 us = 1,000,000 ns

Credit
------
By Jeff Dean:               http://research.google.com/people/jeff/
Originally by Peter Norvig: http://norvig.com/21-days.html#answers

Contributions
-------------
'Humanized' comparison:  https://gist.github.com/hellerbarde/2843375
Visual comparison chart: http://i.imgur.com/k0t1e.png

记录、传播、对话:开始写博客

引用玉伯的话来说:博客就三个关键词:记录、传播、对话。能满足这三个需求的系统,就是博客
于是乎,正式打算利用github的issue来正式写博客了。虽然从毕业到现在已经好些年了,写博客的习惯一直没有很好的养成,但是一直未放弃,一直是念念不忘。带团队的过程中,我也鼓励小伙伴们把一些感想,一些发现问题分析问题总结问题的收获,都及时写下来,后面可以回味,可以分享,可以大总结。

性能优化之套路:简化、异步、去锁、复用、零拷贝、批量

image
图:性能优化在路上

避免过早优化

业务都还没有理顺,流程还没有抽象,就考虑优化,有点过早。

简化

把业务进一步精简,去除伪需求。

异步

也就是消除同步,去除阻塞。

去锁

也就是Lock Free。使用CAS,Ringbuffer等。

复用

池化一些资源(线程池、连接池、XX池)

零拷贝

使用ByteBuffer等,例如Netty。
参考文章:走进科学之揭开神秘的"零拷贝"

批量

一个个处理,费时费力。一批处理时,可以尝试合并,压缩等。

十万个为什么?为什么选择GO

Linux/Unix设计**之FLAT原则

image
图片来源于500px

何谓FLAT原则

即“使用纯文本来存储数据”。为什么呢?因为这样可以使数据具有最大的可移植性。

遭遇问题

在使用Apache POI来生成Excel时,当修改Excel表单的名字时,导致保存后的Excel打开,图标数据丢失,打开报告错误:
image

image

原因是图表数据中有对Excel文件名的引用:
image

原因找到了,可是怎么去修复呢,完全不知道POI图表的哪个API能设置这个啊。这时候连万能的谷歌都不好使了,毕竟这个是非常小众的场景啊。

从放弃到再试

简单尝试了去修正图标对表单名的引用,但是因为找不到正确的API,都无功而返。只好先把精力放在业务相关上,暂时不允许表单改名了。

周末在家里想着,XLSX不就是OOXML格式么,其实就是一个普通的zip格式啊,完全可以解压开来,然后去看看图表数据中对于表单名的引用是存储在哪里的。
image

然后全文搜索一下,马上就找到了位置了:
image

分析XML结构,寻找对应的POI的API.

在线XML工具分析一下chart1.xml的文档结构,如下:
image
发现文档名的位置在:chart->plotArea->barChart->ser->val->numRef->f下面。然后就顺藤摸瓜,很快找到了对应的API了,成功完成表单名的改名动作。

/**
 * 修正图表中对于表单名字的引用。
 *
 * @param sheet        EXCEL表单。
 * @param oldSheetName 旧的表单名字。
 * @param newSheetName 新的表单名字。
 */
public static void fixChartSheetNameRef(Sheet sheet, String oldSheetName, String newSheetName) {
    val drawing = sheet.getDrawingPatriarch();
    if (!(drawing instanceof XSSFDrawing)) return;

    for (val chart : ((XSSFDrawing) drawing).getCharts()) {
        for (val barChart : chart.getCTChart().getPlotArea().getBarChartList()) {
            for (val ser : barChart.getSerList()) {
                val val = ser.getVal();
                if (val == null) continue;

                val numRef = val.getNumRef();
                if (numRef == null) continue;

                val f = numRef.getF();
                if (f == null) continue;

                if (f.contains(oldSheetName)) {
                    numRef.setF(f.replace(oldSheetName, newSheetName));
                }
            }
        }
    }
}

总结

因为刚刚回看了Linux/Unix设计**,想到XML其实也是FLAT原则的一种体现。如果不是OOXML的格式,而是以前的xls,那么就没那么容易肉眼去分析格式,去寻找对应的API了。感谢FLAT,感谢纯文本。

Elon Musk的第一性原理思维

怎么来了这么一个词汇

image

2018年02月07日凌晨,SpaceX旗下的新型火箭,也是现役运力最强火箭“重型猎鹰”(Falcon Heavy)首飞成功。

"重型猎鹰"火箭有多厉害,看看他打破了多项世界纪录:近地轨道运载能力(LEO)63.8 吨,比目前纪录保持者德尔塔 IV 重型火箭的 28.8 吨提高了两倍多;地球同步轨道运载能力(GTO)26.7 吨;火星轨道的运载能力16.8 吨。"重型猎鹰"火箭起飞时 27 (9*3)台梅林 1D 发动机同时工作,可以提供高达 2280 多吨的起飞推力。

这一天,三联生活周刊的公众号也推送它的新文章:SpaceX再创历史|埃隆·马斯克:无限创想与意志胜利,里面提到:

马斯克也开始诠释打上他个人烙印的硅谷极客精神,叫思维的“第一性原理”(First principle thinking),也就是打破一切知识的藩篱,回归到事物本源去思考基础性的问题——这与他的物理学出身又密不可分。

然后我就被这个“第一性原理”的字眼吸引住了,然后开始了一系列的谷歌搜索。

Elon Musk 采访中的原文解释

我们运用第一性原理,而不是比较思维去思考问题是非常重要的。我们在生活中总是倾向于比较,对别人已经做过或者正在做的事情我们也都去做,这样发展的结果只能产生细小的迭代发展。

第一性原理的**方式是用物理学的角度看待世界,也就是说一层层拨开事物表象,看到里面的本质,再从本质一层层往上走。

媒体在采访 Elon Musk 时,提到的一个运用第一性原理,发现商业机会的例子是这样的

传统电池组,市场平均价格是600美元/千瓦时,主要电池供应商是松下。马斯克通过第一性原理发现,如果从伦敦金属交易所购买锂电池组的原材料组合在一起,只需要80美元/千瓦时。
他从中发现有巨大的价格差距,所以特斯拉在2013年开始自己建立了电池厂,今年一月份开始大规模生产,投产之后电池的价格可以下降30%,每年可以支持150万辆电动车对电池的需求。这就是他对第一性原理的一个应用。

与其根据参照物去推论,不如我们把问题分解成几个最基本的事实,然后检查每个事实部分。即使问题已经解决,我们还是要从问题最基本的组成部分入手,重新审视是否有更好的解决方案。 这就是马斯克一直推崇的用第一性原理思考问题,而不是类比。

马斯克,何许人也?

image
注:图片来自blogspot的一篇博客

摘自维基百科:伊隆·马斯克(英语名:Elon Musk,1971年6月28日-)是一名美籍和加籍企业家。他于南非出生。因他为SpaceX的创办者,及特斯拉汽车和PayPal(原X.com)的联合创办人而闻名。目前,马斯克担任Space X的首席执行官兼首席设计师、特斯拉汽车首席执行官兼产品架构师、以及SolarCity的主席;与此同时,他还是现代第一辆可行电动车Tesla Roadster的联合设计者之一。

罗辑思维讲的第一性原理

摘自一篇简书

加拿大华人老喻总结的一个公式 —— 为一切问题提供广泛可行的解决方案:

你的成就=核心算法×大量重复动作²

所谓“核心算法”,就是你的“第一性原理”。就是你始终揪住它不放松的东西,做任何事都是使用这个“核心算法”,在任何选择关头,不管别人怎么说,怎么看,都用这个原理做决策。

所谓“大量重复的动作”,就是一旦启动开始重复地做,笨笨地坚持往下做。每多做一次,就会比其他人积累更大的优势,而且这个优势是指数级积累的。

还是那几个概念:刻意练习一万小时定律;七年就是一辈子;一次只做一件事;把运气交给上天自己只管练就一颗坚强的心。

这些道理在今天已经成为普世的精进理论,但如果核心算法是飘忽不定的,那再多大量重复的动作,也难以换来自己期望的成就。揪住核心算法不放,这一点比什么都重要。

量子力学中的术语

摘自思维浩的解释

“第一性原理”是一个量子力学中的一个术语,意思是从头算,只采用最基本的事实,然后根据事实推论。

在MUSK开发电动车案例中,有些人会说现在的电池组真的很贵,而且未来价格也不会低到哪去,因为它过去一直都是那么贵。这些人会说电池组每千瓦小时要烧掉600美元。而且未来也不会好到哪里去的。有史以来,成百上千的权威人士都声称一个行业、设计图样、一个实体或者一个想法都已经达到了它的顶峰。在这种思路的影响下,电池组再也没有改进的空间,或是以更低的成本生产出来。这些人说的话都被汹涌扑来的创新证明是无稽之谈。或者,他们可以拍屁股想出一些微不足道的改进。”

然而从第一性原理出发,我们问:电池的材料构成都是什么?这些材料的现货市场价值是怎样的?电池是由碳、镍、铝、其他用于分离的聚合物还有一个金属罐组成的。如果我们去伦敦金属交易所购买这些金属材料,然后把这些材料分解一下,那么这些组成电池组的材料每种又值多少钱呢? Musk在采访中说道,他将电池组分解成最基础的材料组成部分:碳、镍、铝、其他用于分离的聚合物还有一个盒子。这些都是电池组重要的组成元素,这是形成一块电池的最基本的事实。从那里,每个部分都可以优化、改进,最终的优化程度也取决于解决问题这些人的聪明才智。于是,现在电池的价格就变成了每千瓦小时80美元。

与其根据参照物去推论,我们应该把问题分解成几个最基础的事实,然后检查每个事实部分。即使问题已经解决,我们还是要从问题最基本的组成部分入手,从新审视是否有更好的、可能的解决方案。

这就是MUSK一直推崇的用第一性原理思考问题,而不是类比

为什么用“第一性原理”这个字眼

摘自梦之轮回

据说这是,来源于“第一推动力”这个宗教词汇。

第一推动力是牛顿创立的,因为牛顿第一定律说明了物质在不受外力的作用下保持静止或匀速直线运动。如果宇宙诞生之初万事万物应该是静止的,后来却都在运动,是怎么动起来的呢?牛顿相信这是由于上帝推了一把,并且牛顿晚年致力于神学研究。

现代科学认为宇宙起源于大爆炸,那么大爆炸也是有原因的吧。所有这些说不清的东西,都归结为宇宙“第一推动力”问题,它可能由某种原理决定,这个原理可以称为“第一原理”。

爱因斯坦晚年致力于“大统一场理论”研究,也是希望找到统概一切物理定律的“第一原理”,可惜,这是当时科学水平所不能及的。现在也远没有答案。

但是为什么称量子力学计算为第一性原理计算?大概是因为这种计算能够从根本上计算出来分子结构和物质的性质,这样的理论很接近于反映宇宙本质的原理,就称为第一性原理了。

打个比方,在人文领域中,一个国家,其出发点一定是每个公民都觉得不言而喻的公理,科学的方法可用于治国,让建国也基于最简单的公理,公民能理解,国家才会牢固。

简单的说,再复杂的知识体系,一定要归纳成最简单的几条不言而喻的公里,这就是第一性原理。

从商业的角度进行理解

摘自不要拦我的回答

商业角度总结如下:

  1. 第一性定理思维是一种演绎法思维,但与追本溯源法是不同的;
  2. 用第一性原理思维常常能带来颠覆式创新,而这一点,是其他思维方法很难实现的;
  3. 第一性原理思维有局限性,避免这种尴尬的最佳方法就是搜集更多的第一性原理;
  4. 跨学科学习是搜集第一性原理的捷径。

什么是第一性原理思维?其实是一种演绎法思维。

我们不妨从它的定义开始。

“第一性原理”是物理学的一个专业名词,是指某些硬性规定或者由此推演得出的结论。与之相对的则是“经验参数”,经验参数是通过大量实例得出的规律性的结论。(来自百度百科)

简单来说,第一性原理就是指一个定理,或者定理的推论。那么,一个物理学上的概念,为什么会被埃隆·马斯克如此推崇?又是如何被运用到商业中呢?仔细研究下定义,你会发现“第一性原理”和与之相对的“经验参数”,其实是人类的两种思维方式——演绎法与归纳法。
“第一性原理”是一种演绎法思维,由1个或多个定律推演而来,或者它本身就是一个定律;
“经验参数”是一种归纳法思维,由N个已知的数据或现象,推论出一个规律。
两者有何区别呢?举个例子说明,船长观察到前方有座冰山:
image

一个善用归纳法思维的船长A会这么想:我上次遇到的冰山在水面下还有一个大冰山,我上上次遇到的冰山也是这样,泰坦尼克号遇到的冰山也是一样的,所以这个冰山下面一定也会有一个大冰山,看来我要小心地绕过去。
一个善用演绎法思维的船长B会这么想:观察到这个冰山是移动的,说明它是浮在水中。根据浮力原理,F浮=G排(即物体浮力等于物体下沉时排开液体的重力),所以水面下一定藏有一个体积足够大的冰山,看来我要小心地绕过去。

虽然殊途同归,但是很明显这是两种完全不同的思维方式。(这句话很重要,后面我还会提到)

人们总是更习惯于用归纳法思维来总结规律和解决问题。比如:

员工A销售业绩突出,电话量很高;员工B销售业绩突出,电话量同样很高;……
所以得出结论,电话量是影响销售业绩的一个重要指标,于是KPI加上了电话量的考核。

这是我们工作中很常见的一种现象。但并不是**教育和文化所带来的特有现象,而是全人类都更倾向于使用归纳法。

如果用演绎法,又会如何思考这个销量问题呢?

销售是什么?
是让产品与客户接触,激发出客户购买欲望,并且实现成交的一个过程。
由此可见,销售分为3个阶段:
step 1. 让客户接触到产品→step 2.激发出购买欲望→step 3.成交过程
其中step 1又可以拆分成官网展示、电话推销、样品寄送;同样step 2也可以拆分为官网广告文案、电话推销术语等环节。
因此,如果是以电话销售为主的产品,想要提高step 1和step 2的产出,可以增加电话量。(注意,这个时候的演绎法的结论与归纳法思维再次殊途同归。标记为结论一。)
但是,换个角度的话,你还能发现,为什么一定要用电话销售的形式呢?step 1和step 2是否可以变成新媒体营销,是否可以变成电商营销,是否可以变成网红营销?(此时,演绎法与归纳法的区别就出现了。标记为结论二。)

看到这里,你应该知道了归纳法与演绎法这两种思维的不同之处。

归纳法只能对已发生的事实总结规律,常常会忽略尚未在内部发生的新生事物。而颠覆式创新却是一种未发生的事情,所以归纳法思维是很难创造出颠覆式产品的。

只要有演绎法思维就能成为埃隆·马斯克吗?当然不是!

自从埃隆·马斯克提出第一性原理之后,互联网圈里立刻流传开来,网上有很多文章讲解“第一性原理”到底是什么。我看到很多人这样诠释:

第一性原理思维是一种“追本溯源”的思考方式,万事都要寻找到根本性问题,也可以叫本质思考法。

然后读者们马上看懂了,新瓶装旧酒嘛,工作中已经都在用了。我还看到丰田公司有位员工在某篇文章底下评论,“我们一直在这样做,制造业上叫根因法。”意思是说,这个大家早就知道了,根本不是埃隆·马斯克的独门秘籍。

但是很可惜,这个解释其实是不对的,误导了很多人。

第一性原理思维≠追本溯源、根因法、本质思考法

回想下之前提到的两个例子,冰山案例与销售案例。从中我们能发现两条重要结论:

第一条,归纳法与演绎法有时会出现一致的结论(参考:冰山的结论与电话量的结论一)。这说明,同样的结论,不一定思维方式是一样的。
第二条,使用演绎法出现了两个结论(参考:电话量的结论一和结论二)。这说明,同样的思维方式,不同的思考深度和角度可能会带来不同的结论。

根据这两个重要结论,我们会有些新发现——

  1. 同样的结论,不一定思维方式是一样的
    可推导出,即使第一性原理思维和追本溯源法的思考结论常常一样,也不能说明他们两个是同一种。
  2. 同样的思维方式,不同的思考深度和角度可能会带来不同的结论
    可推导出,同样是演绎法,第一性原理思维和追本溯源法采用的是不同的角度,因此结论可能会出现不同。

也就是说,第一性原理思维是演绎法思维的一种,追本溯源、根因法也是演绎法思维的一种,但两者不是同一个。并且如果两者的思考角度不同,就可能会带来不同的结论。

这就是为什么跟埃隆·马斯克一样有深度有广度的商业大咖们,没能像埃隆·马斯克一样。即使他们都是善用演绎法思维的人,也是更习惯于使用“追本溯源”演绎法。第一性原理思维这种演绎法思维只有极少数人知道。

(这里插入一句,并不是思考角度不同都会带来结论差异。大家之所以弄混第一性原理思维和追本溯源思维,恰恰是因为这两者的结论经常相同,才会误以为两个是同一种思维。)

那么,这两种演绎法,有何不同呢?用鱼骨图能很直观的看出区别。

追本溯源法是从问题出发,一步步分析问题背后的原因,直到找出最终原因(有1个或者N个)。
image
第一性原理思维法则是从原理出发,一步步往前推演,直到找出适合该问题的解决方法(有1个或者N个)。
image

由此可见,第一性原理思维和追本溯源思维是不同的,一个是从问题出发,推演出根本原因;一个是从原理出发,推演出解决方法。

我们再仔细分析下,为何出发点不同,会带来差异性的结果。
image
如果用追本溯源思维,从问题出发,那么它能一步步发现子路径1→路径1→第一性原理;但是它很难发现路径2、路径3、路径4,因为这种思维方式是要从问题开始推演的。而新路径(创新)恰恰隐藏在路径2、路径3、路径4之中。这就是用第一性原理思维常常能带来颠覆式创新的根本原因!

这才是埃隆·马斯克极度推崇第一性原理思维的真正原因。

如果看过《创新者的窘境》这本书的话,可能对这句话的感触会更深一点,因为能更清楚企业创新的重要性。

再看看上面那张图,聪明的朋友可能会发现一个问题,那就是第一性原理思维的局限性。

任何一个原理或定律,都不可能解决世界上所有的问题。因此,如果出问题的地方不在你的第一性原理的体系中,那么采用第一性原理的思维方法,是找不到解决方案的。

怎么办呢?

不断收集,跨学科学习

第一性原理是一个定律,或是一个模型。搜集的越多,那你能解答的问题也就越多。

巴菲特最重要的伙伴查理·芒格就曾说过,他热爱学习,尤其是跨学科学习,通过这种方式他搜集了100多种思维模型,他就是用这些模型来制定投资策略的。

感谢芒格给我们指出了一条捷径,培养第一性原理思维的捷径就是跨学科学习

不要只看商业书籍,其实每个学科里都散落着大智慧,物理学的力学定律、生物学的进化论、经济学的看不见得手……很多看似与商业不相干的定律,都是很好的第一性原理,认真反复推演之后,一定能发现适用于自己的思维模型。

优雅代码实践5 之 飞奔的定时任务

飞奔的定时任务

突然看到一段代码,看完就是那种感觉,闭上眼睛,眼前就一行行文字在飞奔,很Happy地在飞奔。

@Override public void run(int periodSeconds) {
    log.info("->->->->->飞奔的定时任务->->->->->飞奔的定时任务->->->->->飞奔的定时任务->->->->->飞奔的定时任务->->->->->");

    val now = DateTime.now();
    val targetTime = now.minusSeconds(periodSeconds);
    if (now.getHourOfDay() == targetTime.getHourOfDay()) {
        return; // 每小时执行
    }
    log.info("->->->->->钻研的定时任务->->->->->钻研的定时任务->->->->->钻研的定时任务->->->->->钻研的定时任务->->->->->");

    val memberCards = memberCardLatestActivateDayDao.queryUnactiveMemberCardsWithLastedActivateDay();
    for (val memberCard : memberCards) {
        tryActivateMemberCard(memberCard);
    }
}

写这个代码的不一定是高级程序员,但我想一定是一位快乐的码农。这个有点让我想起"Go语言趣味教材:并发不是并行"中囊地鼠gopher很Happy地运书的一张图片
image
囊地鼠运书,运得很快乐啊。也感觉像一枚枚成功飞向太空的小火箭(猎鹰重型首射的有效载荷是代号为Starman的红色特斯拉跑车Tesla Roadster),也飞得很Happy。
image

GIT看一下这段代码的提交记录,还是热热乎乎的代码,提交注释是test
image

于是我就问原始作者,

问:小雨,你打算什么时候去掉它?
答:我不准备去掉它,留着很好啊。
问:五段重复的话,中间大量的箭头字符,留着有什么意义?
答:打日志的时候会看得很清楚啊。
问:万一大家也都跟风,也都这么的打印,那你还能从日志中看定时任务看得很清楚呢?
答:……

也许我担忧过头了,只要诸多打日志的代码中,有且仅有这么一处这么打印的话,问题倒也不大,而且还感觉有『某种新意』。但是,经常是『好事不出门,坏事传千里』,这毕竟不是一个好的打印日志的方式,开启了这么一个方便之门后,后面其他人活着新来的同学也都这么打印,就会导致满屏幕火箭飞了,到底是飞的定时任务的小火箭,还是飞的其它业务的火箭,就不是那么容易区分了。而且,大概率还会嘟嘟说,组长就是这么写代码的,那就不好了。

调试用的日志

明显“飞奔的定时任务”属于调试性的日志,可以用debug级别。另外,靠肉眼去看日志,只适合在开发环境,在生产环境大量访问情况下,这种打印日志的方式,只会造成日志大小的无意义的增长。
如果不删除,至少所以建议的改动如下:

@Override public void run(int periodSeconds) {
    log.debug("定时任务检查开始");

    val now = DateTime.now();
    val targetTime = now.minusSeconds(periodSeconds);
    if (now.getHourOfDay() == targetTime.getHourOfDay()) {
        return; // 每小时执行
    }
    log.debug("定时任务执行开始");

    val memberCards = memberCardLatestActivateDayDao.queryUnactiveMemberCardsWithLastedActivateDay();
    for (val memberCard : memberCards) {
        tryActivateMemberCard(memberCard);
    }
}

改了:

  1. 级别从info改成了debug;
  2. 去除了日志中的噪音,只打印日志的初心。

JAVA异常好虽好,就是SLOW一个量级

这两天我升级了缓存类库westcache和SQL类库eql,以使得二者更加方便的集成,比如以下写法:

@EqlerConfig
public interface DictDao {
    @WestCacheable // westcache注解
    @Sql("select id, name, addr from cache_dict") // Eql注解
    List<CacheDictBean> selectAll();
}

我很快就把二者集成的实现写好了,但是,发现应用启动由原来的大概50秒,慢吞吞地变成了5分多钟,OMG,要疯了,这还受得了。
很快,我就定位到了问题,原来是westcache里面的有一处实现,利用了异常NoSuchMethodException来查找方法是否在一个class中存在,我意识到这很可能是一个问题,因为我早就耳闻异常造成的性能低下。我尝试将它改造成不使用异常的方式来实现,改完后,再测试,应用启动时间就马上降下来了,恢复到原来差不多的时间。
于是,我单独写了一个基准测试来模拟一下两种情况,形成一个比对,运行结果如下:

Benchmark                                                   Mode  Samples        Score       Error  Units
o.n.s.s.MethodSearchWaysCompare.methodNotFoundException    thrpt       10   470604.189 ± 80316.210  ops/s
o.n.s.s.MethodSearchWaysCompare.methodsSearch              thrpt       10  1295466.093 ± 34110.967  ops/s

可以看到,不使用异常的吞吐量比使用异常的快一个数量级。可见,异常在循环中,或者高并发处理中,还是要慎用,否则很可能就成了性能杀手。

优雅代码实践3 之 无码之码

image

是男是女的忧伤

  1. X年X月X日,项目X中:
    小明:师傅,用户表中的性别字段1和0,哪个表示男哪个表示女呢?
    师傅:1表示女,0表示男。
    新人:哦,知道了。(思索片刻,然后顿悟了,原来组内的女生都亭亭玉立,男生大多大腹便便。)

  2. Y年Y月Y日,项目Y中:
    小明:组长,客户表中的性别字段1和0,哪个表示男哪个是女呢?
    组长:1表示男,0表示女。
    小明:了解了。(发呆片刻,然后理解了,原来1是带把的,0是不带把的。)

  3. Z年Z月Z日,项目Z中:
    小明:兵哥,联系人表中的性别字段M和F,哪个表示男哪个表示女啊?
    兵哥:Male和Female啦。
    小明:哦,原来是英语单词首字母啊。

  4. S年S月S日,项目S中:
    小明:咦,这个项目中的性别,竟然直接就用“男”和"女"表示,终于不用问人了。

SQL的忧伤

  1. 在SQL中使用CASE WHEN THEN进行硬解码(MySQL),看到那么多优惠类型编码,我的内心是崩溃的,记不住啊记不住。
-- [getEarningsDetailLogList]
SELECT U.NICKNAME AS 'memberName',
       U.MOBILE AS 'memberMobile',
       mc.MBR_CARD_ID,
       c.CARD_NAME,
       mc.CHANGE AS 'changeLog',
       (CASE WHEN mc.LOG_TYPE = '105' THEN '续卡'
             WHEN mc.LOG_TYPE = '112' THEN '作废卡'
             ELSE '开卡'
         END) AS 'logType',
       mc.PAY_TYPE,
       DATE_FORMAT(mc.OPERATOR_TIME, '%Y-%m-%d') AS 'operatorTime',
       DATE_FORMAT(mc.OPERATOR_TIME, '%Y-%m') AS 'operatorTimeSub',
       CASE WHEN c.STORED_MONEY IS NULL THEN FALSE ELSE TRUE END AS 'valueCard',
       CASE WHEN mc.PREFERENTIAL_WAY = '0' THEN '优惠券减免'
            WHEN mc.PREFERENTIAL_WAY = '1' THEN '特价优惠'
            WHEN mc.PREFERENTIAL_WAY = '2' THEN '赠送'
            WHEN mc.PREFERENTIAL_WAY = '3' THEN '活动优惠'
            ELSE '' END AS 'preferentialWay'
  FROM TT_L_MBRCARD_CHG mc, TT_F_MBR_CARD m, TT_F_CARD c,  TT_F_USER U, TT_F_MEMBER me
 WHERE mc.MBR_CARD_ID    = m.MBR_CARD_ID
       AND m.CARD_ID         = c.CARD_ID
       AND U.USER_ID         = me.USER_ID
       AND m.MEMBER_ID       = me.MEMBER_ID
       AND mc.LOG_TYPE IN ('105' , '111' ,'112')
  1. 在SQL中使用DECODE进行硬解码(Oracle),看到那么多订单状态,我的内心是窃喜的,我就不信你小年轻都记得住。
SELECT O.ORDER_ID,
       O.ORDER_NO,
       TO_CHAR(O.CREATE_TIME,'YYYY-MM-DD HH24:mi:ss') CREATETIME,
       DECODE(O.PAY_TYPE,'0','在线支付','1','货到付款','2','自提支付','3','公司转帐','4','邮局汇款') PAYTYPE,
       TO_CHAR(NVL(O.INCOME_MONEY,0)/1000,'FM9999999990.00') INCOMEMONEY,
       TO_CHAR(NVL(O.POST_FEE,0)/1000,'FM9999999990.00') POST_FEE,
       O.POST_TAG,
       DECODE(O.ORDER_STATE,'00','未处理','01','待分配','02','订单补录','03','待发货','04','发货中','05','物流在途','06','成功关闭') ORDERSTATESTR,
       T.GOODS_ID,
       TO_CHAR(NVL(T.AMOUNT_RECEVABLE,0)/1000,'FM9999999990.00') PRICE,
       T.SALE_QUANTITY,
       TO_CHAR(NVL(T.AMOUNT_RECEIVED,0)/1000,'FM9999999990.00') AMOUNT_RECEIVED,
       T.GOODS_NAME,
       P.POST_CODE,
       P.POST_ADDR,
       P.MOBILE_PHONE
 FROM  TF_B_ORDER   O, TF_B_ORDER_GOODS T, TF_B_ORDER_POST  P
 WHERE T.ORDER_ID = O.ORDER_ID
   AND P.ORDER_ID = O.ORDER_ID
   AND O.ORDER_ID = #ORDER_ID#
  1. 不再硬编码,使用编码表,然后关联查询。(我勒个去,赶紧把编码字典表打印一份,放在手边,随时备查)
SELECT O.ORDER_ID, O.ORDER_STATE, D.STATE_NAME
FROM T_ORDER O LEFT JOIN ON T_ORDER_STATE_DICT D
ON O.ORDER_STATE = D.ORDER_STATE

不再忧伤的忧伤

  1. 2017年X月X日,办公室中,新版本升级正在如火如荼的进行中。
    KING哥(资深DBA): 老黄,怎么现在状态字段都是中文了,什么“排队中”、“排队成功”、“排队失败”,为什么不用编码1,2,3呢,你确定这样使用中文不会有字符集问题,不会浪费存储空间嘛。
    老黄(资深码农):KING哥,我现在感觉自己老了,记忆力不如以前了,所以不用1,2,3等之类的编码了,因为每次我都要去查编码说明或者询问小雨,浪费时间太多,所以就直接用中文了。另外中文字符集问题,现在都什么年代了,我对中文表示很有信心(文化自信)。存储空间嘛,我们就这点数据量,完全不在话下。

  2. 同年Y月Y日,给一新人小禹讲技术。
    小禹:兵哥,现在编码都提倡用直接的中文短语了,我这个新设计的存储表格里,是否启用这个字段,我也设计成了字符类型,注释里面写上:“是否启用,是/否”啊,这样就与你的**一致了。
    兵哥:小禹,你确定就是布尔二值类型的字段么,不会有第三种值了么?
    小禹:目前看来是的。
    兵哥:那还是保持一贯的约定吧,使用tinyint的0和1来表示吧。
    小禹:啊?(懵懂中)。

  3. 摘:Codeless Code《第85案-空空大师的中庸》
    两个僧人找到空空大师解决纠纷。
    僧人甲说:“我提出了一个绝对漂亮的设计,可是那个家伙说太复杂、没法维护。”
    僧人乙说:“我提出了一个绝对简单的设计,可是那个家伙说太局限、不会有用。”
    空空大师转向她的白板,画下一个大大的Φ。
    僧人甲说:“我不明白。”
    僧人乙说;“我也不懂。”
    空空答道:“你们是在用二进制争论,不是零就是一。等你们学会浮点数再回来吧。”

优雅代码实践4 之 保持术语、短语的完整

image
比如以上页面需要进行业务调整,我想快速找到相关代码实现的地方,于是通过IDEA代码全文搜索,检索“取消签到”、“确认帮约”、“确认修改”等在页面上展示在一起的短语,但是经常找不到。只能退而求其次,去寻找“签到",”帮约",“修改”等字眼。在寻找”帮约"时,都失败,最后通过字眼"约“(大量结果中一个个比对寻找)才找到。
image

为什么找不到呢,因为我们代码中把这些短语拆零了。这些拆零的短语,使得我们直观想通过完整的短语来全文检索代码变得很不方便,只能使用更短的词语来花时间寻找,大大延长了代码定位的时间,因为更短的词语检索,带来的是更多的结果。

span(@click.stop="checkIn(subscriptionId, isCheckIn)") {{isCheckIn ? '取消':''}}签到

span 确认{{ isStaff ? '帮' : '预' }}约

button.btn.btn-theme-primary(type="submit") 确认{{cardId ? '修改' : '添加'}}
const url = `/SubscriptionList/${checkIn ? 'cancelCheckinSubscription' : 'checkinSubscription'}/${subscriptionId}`

const styleLoader = `${options.vue ? 'vue-' : ''}style-loader`

类似以上这种代码,只需要稍作调整,就可以通过完整短语来快速搜索定位到了。

span(@click.stop="checkIn(subscriptionId, isCheckIn)") {{isCheckIn ? '取消签到':'签到'}}

span {{ isStaff ? '确认帮约' : '确认预约' }}

button.btn.btn-theme-primary(type="submit") {{cardId ? '确认修改' : '确认添加'}}
const url = `${checkIn ? '/SubscriptionList/cancelCheckinSubscription' : '/SubscriptionList/checkinSubscription'}/${subscriptionId}`

const styleLoader = `${options.vue ? 'vue-style-loader' : 'style-loader'}`

代码中术语、短语虽然有时候虽然只有一两个个字的差异,但是最好不要拆散它们,尽量保持完整,这样使得短语检索,能更快地定位到代码的位置,何乐而不为呢。
保持完整、保持完整、保持完整,代码离完整、离优雅就更近了。

排队功能到底是不是一个鸡肋

image

"奕起嗨"瑜伽经营管理互联网服务最近上线了一个"课程订满排队"的功能,方便瑜伽馆的会员在热门课程已经订满又极度想预订的情况下,加入排队,一旦有人取消预订则可以排上队。大体流程如下:

image

虽然看上去小小的排队功能,涉及到的改造点还挺多:预订排队、帮订排队、取消预订、取消课程、取消排队、排队设置、我的预定等。经过小伙伴们一起努力设计开发测试发布,在一个迭代后终于上线了。

看上去多美好的一个功能啊,能排队啦。这年头,热门的稀缺的,排队是常态。去政府部门国有银行办事得排长队;去火车站进站,要排短队;去领券参观辽宁号,还要通宵排队。

在我们自己沉浸在自我满足中时,客服有一天突然找过来,说有一个馆主要求关闭排队,但是关闭不了,因为目前还有人正在排队中,必须等没人排队了,才能关闭,要我们协助处理。具体原因大概是,馆主刚开始也觉得排队不错,就打开了,结果发现自家的课程,个个都是热门,个个都很快订满了,后来的就需要排队,本来是好事,但是馆主仔细想想,觉得不对劲啊,照这样子谁还办新卡啊,课程这么难预约上还要排队,影响发展新会员卖新卡啊,坚决要求关闭,说会让人产生混淆。

仔细一想,这个馆主想得好像没问题(逻辑上另论),我们也客户第一,第一时间从后台帮他关闭了排队功能。再回头想想,我们花费了力气,开发的这个排队功能,竟然被客户排斥了,那么它到底是不是鸡肋呢。

我们的开发人员也经常抱怨,产品老是闭门造车,想出一些客户很少或者基本上不用的功能,让开发去实现,然后再不停地改,有时候还反复地改,造成开发与产品之间的常态怒怼。

精益创业中,有一个概念,就是MVP(最小可行性产品),也就是快速最小成本造一个MVP,看看用户,试水市场,有人用就说明有价值,然后再跟进丰富它,没人用,那就放弃。

对于产品中的一个小功能,应该也可以有类似概念。比如排队,如果用的人多,有人提意见了,比如想在某一个课程上设置是否允许排队等,那就是好事,说明真的有人用了还有人提需求了。但是更多的可能是,真的没什么人去用它,我们能否接受这种现实,或者乐于接受这种现实而减少吐槽呢?

套一下8020原则,一个产品的核心功能在它的20%。那么我们是不是那个80%就不做了呢,应该不是,如果没有那个80%,20%也没有意义,所谓皮之不存毛将焉附,不能因为吃到第3个饼吃饱了,就说前面两个饼吃得没有意义。

我们需要的是螺旋式上升,我们希望能做的都是20%的核心功能,但是可遇不可求,只有我们把80%的基础打牢了,20%才能从其中涌现。

(图:设计一个大象🐘的滑板车,可能是一个鸡肋。)

「鸡肋」直译是chicken's ribs。不过,**人说「鸡肋」,通常有两个意思。一出自三国杨修的故事:曹操讨刘备,进退两难,给将士的命令只有「鸡肋」二字,众人大惑不解,杨修却知道曹操准备退兵,指出「夫鸡肋,食之则无所得,弃之则如可惜」(《后汉书.杨修传》)。这个「鸡肋」,英文恐怕没有同义成语。《林语堂汉英词典》译做anything insipid, uninteresting(任何燥枯无味的东西),只译出「鸡肋」的一半意思。要不失原意,译文须详细一点: something not good enough to get excited over, but not bad enough to forego without regret。Forego是「放弃」的意思。
「鸡肋」的另一意思,出自晋朝刘伶的故事:刘伶有一次醉酒触怒了别人,那人捋起双袖准备揍他,他说自己瘦弱如「鸡肋,不足以安尊拳」,逗得那人一笑息怒(《晋书.刘伶传》)。这个「鸡肋」,林语堂译做fragile(虚弱的),自然不错;假如作名词,则可译做weakling(孱弱的人),例如: A puny weakling, he was bullied by his classmates(他体如鸡肋,被同学欺凌)。

纯文本的状态图,及画图工具汇总

在看文章golang中goroutine的调度时,发现里面的几张图,竟然是纯文本画出来的,感觉很有意思,特此mark一下,回头也尝试尝试。

状态

       mstart
          |
          v        找不到可执行任务,gc STW,
      +------+     任务执行时间过长,系统阻塞等   +------+
      | spin | ----------------------------> |unspin| 
      +------+          mstop                +------+
          ^                                      |
          |                                      v
      notewakeup <-------------------------  notesleep

状态转换

                                            acquirep(p)        
                          不需要使用的P       P和M绑定的时候       进入系统调用       procresize()
new(p)  -----+        +---------------+     +-----------+     +------------+    +----------+
            |         |               |     |           |     |            |    |          |
            |   +------------+    +---v--------+    +---v--------+    +----v-------+    +--v---------+
            +-->|  _Pgcstop  |    |    _Pidle  |    |  _Prunning |    |  _Psyscall |    |   _Pdead   |
                +------^-----+    +--------^---+    +--------^---+    +------------+    +------------+
                       |            |     |            |     |            |
                       +------------+     +------------+     +------------+
                           GC结束            releasep()        退出系统调用
                                            P和M解绑                      

G的状态图

                                                      +------------+
                                      ready           |            |
                                  +------------------ |  _Gwaiting |
                                  |                   |            |
                                  |                   +------------+
                                  |                         ^ park_m
                                  V                         | 
  +------------+            +------------+  execute   +------------+            +------------+    
  |            |  newproc   |            | ---------> |            |   goexit   |            |
  |  _Gidle    | ---------> | _Grunnable |  yield     | _Grunning  | ---------> |   _Gdead   |      
  |            |            |            | <--------- |            |            |            |
  +------------+            +-----^------+            +------------+            +------------+
                                  |         entersyscall |      ^ 
                                  |                      V      | existsyscall
                                  |                   +------------+
                                  |   existsyscall    |            |
                                  +------------------ |  _Gsyscall |
                                                      |            |
                                                      +------------+

AsciiFlow:在线流程图绘制平台是一个强大的在线ASCII图形绘制工具,ASCIIFlow是上世纪九十年代黑客们最爱的制作流程图表方式,全文本易传播,Geek 风格的反璞归真。不幸的是,目前似乎无法输入中文。

支持中文版本修改版本asciiflow

问:何以取得真经?答:翻墙。

image
早上西游挂了,每每浏览器地址栏中输入关键字,都自动跳到谷歌搜索都无果,转而使用百度,感觉即为难受。搜索倒也罢了,问题是相关OAUTH2到谷歌账号的,也全都失效了。看来,翻墙竟成了我的日常基本需求之一了。还好,中午借来了同事的Shadowsocks账号,技术搜索的网络世界终于可以畅通无阻了,一种特别爽快的感觉。

世界上本没有翻墙,因为有了墙,所以有了梯子,而且有了造梯子的木匠。
这堵围墙,挡住的不仅仅是那么几篇看来大逆不道的网页,那只是表象,它真正挡住的,是一个人独立思考的机会。在围墙内,只有一种生意,一套文字,一个面孔,一种态度,而在围墙外,确有千百种声音,千百种态度。 有人说他们都不是真相,都是谎言。是的,也许他们都不是真相,但是围墙里的就是真相么?!古人有云:兼听则明,偏信则暗。
还好,我只搜索技术,对于其它,毫无兴趣。
image

使用古诗词创建好记的密码

背景

有一定复杂度的密码,一般都要求以下3点:

  1. 长度在8位以上;
  2. 包含大小写字母混合;
  3. 包含数字、特殊字符;
  4. 如果非要加上第4点,我想一定是趣味好玩记得住。

可是,这样的密码即使构建出来了,很不容易记忆,比如Q4m)h4gWlczr!h,很复杂吧,但是枯燥无味保准记不住。

以古诗词为基础,创建符合要求的密码

比如:

出塞
唐·王昌龄
秦时明月汉时关,万里长征人未还。
但使龙城飞将在,不教胡马度阴山。

秦时明月汉时关,万里长征人未还。这句就可以创造出密码Q4m)h4gWlczr!h,输入密码的时候,只需要默念秦时明月汉时关,万里长征人未还。 以拼音首字母为基础,句首字母大写,时谐音成4,月形象为),未转换为!(编程语言C中的逻辑否定语义)。

通用转换规则

  1. 两句连在一起,取拼音首字母,句首字母大写;
  2. 数字规则
  • 谐音líng/ lín换成0;形象字
  • 谐音换成1;形象字
  • ai/换成2,亦可替换
  • 谐音san/shan换成3
  • 谐音si/shi换成4
  • 谐音wu/wo换成5,可替换
  • 谐音liu/lu/换成6
  • 谐音qi/qu/chi换成7,
  • 谐音ba/bai换成8
  • 谐音jiu换成9
  1. 特殊字符规则:
  • ! 叹号 exclamation mark/bang,可替代等否定字,也可代
  • ? 问号 question mark,可替代
  • , 逗号 comma,可替代谐音dòu
  • . 点号 dot/period/point ,可替代
  • : 冒号 colon ,可替代
  • ; 分号 semicolon ,可替代
  • ” 双引号 quotation marks/double quote ,可替代
  • ‘ 单引号/撇号 apostrophe/single quote ,可替代
  • ` 重音号 backquote/grave accent,可替代
  • * 星号 asterisk/star,可替代
  • + 加号 plus sign,可替代谐音jiā
  • - 减号/横线 hyphen/dash/minus sign/ ,可替代
  • = 等号 equal sign,可替代
  • / 斜线 slash,可替代
  • \ 反斜线 backslash/escape,可替代反转
  • | 竖线 bar/pipe/vertical bar ,替代谐音shù
  • _ 下划线 underline/underscore,可替代
  • $ 美元符号 dollar sign,可替代(盘在柱子上的龙) 、、谐音
  • @ at at sign ,替代以及任何可滚动或环状意境的字,比如;还可替代所有有的字,如:逃进近遁边随等。
  • # 井号 crosshatch/sharp/hash,可替代,也可谐音jǐng
  • % 百分号 percent sign/mod,替代
  • & and/和/兼 and/ampersand,替代
  • ^ 折音号 circumflex/caret ,可替代
  • ~ 波浪号 tilde,可替代等有波纹意境的字以及叠字的第二字
  • {} (左右)花括号/大括号 (left/right|open/close) braces,{可替代西}可替代
  • [] (左右)方括号/中括号 (left/right|open/close) brackets,]可替代[可替代
  • () (左右)圆括号/小括号 (left/right|open/close) parentheses, )可替代(可替代
  • <> 尖括号 angle brackets
  • < 小于号 less than,可替代
  • > 大于号 greater than,可替代
  • 空格 可替代
  1. 补充规则:
  • 对于既可以替代为数字也可以替代为特殊字符的时候(比如可以替换成*或者1),如果当前密码缺乏数字,那就替换成数字,如果当前密码缺乏特殊字符,那就替换成特殊字符,如果都缺乏或者都有,也替换成数字。
  • 长度达不到8位。补充到8位,缺几补几,例如缺1个那就补1,缺2个就补22,以此类推。例如羌管悠悠霜满地(Qgy~smd1)。
  • 缺少数字。取末位字拼音声调,1234。
  • 缺少特殊字符。补刀($)即可。
  1. 扩展:不同网站不同密码
  • 比如大吕勇这种技术控,不止想要有复杂的密码,还想有不同网站不同密码,咋办呢?
  • 选定基础密码后,在头尾进行扩展。比如,选定基础密码,煮豆燃豆萁,豆在釜中泣(Z,rd7,@f]7)。京东的密码就扩展成:j{基础密码}d=>jZ,r,7,@f]7d,淘宝的密码就是:t{基础密码}b=>tZ,r,7,@f]7b,亚马逊的密码就是:y{基础密码}mx=>yZ,r,7,@f]7mx,QQ的密码就是:q{基础密码}q=>qZ,r,7,@f]7q,以此类推。
  • 如果有的网站不允许你输入特殊字符,你又不得不录入密码的话,你就只能心里默默的骂一句狗娘养的,然后,去掉一切特殊字符的规则,基础密码换成Zdrd7Dzfz7。如果有的网站还不允许超过8位,那就只能再骂一句娘希匹,基础密码换成Zdrd7了,再加上头尾,就是7位了。
  1. 再次扩展:不同网站不同古诗
  • 京东,在古诗中找京字或者东字的,比如:京口瓜洲一水间,钟山只隔数重山。
  • 淘宝,淘->涛->大江东去,浪淘尽,千古风流人物。
  • 亚马逊,马->下马饮君酒,问君何所之。
  • 其它自己去中华诗词网查询去
  • 这些古诗,可以在云笔记里面记录下来,什么网站对应什么古诗。
  1. 示例:
咏柳
唐·贺知章
碧玉妆成一树高,万条垂下绿丝绦。Byzc1|^Wtc_6~d
不知细叶谁裁出,二月春风似剪刀。!Zxy?cc2)C~4j(

绝句
唐·杜甫
两个黄鹂鸣翠柳,一行白鹭上青天。2ghlmcl1hbl^qt
窗含西岭千秋雪,门泊东吴万里船。Ch{0qq*Mb}5wlc

清明
唐·杜牧
清明时节雨纷纷,路上行人欲断魂。Qm4j~f~6^xrydh
借问酒家何处有,牧童遥指杏花村。J?9+?cyMtyzx*c

赠花卿
唐·杜甫
锦城丝管日纷纷,半入江风半入云。Jc4g*f~B>j~b>~
此曲只应天上有,人间能得几回闻。C7zyt^yRjnd?hw

乌衣巷#wyx
唐·刘禹锡
朱雀桥边野草花,乌衣巷口夕阳斜。Zqqbyc*W1xkx1/
旧时王谢堂前燕,飞入寻常百姓家。94wxtqyF>xc%x+

又背古诗词,又记住复杂密码,不亦乐乎。

DEBUG怪诞之背后的秘密

缘起

昨天接到同事高禹的报告,说eql从0.0.72升级到0.0.96版本后,原来可以用的代码,有一处不能用了,但是如果把参数名从table改为table1,那就没问题。凭直觉感觉这应该是一个很有意思的BUG,我就他那边把出BUG的SQL和代码都要过来了,先添加一个新的测试用例。然后我就直接RUN这个测试用例,发现果然不通过。很纳闷,开始单点调试(DEBUG)看看,但是结果竟然反转了,竟然能通过了。多次试验皆如此。有意思有意思,简直百思不得其解了。

重现

随着对代码更进一步的跟踪与分析,我逐渐发现了原因了。为了简单的重现原因,我截了两个图。
第一张图是加了两个断点跑的图,结果是v1和v2相等的,都是同样的一串内容。如下:
image

第二张图是去掉了第一个断点,只保留第二个断点,结果是v1和v2完全是不一样的东西。如下:
image

匹夫无罪,怀璧(Map动态代理)其罪

到底是为什么呢?从两张图上的右下角的console输出差别可以看出来,在debug断点时,IDE为了展示变量的概要信息,其实是做了一些调用的。在第一张图停在第一个断点的时候,IDE其实在背后调用了Map.isEmpty和Map.size方法,然后才是被调试程序调用了Map.get方法。而在第二张图只有一个断点的时候,IDE在Map.get方法调用之前,并没有调用Map.isEmpty和Map.size方法。

问题是,这个Map不是普通的Map,是一个JAVA的动态代理,如下:
image
所以,这个代理实现上有缺陷,取map中属性为table的值时,又去找了名字为table的属性,结果HashMap中刚好有这个属性,于是取出来了,v1的值就成了HashMap$Node类型了(见下图),导致了问题的存在,所以改成table1就没这个问题,但是如果使用entrySet肯定有问题。
image
问题定位了,修复起来就简单得多,只需要在if判断Map类型之外,取属性/方法的时候加一个else就行了。

为什么要用动态代理

当初为什么要做一个动态代理呢,其实这个跟eql的参数传递实现有关系。一开始是并没有这个动态代理的,直到某一天,生产环境上老是报告空指针异常,然后异常的堆栈就指向了eql的内部实现。我一查,都是使用JavaBean传递参数遭遇问题,因为JavaBean里面有一些属性并没有被sql用到,却调用get方法,导致空指针异常。比如下面这样:

public class Person {
      // ...
      public String getName() {
             return name;
      }
      public String getImage() {
             throw new NullPointerException();
      }
}
-- [findPersonByName]
SELECT NAME,SEX,EMAIL,ADDRESS FROM PERSON WHERE NAME = #name#

eql的使用者感觉很无奈,sql里面只用到了name啊,并没有使用image,为什么要调用image方法呢。确实是这样子,刚开始时,我是为了省事,把Bean的所有属性都转换成Map使用了,而不会管这个属性在sql中有没有实际使用到。(注:JavaBean的getter/setter方法应该只是简单的get和set,抛出异常是违反约定的)。所以为了避免这种无用的调用,我就开挂了Map的动态代理,只有等真正需要读取属性的时候,才去调用(Map就用get取,JavaBean用getter或者filed取)。然后这就种下了本次问题的因了。

总结

Debug断点模式下,IDE为了展示堆栈上变量的信息,经常会调用一些方法,比如toString()方法,比如Map.isEmpty,Map.size等等。这些方法一般情况下都不会有问题,所以DEBUG与非DEBUG行为上一般都是一致的。但是在某些特殊情况下,却可能产生不一致的行为,比如本案所示,比如当初我在跟踪druid的sql解析代码时,就发现IDE的DEBUG窗口的一些变量直接就报告了空指针异常,然后我发现原来作者对变量的toString()方法实现没有特别考虑和测试。

一粒米饭的遐思

image

吃完晚饭,准备看所谓"权游史上最好看的第一集"《权力的游戏》最新的S07E01,打开MAC,突然发现右手手背上残留了一粒米饭。我先想看看垃圾桶,感觉离得有点远,懒得动身去扔;然后再想留在桌上,一会肯定忘记,会再次粘到身上或者衣服上;干脆一口吃了。

吃完一想,以前背诵古诗《锄禾》,不就说的是要这么做的嘛。锄禾日当午,汗滴禾下土,谁知盘中餐,粒粒皆辛苦。古诗背完,再一想,这才是知行合一。

然后再谷歌一把,一粒米粒中竟然包含有佛理,“佛观一粒米 大如须弥山”。

佛陀开示道:“无二之性,即是实性。一粒稻穗从最初的播种起,经过灌溉、施肥、收割、制造、贩卖……累积了种种的力量与辛苦才能成就一粒米,它所蕴含的功德是无量的,正如同那件裤子是贫苦夫妇唯一的财物、全部的家当,它所包藏的心量也是无限的!四海龙王懂得一粒米的功德与裤子的功德一样大,都由虔诚一念引出,所以赶紧退让称善。由此可见,只要虔诚一念,则小小一粒米、一条衫裤的力量,都可以与千千万万座须弥山相等!”
後来有人把这件事写成一首偈,来警示天下的冥顽众生:“佛观一粒米,大如须弥山;若人不了道,披毛带角还。”

吃完这一粒米饭,我竟然顺带参悟了一丁点佛道,南无阿弥佗佛!

不再因为timeout的单位而蒙圈

每每在看代码时,看到超时时间,我就容易蒙圈,因为不知道单位是什么,因为有时候是秒,有时候是毫秒。
image
(图:让人蒙圈的timeout)

还有让人迷惑的,一个命令中使用两种不同单位:
image

所以,当自己写代码的时候,我就避免这种情况,要么单独带上单位参数,要么命名的时候就带上seconds或者millis。
image
(图:带上时间单位参数)
image
(图:命名带上时间单位)

自己挖坑自己填2:笑笑老师的私教课

最近连续接到东湖区静心瑜伽馆馆主的反应,说笑笑老师的私教课被连续约了,比如下午4点钟被人约了1小时私教课,5点钟又被人约了1个小时私教课,中间没有休息间隔。
image
图:笑笑老师私教课被连续预订了,中间没有休息间隔。

我们在系统参数设置上,有私教休息时间间隔,理论上这种情况不应该出现啊。然后我们查询了一下后台数据,发现确实如此:
image
图:笑笑老师私教课被连续预订了,中间没有休息间隔。

然后我们就想在开发环境、测试环境、生产环境体验馆上验证一下,总是重现不了,比如如下图,我先预定14:00开始的1小时私教课,那么接下来15:00开始的私教课就预约不了啊,只能约最近15:30开始的私教课,因为14:00开始的私教课在15:00结束时,需要休息半个小时,符合预期。可是生产环境为啥这样子呢,好郁闷。
image
图:14点1小时的课预订了以后,15点的时间点就选不到了。

既然暂时查不到问题,只能暂时搁置一下,继续做其他事情。但是客服接连报告了好几次,说馆主火气很大,要我们赶紧解决。然后每次又再看一遍,看看是否有可能有新的发现。直到昨天临下班的时候,客服又来说了:“馆主一天来找一趟,都不知道要怎么说了”,关键还带了一句:“已经有人订了周六16点的课了,现在展示的周六15点的还可以订”。豁然开朗,奶奶个X,原来是往前订没有时间间隔,之前测试的一直都是往后看。再回头来看,上面的图就已经体现了。问题只要能重现,修复就是很快的事情。
image
图:10点1小时的课预订了以后9点和11点的时间点就选不到了。

问题解决了以后,多了一个测试面试题,私教预约需要包含至少以下测试用例:

  1. 预订一节课后,课程结束并且休息30分钟之后才能预约下一个时间点;
  2. 预订一节课后,再预订前面时间点的课程,也需要保证30分钟时间间隔。

消除公网出口单点问题

我们的瑜伽应用,部署在阿里云上。其中VPC中有一台ECS专门用来做公网出口(GW01),这样VPC中其它ECS就只分配内网IP就行了。GW01只用来做公网出口,以及系统管理入口,其它业务都不跑,就是为了让GW01保持简单可靠,防止这个系统中唯一的单点出现故障,导致整个系统对外部接口不可用。随着我们业务量的持续提升,我们对系统的可用性也越来越重视,我们就需要考虑,如何消除这个系统中唯一的单点。
翻阅阿里云的文档,有两种方案,一种是VIP方案,另外一种是NAT网关方案。

VIP方案

image
需要配合Keepalive进行配置。但是还是内测阶段,阿里云并没有上线此应用。

NAT网关方案

image
image
配置非常简单,申请NAT网关,绑定弹性公网IP,添加SNAT规则允许访问外部公网,添加DNAT规则允许外网访问VPC内网机器。

优雅代码实践6 之 名副其实

先整个反面例子吧

新鲜出炉的,我更新代码,随便看了一下更新列表,有这么一段代码:

@Sql("delete  from f_user_role where user_id = ## and role_name = '候选人'")
void deleteRoleInfo(String userId);

然后,先不论“硬删除”与“软删除”,只论命名,我就习惯性的,在群里吼一声:
image
然后,再更新代码,看到了更新后的版本:

@Sql("delete  from f_user_role where user_id = #1# and role_name = #2#")
void deleteRoleInfo(String userId, String roleName);

改的还是蛮及时的,看来吼一声达到效果了。

缘何12个月,竟然折腾成了12年

今天在维护群中,受到用户的一条反馈,说给会员开卡,明明开的12个月,咋就成了12个年头呢。
image
马上,查了一下数据,发现原来是馆主在卡激活之前做了一次卡编辑,误操作改成了12年。
image

在代码中顺手一查,代码中竟然有64处有年月天地方,看来这个单位没有很好地统一啊。
image

然后就顺手提取一个单独的类,来处理这些年月天周啥的,如下:

//  时间单位对象。
@RequiredArgsConstructor
public enum ExpiredUnit {
    年("N", "", ChronoUnit.YEARS), 月("Y", "个月", ChronoUnit.MONTHS),
    天("T", "", ChronoUnit.DAYS), 周("Z", "", ChronoUnit.WEEKS);

    @Getter private final String unit;
    @Getter private final String suffix; // 单位作为数量后缀时的名称
    @Getter private final ChronoUnit chronoUnit;

    public static ExpiredUnit ofUnit(String unit) {
        return Arrays.stream(ExpiredUnit.values()).filter(x -> x.unit.equals(unit)).findFirst()
                .orElseThrow(() -> new RuntimeException("unknown expire unit " + unit));
    }

    public static boolean isValid(String unit) {
        return Arrays.stream(ExpiredUnit.values()).anyMatch(x -> x.unit.equals(unit));
    }

    public static Expired of(String value, String unit) {
        return of(NumberUtils.toInt(value), unit);
    }

    public static Expired of(int value, String unit) {
        val expiredUnit = ofUnit(unit);
        return expiredUnit == ExpiredUnit.天 && value % 7 == 0
                ? new Expired(value / 7, ExpiredUnit.周)
                : new Expired(value, expiredUnit);
    }


    @Value
    public static class Expired {
        private final int value;
        private final ExpiredUnit unit;

        public String getDesc() {
            return value < 0 ? "无限期" : (value + unit.getSuffix());
        }

        // 作为/后面的文字描述。例如1次/月,3次/2周
        public String getOfDesc() {
            return value == 1 ? unit.getUnit() : value + unit.getUnit();
        }

        public DateTime createExpired(DateTime effective) {
            return effective.plusSeconds((int) (value * unit.getChronoUnit().getDuration().getSeconds()));
        }
    }
}

重构完成后,再次搜索,只剩下22处(减少42处)与日月天单位解析无关的代码了。
image

优雅代码实践1 之 JAVA日志

今天从代码基中看到有几行记录日志的代码,如下所示:

log.info("---cardPay-----{}", cardPay);
log.info("---isChargedSchedule ", isChargedSchedule);
log.info("--- cardPay.getTimes() > 0-----{}", cardPay.getTimes() > 0);
log.info("--- isNotBlank(cardPay.getPayId()) -----{}", isNotBlank(cardPay.getPayId()));
log.info("--- All  -----{}", isChargedSchedule && cardPay.getTimes() > 0 && isNotBlank(cardPay.getPayId()));
if (isChargedSchedule && cardPay.getTimes() > 0 && isNotBlank(cardPay.getPayId())) {

我想,原始作者,就是为了从日志中追踪下面那个复杂的if语句是否命中。
但是这种写法有一些问题:

  1. 记录cardPay.getTime()就可以了,何必记录cardPay.getTimes() > 0;
  2. 同理,记录cardPay.getPayId()就可以了,何必记录isNotBlank(cardPay.getPayId());
  3. 既然已经记录了cardPay,就不需要再记录cardPay.getTime()cardPay.getPayId()了;
  4. 打印那么多---干啥子(类似的还有打印一堆###或者+++之类的),只会白白地增加日志的体量,让日志文件增长的更快之外,根本无助于日志查找(干过平台的知道,偶尔为了在大几G的日志中找点东西,vi打开以下贼慢啊,而且还要定时清理大文件)。

顺手做一下重构,代码如下:

val hasEnoughBalanceForSchedule = isChargedSchedule
        && cardPay.getTimes() > 0 && isNotBlank(cardPay.getPayId());
log.info("cardPay:{}, isChargedSchedule:{}, hasEnoughBalanceForSchedule:{}",
        cardPay, isChargedSchedule, hasEnoughBalanceForSchedule);
if (hasEnoughBalanceForSchedule) {

这样子的好处如下:

  1. 日志只记录了一行,没有冗余信息;
  2. 代码很清爽,很明确。

读书《时间的形状》:人类一思考,上帝就吓尿

科普书《时间的形状》

最近看了一本科普书,叫做《时间的形状》,写得很上口,我几乎是几天之内就读完了。(相比之下,另外一本老外写的《物理世界奇遇记》,就很不对我的胃口,觉得就写得没劲,读了几段就扔一边去了)
image
书的序言就吸引了我,相对论之后,上帝改口了:“人类一思考,上帝就吓尿”了。

物体下落速度的思维实验(思维实验——在大脑中运行的实验)

image
伽利略:“亲爱的亚里士多德先生,您不是说重的东西比轻的东西下落得更快吗?那么如果我们把一个铁块和一个木块用绳子拴在一起,从高处扔下来会发生什么?按照您的说法,较轻的木块下落得慢,因此它会拖累铁块的下落,所以它们会比单扔一个铁块下落得慢一点,是不是这样?”

亚里士多德:“没错,逻辑正确。”

伽利略:“但是,铁块和木块拴在一起以后,总重量却要比一个铁块更加重了啊,那么岂不是它们又应该比单个铁块下落得更快?”

亚里士多德:“呃……”

伽利略:“这个实验不用实际去做了吧,单单就在我们脑子里面做一下就可以发现您的理论是自相矛盾的。”

亚里士多德:“你让我想想,你让我想想……”

狭义相对论

  1. 光速与光源的运动无关,对于任何参考系来说,光在真空中的传播速度恒为c。
    还是思维实验:假设我现在一个人在黑漆漆的宇宙中飞行,虽然我飞得跟光一样快,但是因为没有任何参照物,我感觉不到自己的速度,就我自己的感觉而言和静止是一样的。这时候如果我身边有一束光,或者一个电磁波,我将看到什么呢?一束和我保持静止的光吗?一个静止的电磁波吗?也就是看到一个虽然在振荡的电磁场 ,但是它却不会交替感应下去吗?哦,不,这显然违背了麦克斯韦的方程组,波的速度和波源的运动速度无关,虽然我在以光速飞行,不论是我自己用发生装置发生一个电磁波,还是我飞过一个电磁波发生装置,我看到的电磁波都应该是相同的,因为介质没有变。我将看到一个振荡中的电场能够产生振荡的磁场,而一个振荡中的磁场又能够产生振荡的电场,这个交替反应绝不会停下来。再想象一下报数的情况,如果我和这队报数的人都在一节火车车厢中,火车高速行驶,但是我并不能感觉到火车是静止的还是运动着的,我会看到报数人的反应速度提高了吗?这也显然很荒谬,火车跑得再快也应该跟报数人的反应速度无关,我应该仍然看到它们以同样的反应速度传递着“一、二、三……”才对啊。

    这么说来,光速应该相对于任何参照系来说,都是恒定不变的。哦,我这个想法实在有点疯狂。

    他拿起笔在草稿纸上写下一句话:光速与光源的运动无关,对于任何参考系来说,光在真空中的传播速度恒为c。

  2. 在任何惯性系中,所有物理规律保持不变(相对性原理)。

  3. 同时性的相对性。

  4. 时间会膨胀
    image

  5. 空间会收缩

广义相对论

时空弯曲

image

从黑洞到虫洞

image

电子书下载

半仙说Apache Shiro其实很好用

缘起

2018年03月16日 19:41张半仙在QQ上跟我吐槽:"原来我也不高兴玩shiro 但是现在发现shiro 确实挺好用的,只是我们没优化好,并且复杂化了",并且表示这几天要好好研究,然后我说我也好好看看吧,并且写一篇博客,就叫《半仙说SHIRO其实很好用》,你也可以写写,咱们写的角度肯定不一样。

Shiro洗白

维基百科这么说:Apache Shiro(读作“sheeroh”,即日语“城”)是一个开源安全框架,提供身份验证、授权、密码学和会话管理。Shiro框架直观、易用,同时也能提供健壮的安全性。我随即谷歌一下关键字“日语 shiro”,真是以讹传讹,明明是白色的意思,不知道从哪里开始变成的意思了。白色白(しろ= shiro)ホワイト(White)黑色黒(くろ= kuro )ブラック(Black)红色赤(あか= aka )レッド(Red)

What is Apache Shiro有感

  1. JAAS缺点很多:绑定了JVM底层概念、依赖容器或者有状态EJB、到处都是烦人Checked异常(哈哈哈,Checked异常必须显式catch,否则编译报错啊)。
  2. Shiro好处很多:灵活、易用、易理解、可插拔、WEB支持好。还提到POJO-compatible的配置机制,真的不错哦。

Shiro短史

  1. 2004年,Les Hazlewood和Jeremy Haile创办了Jsecurity。
  2. 2008年,JSecurity项目贡献给了Apache软件基金会(ASF),并被接纳成为Apache Incubator项目。期间,Jsecurity曾短暂更名为Ki(读作“Key”),随后因商标问题被社区更名为“Shiro”。
  3. 2010年7月,Shiro社区发布了1.0版。
  4. 2010年9月22日,Shrio成为Apache软件基金会的顶级项目。

可见,历史还是很悠久的,都15岁了。

Shiro的那些让人记不住的名词

Subject卤猪蹄

理解框架,首先的理解概念,Shiro的概念挺好理解,就是名字取得差劲了点。什么Subject, Realm,跟单词本身含义一点都挂不上边。

Subject表示当前的操作用户,在安全领域,术语“Subject”可以是人,也可以是第三方进程、后台帐户(Daemon Account)或其他类似事物。它仅仅意味着“当前跟软件交互的东西”。这些解释跟Subject这个词感觉靠不上边,所以每次看完隔一段时间,就记不起来了Subject是个啥意思。所以,换个方式:Subject -> 主题 -> 猪蹄,所以从卤猪蹄开始理解Shiro的概念。

Realm真的么

Realm,单词本身是领域、范围的意思,在Shiro里面则是桥梁、连接的意思。Realm充当了Shiro与应用安全数据间的“桥梁”或者“连接器”。也就是说,当切实与像用户帐户这类安全相关数据进行交互,执行认证(登录)和授权(访问控制)时,Shiro会从应用配置的Realm中查找很多内容。记不住的话,就记得real+m(真的么?)你说的你是谁你要去哪里,都是真的么。

Authentication/Authoriztion 认证/授权

就是简单的"你是谁"(认证,是不是你)和“你要到哪里去”(授权,能不能去),两个词经常容易混淆。Authentication简称Authc,Authorization简称Authz,对了,必须先认证然后才是授权,所以按照字母顺序,应该是Authc然后是Authz,一下子记住了吧。

image

RBAC(Role-Based Access Control)基于角色的访问控制

RBAC就是用户通过角色与权限进行关联。简单地说,一个用户拥有若干角色,每一个角色拥有若干权限。这样,就构造成“用户-角色-权限”的授权模型。在这种模型中,用户与角色之间,角色与权限之间,一般者是多对多的关系。
image

用简约的"KISS"表达丰富的"爱"

image

今天,无意在朋友圈里又看了一遍朋友发的文章,致敬C语言之父—丹尼斯·里奇。复杂导致了Multics的失败,简单促成了Unix的诞生。文章中提到了两句话,我摘录一下:

吸取了Multics设计复杂而导致失败的教训,丹尼斯·里奇将Unix的设计原则定为”保持简约和直接”(Keep
it simple stupid),也就是后来著名的KISS原则。为了做到这一点,Unix由许多小程序组成,每个小程序只能完成一个功能,任何复杂的操作都必须分解成一些基本步骤,由这些小程序逐一完成,再组合起来得到最终结果。

C语言也贯彻了”保持简约”的原则,语法非常简洁,对使用者的限制很少。丹尼斯·里奇编写的教材《C编程语言》总共只有100多页,薄得难以置信。很多人都被它的简洁性吸引,学习并使用C语言。直到今天,C语言依然是世界上最重要的编程语言之一,”保持简约”原则显示了强大的生命力。`

我把里面的字眼“简单”替换成了“简约”,因为简约更加符合其本意。向里奇致敬,保持简约。要做到简约,却不简单。

Redis使用中的可维护性考量

首先打开Redis随机看一条数据:
image
做得还不错的有:

  1. KEY的规范性还行,使用了冒号分割
  2. ...

做得欠缺(缺乏可维护性)的有:

  1. KEY的固定前缀太长,shiro:cache:mobileCaptcha.authorizationCache:,太长了;
  2. 有效期没有设置(长久有效),导致在KEY调整时,REDIS中的垃圾越来越多;
  3. VALUE的值是二进制,对于维护人员来说,不够友好。

下面是做得还不错的一条数据:
image

总结一下,Redis使用提升可维护性,需要考虑一下几点:

  1. KEY的分隔符必须是英文:号,比如mobileCaptcha:18551855099,比如courseTypes:18012345678;
  2. KEY的组成,固定量放在前面(采用相对较短的固定),比如mobileCaptcha,变量放在后面,比如18551855099。
  3. 在代码中书写是,固定量和冒号放在一起,比如"mobileCaptcha:",这样方便全文查找。
  4. 有效期必须设置,哪怕设置长一些,比如30天,防止redis中垃圾数据越来越多;
  5. VALUE值建议使用友好性的字符串或者JSON,避免二进制。

注:图中使用本人用GO语言开发的REDIS可视化工具go-redis-web

开发中的一些约定(又名最佳实践)

数据库相关

  1. CREATE_TIME和UPDATE_TIME字段建议定义成如下形式,保证UPDATE_TIME能自动更新,CREATE_TIME能自动创建,例如:
CREATE TABLE YOUR_TABLE (
-- ...
UPDATE_TIME  TIMESTAMP ON UPDATE CURRENT_TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '更新时间',
CREATE_TIME  TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
REMARK VARCHAR(100) NULL COMMENT '备注'
)ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COMMENT ='什么表';

ALTER TABLE YOUR_TABLE
MODIFY COLUMN UPDATE_TIME TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE  CURRENT_TIMESTAMP COMMENT '更新时间',
ALTER TABLE your_table
MODIFY COLUMN CREATE_TIME TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
  1. 非字符类型字段,建议都设置成NOT NULL DEFAULT 0的形式,然后在Java代码中使用Primitive类型变量(比如int/long等)而不是包装类型(例如:Integer/Long等),例如:
CREATE TABLE YOUR_TABLE (
-- ...
`CYCLE_VALUE` bigint(20) NOT NULL DEFAULT 0 COMMENT '限次周期',
)ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COMMENT ='什么表';
  1. 状态字段,使用中文词汇使用形容词,并且维护状态流转图,比如:
    image
    但是对于某些布尔含义的字段(比如可见性、是否匿名等),则不推荐使用中文形容词。直接使用tinyint,1表示true,0表示false。

  2. 表必须有主键,在无法使用业务字段作为主键的情况下,使用ID作为主键,ID的类型统一为bigint(20),并且在JAVA代码中使用WestId.next()生成。例如订单表的主键是订单ID;会员卡表的主键是会员卡ID;

  3. 表示范围Range的字段,采用左闭右开的原则。比如开始时间为2018年04月19日00:00:00,到期时间为2018年04月20日00:00:00,表示范围为[2018年04月19日00:00:00, 2018年04月20日00:00:00),判断有效期代码为'2018年04月19日00:00:00' <= now() and now() < '2018年04月20日00:00:00'。JAVA中也同样采取左闭右开的原则。

  4. 根据ID(作为主键)在表中进行查找时,不带其他条件。其它条件在代码中再次进行判断,这样可以本条SQL的重用率。比如:

-- 推荐写法
SELECT CARD_NO, USER_ID, STATE, AVAILABLE_BALANCE FROM T_MEMBER_CARD WHERE CARD_ID = '#cardId#';

-- 不推荐写法
SELECT CARD_NO, USER_ID, STATE, AVAILABLE_BALANCE FROM T_MEMBER_CARD WHERE CARD_ID = '#cardId#' AND STATE = '正常';

JAVA代码

  1. 区分PO,BO,VO,DTO,POJO的概念;
  • 普通映射到表一行数据的对象称为领域对象,不加后缀,比如Coach/Schedule
  • 页面向后台传输对象称为DTO对象,加后缀Request,比如,CoachSalaryChangeRequest
  • 后台向前台返回对象也称为DTO对象,加后缀Dto,比如,CoachSalaryConfigDto。此对象建议使用@value @builder来修饰,做成Immutable对象。
  • 后台计算中,需要临时存储一些数据,可以使用VO对象,称之为VO对象(Value Object),用于在Controller和Service中间传递,或者Service内部之间传递。
  • 暂时不引入PO/BO等概念。
  1. 区分Controller与Service的职责划分;
  2. Service与Controller分别放在不同的代码工程中;
  3. 建议使用DAO的@Sql注解的形式使用访问数据库;
  4. 避免使用Map对象;
  5. 在表示时间概念的地方使用Joda的DateTime类型,避免使用String类型;
  6. 避免手工撰写缓存的使用代码,而是使用注解声明方式激活缓存;

部署

  1. 使用默认的IP和端口,然后在本地使用HAProxy来代理本地端口,指向真实的服务。比如Redis,在应用中可以直接写死127.0.0.1:6379。比如MySQL可以本地直接写死127.0.0.1:3306。这样的好处是,程序不需要单独去维护这些配置,而是由HaProxy或者Nginx等软件来根据实际部署环境灵活配置。
  2. 内网地址中,相同的服务集群机器,设定在同一个局域网IP段呢,比如10台集群,那么就可以指定IP为192.168.0.1到192.168.0.10,方便WestId等获取ID的框架,利用本机IP作为ID中组成部分时,不会发生冲突。
  3. 一个主机,除了root用户,只建立一个app用户,应用都尽量跑在app用户下。
  4. 阿里云ECS,建议在/etc/security/limits.d/90-nproc.conf中把用户最大进程数设置成10000等大一些的数字。

Piper观感:不断的走出舒适区,享受不断的成长

image
《Piper鹬yù》

先欣赏短片。

这是一部仅仅6分钟的动画小短片,却让皮克斯动画耗时近三年才制作完成!除了画面细腻唯美、富有诗意外,我们还能在其中读出很多在成长教育中寓意深刻的道理。

晨曦微茫的小岛上,海水轻柔俯视着纤尘不染的沙滩。伴随着浪花的涌动,浮游生物、海藻、海螺、扇贝等海洋生命搁浅在陆地之上,等待它们的则是饥肠辘辘的捕食者。矶鹬(sandpiper)三五成群,鸣叫着落在了湿漉漉的沙滩上,啄食着可口的扇贝。许是经过了多年的历练,每当新一波潮水涌过来时,它们便第一时间轻盈地躲开,绝不会沾湿半根羽毛。不远处的灌木丛里,一只破壳未久的小矶鹬窥伺着父母的一举一动。少顷,妈妈飞回巢穴,小家伙一如既往张开嘴巴等着喂食,不过妈妈似乎决意要让孩子独立,自顾自将扇贝肉吃到肚子里。没有办法,小矶鹬只能走向海边。然而海水是那么讨厌,一波一波扑向这个未谙世事的小家伙。这时,一个意外突然发生……

然后,家长都容易得出一个观感:让孩子走出舒适区,是成长的第一步。问题是,自己也不是孩子,是不是也需要成长呢。答案肯定是,人需要经过不断的成长。那如何才能不断地成长呢,那当然是不断的走出当前的舒适区。随之而来的就是,当前的舒适区是什么?向哪个方向走出当前的舒适区?怎么做到不断的走出?这个就是本篇博客想一直探讨的内容。

TODO.

盐少许之SRP单职原则

image
图:天空之境茶卡盐湖上的背影,来自500px

读了3天前的思特沃克一篇文章:写了这么多年代码,你真的了解SOLID吗?| 洞见,里面提到了SRP原则是最容易理解,但是却最难于驾驭的一个设计原则,有点像烹饪中“盐少许”一样,不太容易把握。

从SRP的S代表线条美来看,有人觉得胖点好,有人觉得瘦点妙,所谓环肥燕瘦,不能苛求。但是在一个全球化的时代,在一个信息满天飞的时代,这种审美潮流还是趋向一致的。

(未完待续)

不影响程序运行的同时,如何清理大日志文件

要做到这一点,必须记住:

  1. 日志写入方式是append(追加)的;
  2. 对于重定向输出,是两个大于号>>的。

下面的执行脚本演示了,使用
nohup java -jar xx.jar 2>&1 > xx.log &
的情况下,清除日志,
ls -lh看到的大小不会变化,但是
du -sh能看到变化,二者不一致。

如果使用追加的方式,则没有问题。特此记录一下。

[app@app02 et-server]$ ls -lh et-server.log
-rw-rw-r-- 1 app app 1.2G 12月 22 17:36 et-server.log
[app@app02 et-server]$ du -sh et-server.log
1.2G	et-server.log
[app@app02 et-server]$ > et-server.log
[app@app02 et-server]$
[app@app02 et-server]$ ls -lh et-server.log
-rw-rw-r-- 1 app app 1.2G 12月 22 17:37 et-server.log
[app@app02 et-server]$ du -sh et-server.log
8.0K	et-server.log
[app@app02 et-server]$ ps -ef|grep et-server-0.0.1.jar|grep -v grep
app      22566     1  0 Dec08 ?        00:33:16 java -jar et-server-0.0.1.jar
[app@app02 et-server]$
[app@app02 et-server]$ kill -9 22566
[app@app02 et-server]$
[app@app02 et-server]$ nohup java -jar et-server-0.0.1.jar 2>&1 >> et-server.log &
[1] 27631
[app@app02 et-server]$ nohup: 忽略输入重定向错误到标准输出端

[app@app02 et-server]$ ps -ef|grep et-server-0.0.1.jar|grep -v grep
app      27631 27534 60 17:38 pts/0    00:00:03 java -jar et-server-0.0.1.jar
[app@app02 et-server]$
[app@app02 et-server]$ ls -lh et-server.log
-rw-rw-r-- 1 app app 1.2G 12月 22 17:38 et-server.log
[app@app02 et-server]$ du -sh et-server.log
164K	et-server.log
[app@app02 et-server]$ > et-server.log
[app@app02 et-server]$
[app@app02 et-server]$ ls -lh et-server.log
-rw-rw-r-- 1 app app 0 12月 22 17:38 et-server.log
[app@app02 et-server]$ du -sh et-server.log
0	et-server.log
[app@app02 et-server]$

自己挖坑自己填1:飞越地平线的共享状态

飞越地平线爽后的烦恼

image

那天在上海迪士尼辛辛苦苦排队1小时看了很过瘾的5分钟《飞越地平线》后,就看到冯雨在QQ上向我抱怨,上周我调整过后权限系统貌似有问题,我很气愤,因为我比较讨厌貌似,有问题就有问题,为什么要貌似呢,然后就让我问测试去。

测试没有跟我提,我自己回到宾馆自己简单测了一下,没发现什么问题。周末在折腾完多租户连接池后,又整了一遍权限,也没有发现问题。就等下周一(今天)上班再说吧。

一会行一会不行

开完每日站会后,第一件事情,就是询问测试郑梦蓉,权限咋个不行。很快就报告了,说现在测试环境分店1上,馆主就看不了会员详情。我看了,果然如此。
image

我在代码中加了日志,重新部署了应用,然后再进入测试环境分店1上看会员详情,可以了。此时,再使用馆主登录到测试环境分店2上看会员详情,诡异的是,报告权限不足了。

然后我一通检查代码,检查缓存数据,好像没发现什么问题。然后只有打印更加详细的日志,在大量的权限日志中,发现了端倪,就是在mergeRoleRights合并相同权限的服务包ID后,发现使用注解标注的权限,被丢失了。

/**
 * 访客和会员合并后的 访客和会员的详情页
 *
 * @param userId
 * @return
 */
@YogaFunctionRight(roles = {RoleIds.MERCHANT_100, RoleIds.SERVICE_102, 
        RoleIds.MANAGER_108, RoleIds.ADVISOR_107}, pkgs = {Pkgs.企业版_10011200, Pkgs.创业版_10011100})
@RequestMapping("/find-visitor-or-member-detail/{userId}")
public MemberDetailDvo findMemberDetail(@PathVariable String userId) {
    return userService.findMemberDetail(userId);
}

然后我就再次检查mergeRoleRights的代码,终于发现了问题的真相了。

@Data @AllArgsConstructor @NoArgsConstructor
public class BasicRoleRight {
    private String roleId;
    private String rightCode;
    private String servpackId;

    private boolean merged;
}

private List<BasicRoleRight> mergeRoleRights(List<BasicRoleRight> allRoleRights) {
    List<BasicRoleRight> mergedRoleRights = Lists.newArrayList();
    for (int i = 0, ii = allRoleRights.size(); i < ii; ++i) {
        val roleRight = allRoleRights.get(i);
        if (roleRight.isMerged()) continue;

        for (int j = i + 1; j < ii; ++j) {
            val other = allRoleRights.get(j);
            if (other.isMerged()) continue;

            mergePkgIds(roleRight, other);
        }

        roleRight.setServpackId(roleRight.getServpackId() + ",");
        roleRight.setMerged(true);
        mergedRoleRights.add(roleRight);
    }
    return mergedRoleRights;
}

private void mergePkgIds(BasicRoleRight l, BasicRoleRight r) {
    if (!isRoleIdRighCodeSame(l, r)) return;

    val rPkgId = r.getServpackId();
    if (rPkgId == null) return;

    val lPkgId = l.getServpackId();
    l.setServpackId(lPkgId == null ? rPkgId : lPkgId + "," + rPkgId);
}

原来是共享状态的坑

问题在哪里呢,在BasicRoleRight对象的merged状态上,而本身BasicRoleRight对象是全局共享的,但是对于不同商户,会根据商户配置的服务包做一次过滤和合并,结果第一次访问的商户权限OK,后面访问的就不行了,因为merged状态已经被设置了。所以造成测试环境中,两个测试分店,重新发布后,第一个访问的分店权限可以,第二个就不行了。只要定位了问题,解决起来都是分分钟的了。干掉共享状态。

@Data @AllArgsConstructor @NoArgsConstructor
public class BasicRoleRight {
    private String roleId;
    private String rightCode;
    private String servpackId;
}

private List<BasicRoleRight> mergeRoleRights(List<BasicRoleRight> allRoleRights) {
    List<BasicRoleRight> mergedRoleRights = Lists.newArrayList();
    Set<Integer> merged = Sets.newHashSet();
    for (int i = 0, ii = allRoleRights.size(); i < ii; ++i) {
        val roleRight = allRoleRights.get(i);
        if (merged.contains(i)) continue;

        for (int j = i + 1; j < ii; ++j) {
            if (merged.contains(j)) continue;

            val other = allRoleRights.get(j);
            if (mergePkgIds(roleRight, other)) {
                merged.add(j);
            }
        }

        roleRight.setServpackId(roleRight.getServpackId() + ",");
        merged.add(i);
        mergedRoleRights.add(roleRight);
    }
    return mergedRoleRights;
}

private boolean mergePkgIds(BasicRoleRight l, BasicRoleRight r) {
    if (!isRoleIdRighCodeSame(l, r)) return false;

    val rPkgId = r.getServpackId();
    if (rPkgId == null) return true;

    val lPkgId = l.getServpackId();
    l.setServpackId(lPkgId == null ? rPkgId : lPkgId + "," + rPkgId);
    return true;
}

后话

填平了这个坑后,真实地感受到了,要是使用Immutable对象的话,或许这种坑就不会挖了。

草蛇灰线,伏脉千里之一致性

在知乎中看到一句话,深为欣赏,先记录下:

一致性设计是每一个体面的设计者都应当注意的问题。在神雕侠侣中,郭襄长大后与杨过的初次相逢是在风陵渡,到了倚天屠龙记中,灭绝师太告诉周芷若,她的师父、郭祖师的徒儿叫做风陵师太。权利的游戏中,詹姆把布兰从楼上推下,后来他儿子托曼也是坠楼而亡。前后呼应,因果轮回。更不用说红楼梦里让人津津乐道的草蛇灰线伏延千里。细细品味,回味无穷,所以它们才能成为经典。

十万个为什么:为什么选择RUST

In the real world, Rust is the coating closest to the bare metal.
image
图片来自于Why Programming Language “Rust” is Getting all the Love

没事翻翻slideshare上的ppt。一些觉得好的,收藏一下。

Rust这个名字是个什么鬼?

TL;DR: Rust is named after a fungus that is robust, distributed, and parallel. And, Graydon is a biology nerd.
I was fiddling with a number of names. Primarily it’s named after the pathogenic fungi which are just amazing multi-lifecycle, multi-host parasites, fascinating creatures.

But the name “rust” has other nice features. It fits with our theme of being “close to the metal” and “using old well-worn language technology”. It’s also a substring of “trust” and “robust”. What’s not to like?
Liigo的翻译
我从很多名称中挑选出了"Rust"。主要受到病原菌的启发——那些令人惊讶的、生死轮回的、多源寄生的奇妙物种。而且"Rust"还有其他内涵:它很好地契合了“贴近金属”(bare metal)、“复古编程语言技术”的主题;它从字面上融合了"Trust"(信任)和"Robust"(健壮)。 —— Graydon Hoare

创作者之一是JS之父

image

C/C++永远的痛:内存泄漏、悬停指针、野指针

image

Ownership is intuitive

image
图来自:https://www.slideshare.net/InfoQ/rust-systems-programming-for-everyone

我是这些常数收藏爱好者

这些常数,我会慢慢的背下来,以此证明,我还没有老。

1秒的定义

image
图:铯原子钟
一秒被定义为铯-133(铯与钠为同一主族元素)原子基态的两个超精细能级之间跃迁对应辐射9,192,631,770个周期的时间。

真空光速c

image

1972年,美国的K.M.埃文森等人直接测量激光频率ν和真空中的波长λ,按公式 (其中v为真空中电磁波的速度, 为真空磁导率, 为真空介电常数)算得c=(299792458±1.2)米/秒。1975年第15届国际计量大会确认上述光速值作为国际推荐值使用。既然真空中的光速已成为定义值,以后就不需对光速进行任何测量了。

自然对数的底数e

image
image

e,作为数学常数,是自然对数函数的底数。有时称它为欧拉数(Euler number),以瑞士数学家欧拉命名;也有个较鲜见的名字纳皮尔常数,以纪念苏格兰数学家约翰·纳皮尔 (John Napier)引进对数。它就像圆周率π和虚数单位i,e是数学中最重要的常数之一。
它的其中一个定义是 ,其数值约为(小数点后100位):“e ≈ 2.71828 18284 59045 23536 02874 71352 66249 77572 47093 69995 95749 66967 62772 40766 30353 54759 45713 82178 52516 64274

圆周率π

image

圆周率(Pi)是圆的周长与直径的比值,一般用希腊字母π表示,是一个在数学及物理学中普遍存在的数学常数。π也等于圆形之面积与半径平方之比。是精确计算圆周长、圆面积、球体积等几何形状的关键值。 在分析学里,π可以严格地定义为满足sin x = 0的最小正实数x。
一起来背圆周率:
3.1415926535897932384626 433832795028841971693993
75105820974944592307816 40628620899862803482534
21170679821480865132823 06647093844609550582231
72535940812848111745028 41027019385211055596446
2294895493038196442881 09756659334461284756482
33786783165271201909145 64856692346034861045432
66482133936072602491412 73724587006606315588174
88152092096282925409171 53643678925903600113305
30548820466521384146951 94151160943305727036575
95919530921861173819326 11793105118548074462379
96274956735188575272489 12279381830119491298336
73362440656643086021394 94639522473719070217986
09437027705392171762931767523

多租户系统数据库连接池的那些事

多租户连接池

最近对多租户数据库连接池做了一下改造。改造之前,是一个商户一个小池子,数据库连接只是在小池子里共享。改造之后,是所有商户都一个大池子,数据库连接在大池子**享。每个商户请求时,从大池子里拿连接,然后切换到自己的商户库中。
效果还是杠杠的,一下子把以前的尖刺给削平了。纪念一下。
image

image

改造之后留下的小坑

从连接池中获取连接后,需要将连接切换到当前请求的商户库上。但是如果连接池上次使用时,就是当前商户库,则跳过切换的动作。这个设计本意是利用状态,避免重复切换。但是在实际部署过程中,由于新开商户的数据库,暂时需要手工配置账户权限,导致没有配置权限之前,出现串库现象。很快就从代码中,找到了问题。
image

  1. 第一次访问,连接被打当前商户标记(本质是连的别的商户库)后,切库失败抛出异常;
  2. 用户再次尝试访问,刚好拿到上次的连接,发现刚好是当前商户(假象),不切库,导致串库。

优雅代码实践2 之 黄金比例1:1:1

今天给小组开发人员做了一次简单的分享,分享完了,再给取了这么一个名字,因为联想到了金龙鱼食用油曾经到处轰炸的广告词1:1:1(随便百度一下,才知道是指 不饱和脂肪酸:中度不饱和脂肪酸:饱和脂肪酸 为1:1:1)。我这里指,业务上的一项配置,对应到配置表中的一条记录,对应到代码中的一个Class,所以也是1:1:1。
image

我把这种1:1:1也称之为一一映射,这个是数学中的概念,但是应用到业务开发中来,也是一种很好的借鉴。这种映射,是一种天然的映射,非常简单明了,无需经过大脑二次转换,是非常直接的方式。话说如果原始人最多只能数到数字3,那么Ta怎么能确定左手和右手的手指头数是一样多呢?对了,两只手掌对起来,形成一一映射,左手有一只大拇指,右手有且仅有一根大拇指与其映射,反之如此。

更多的关于构建一一映射,可以翻阅《图灵的秘密》这本书。比如,里面说到能与自然数数列形成一一对应关系的集合,称之为可数的;比如0与1之间实数,可以与大于1的实数一一对应,只需要每个数求一下倒数即可。
image

PS:

  1. 原始设计:
    image
    存在问题:1) 一条业务需要对应到多条记录,是在(业务类型、发送类型、发送角色)三者之殇一个笛卡尔全集;2) 根据触发条件(TRIGGER_SITUATION)进行查询配置,缺乏与业务的直接映射;3) 页面上需要写死诸多业务代码;总之,不是一一映射,是一对多映射,缺乏直观性,不利于数据维护。

  2. 那么多的属性,怎么会不遗漏呢?答:还是一一对应。使用IDEA的分栏模式,Alt+鼠标选择的列模式,再加数字顺序编号(String Manipulation插件)。
    image

  3. 在代码方法的注释上,模板与示例,也形成一一对应关系,有助于对参数设置的理解。
    image

  4. 在枚举变量上,我采用了一以贯之的“大白话”的形式,不做任何编码约定,直接使用“馆主”,“店长”,“教练”,“会籍”,“客服”的字眼,而不是之前的编码100、108、101、107、102。这种编码我总是记不住,每次我都得去下面这张表去找答案(也或许是我主观不愿意去记忆这种编码的原因了)。
    image

负反馈调节 之 🦅是如何抓到🐰的

image
图片🦅抓🐰,来自500px

控制论,看起来很无聊,然而事实是,这本书一!点!也!不!无!聊!《控制论与科学方法论》是一本非常有趣的科普作品!作者用了大量生动的事例,深入浅出地解释了“控制论”这个看似高深的概念。
image

三论:系统论、控制论、信息论

系统论的创始人是美籍奥地利生物学家贝塔朗菲。

系统论要求把事物当作一个整体或系统来研究,并用数学模型去描述和确定系统的结构和行为。所谓系统,即由相互作用和相互依赖的若干组成部分结合成的、具有特定功能的有机整体;而系统本身又是它所从属的一个更大系统的组成部分。贝塔朗菲旗帜鲜明地提出了系统观点、动态观点和等级观点。指出复杂事物功能远大于某组成因果链中各环节的简单总和,认为一切生命都处于积极运动状态,有机体作为一个系统能够保持动态稳定是系统向环境充分开放,获得物质、信息、能量交换的结果。系统论强调整体与局部、局部与局部、系统本身与外部环境之间互为依存、相互影响和制约的关系,具有目的性、动态性、有序性三大基本特征。

信息论是由美国数学家香农创立的,

它是用概率论和数理统计方法,从量的方面来研究系统的信息如何获取、加工、处理、传输和控制的一门科学。信息就是指消息中所包含的新内容与新知识,是用来减少和消除人们对于事物认识的不确定性。信息是一切系统保持一定结构、实现其功能的基础。狭义信息论是研究在通讯系统中普遍存在着的信息传递的共同规律、以及如何提高各信息传输系统的有效性和可靠性的一门通讯理论。广义信息论被理解为使运用狭义信息论的观点来研究一切问题的理论。信息论认为,系统正是通过获取、传递、加工与处理信息而实现其有目的的运动的。信息论能够揭示人类认识活动产生飞跃的实质,有助于探索与研究人们的思维规律和推动与进化人们的思维活动。

控制论是著名美国数学家维纳(Wiener N)

同他的合作者自觉地适应近代科学技术中不同门类相互渗透与相互融合的发展趋势而创始的。它摆脱了牛顿经典力学和拉普拉斯机械决定论的束缚,使用新的统计理论研究系统运动状态、行为方式和变化趋势的各种可能性。控制论是研究系统的状态、功能、行为方式及变动趋势,控制系统的稳定,揭示不同系统的共同的控制规律,使系统按预定目标运行的技术科学。

什么是控制论?

根据创始人维纳(Nobert Wiener)的定义,控制论(Cybernetics)是“关于动物和机器中控制和通信的科学”,简言之,控制论的中心问题就是控制与通信。维纳的定义中同时声明了控制论的研究对象,即动物和机器。然而广义上的机器这个概念涉及的范围极为广泛,所以控制论这门学科极具普适性。

同构与同态,摘自“为什么人人都该学点控制论”

所谓同构,就是两个系统之间存在某种“一一对应”的关系,所有的状态只要经过一个统一的变换就能把这一个系统变成另一个,不外如是。照片与底片是同构的,摩天大楼与它的设计图纸是同构的,纸质书与它的电子版也是同构的。

同构的要求太过严格,所以很多情况下难以实现,比同构更广泛也更深刻的现象是同态。同态的意义也很简单,如果说同构是相等,那么同态就是相似。同构要求两个系统之间建立“一一对应”的联系,同态只要求两个系统之间建立某种“多一对应”,即要求两个系统某些部分相等,或者忽略某些性质后同构。假如我是一个只能看到红色的红绿色盲,那么我看到的红绿灯与正常人看到的红绿灯是同态,正常人看到的红绿灯经过(红,绿)->红的变换后就与我看到的同构了。

由此可见,建立同态的一个重要手段就是模糊化,忽略一些部分的同时保留重要的性质,将多个状态或者多个部分看成一个,就可建立一个更简单的同态系统。说到这里,那两个字就要呼之欲出了:建模。模型大多是对现实世界的简化,通常情况下是理想的、不会在现实世界中出现的系统,然而却能帮助人们考察研究对象的本质。

控制论的一个重要意义就在于,它能够帮助我们建立这样的同构/同态,方法就是构造带状态的、抽象的能动系统。同构/同态都保留了原象的性质(构造同态的时候一定要将想要研究的性质保留下来),要利用这一点,通过建立与被研究系统同构/同态且易于研究的系统,去研究“那个系统”的性质。这便是同构/同态的意义。

豆瓣的评论

让这么多人窥见上帝的秘密😒

早就听人推荐过金观涛、华国凡两位老师写的《控制论与科学方法论》,但忌惮于「控制论[^cybernetics]」这三个字,直到最近才找来读读。我去~~这本小书早该绝版了,让这么多人窥见上帝的秘密!两位老师写作手法之动人,说这是一本小说也不为过。实在不能想象这是在 1983 年写的😱

知乎上的文章

好玩又有用的「控制论」~

发射嫦娥三号到月球,火箭为什么不像打枪一样直接往月球奔?想想看,地月380000km的距离,卫星才多大,这无异于用狙击枪打1000m外的蚊子,你再精准也打不到,况且蚊子在动,月球也在动。

再来看一个例子,老鹰抓兔子,兔子一慌就赶紧跑,老鹰不可能在高空看见兔子跑的那一刻就算好兔子的运动方程吧。数学不好没关系,咱有眼睛估计一下目标兔子的大致距离,先大致地冲过去,然后一直盯着它,不断向鹰脑报告和目标的差距,连续地调整动作以减小目标差,直到目标差为0,逮到兔子。这只老鹰之所以成功,是因为它用了负反馈调节的方法。

负反馈调节:通过系统不断把自己的控制后果与目标作比较,使得目标差在一次一次控制中慢慢减少,最终扩大了控制能力的过程。

老鹰抓兔子用的这套负反馈调节的办法用在了导弹打飞机上。导弹上装了红外线装置(眼睛),配上计算机(大脑),不断把位置(控制后果)和飞机(目标)作比较,减少目标差到0,把飞机炸开花。当然发射月球卫星也采用了这个办法。

负反馈调节提供了解决许多实际问题的思路。

做一款APP,有的人忙了半天,憋出了一些个Big Idea,说我这个创意用户肯定喜欢,然后就开始做,还一定要花很多时间开发出许许多多功能,自以为把东西做得非常完善了,才让这个产品上线,接着在很多个渠道做推广,说我这个产品怎么怎么好。这种做法的错误在于自己意淫用户的需求,看起来一直在做,实际上根本没有把控制后果(APP内容)和目标(用户需求)作比较,不懂反馈,十有八九会失败。

正确的做法则是运用负反馈调节的理念,先有一个想法:用户可能对这个需求感兴趣。再去做用户调查,明确我想要开发的功能和用户真正的痛点之间到底有多少差距,这就是目标差。先做出一些基本的功能,在两个渠道做推广,接着回收数据并一直进行着用户调查,通过多次反馈来实现产品的迭代优化。

这里用到的“用户调查——内容制作——投放渠道——数据反馈——调整优化”的理念也是优秀的产品经理和运营人员必备的思维。

而看了10个减肥技巧、学了100个自我管理的方法、收藏了1000套书单,不行动,它们永远是信息的垃圾。

就像我写的这篇5000字的文章,获得知识——思考整理——写成文字,这个过程让我自己对控制论的理解要比没写文章之前要更加透彻。
控制论与科学方法论.pdf

沧海拾遗-Linux/Unix设计**(Linux and the Unix Philosophy)

一本小书,很容易就随手翻完的,然后再针对里面的一些**,细细琢磨。

image

<Linux/Unix设计**>一书主要介绍了unix系统设计中的一些原则,其中包含了九个主原则和十个原则。

九大主原则

  1. SMALL

即“小即是美”。小则灵活,并易于改变去适应变化。而大则牵一发而动全身,一旦改变就会伤筋动骨。说到这个原则,就想起我们it界那个长久以来口口相传的笑话,说工程师老是被pd说改需求改需求,从而狂燥甚至发狂。笑一笑后,是不是该想一想,之所以出现这么难以改变的情况,是不是将系统设计和实现得太过大了呢?所以才会在有变化的时候如果痛苦?

  1. 1THING

即“让程序只做一件事”。这类似于OOP编程**中的SRP原则(单一职责),以前理解这个原则大约是觉得这样做的一个理由这样易于维护,或者可读性强上。但是看了这本书的介绍后发现,单一职责的优点不仅在可读性和维护性这些上面,还在性能上。因为一个方法如果小,则在调用完毕后,它使用的局部变量等就可以马上回收,降低资源的开销。

  1. PROTO

即“尽快建立原型”。这里提到的一个产品的三个系统感觉相当有意思。所谓三个系统分别是:第一系统,是指在资源紧迫情况下,而开发的预研性质的系统;第二系统是在第一个系统出现后,看到第一个系统的价值后,也看到第一个系统实践的结果后而产生的,在第一系统的基础上做出大量的优化和扩展;第三系统是在第一和第二系统出现后,去芜存精,最终比较完美的一个产出。如果投身于第一个系统的开发,你要冒很大的风险,因为它极有可能失败,但是在开发第一个系统的团队中工作将是一件最有激情的事.如果投向第二个系统,前期还好,你有可能接触到最顶尖的设计,但是到后期,就没意思了,就是重复的添加功能而已。不过不经历第一系统的后期也无法进入第三系统,所以在工作中,评估产品的阶段,选择投向其中,未尝不是一个好的方法。

  1. PORT

即“可移植性高于效率”。之所以推崇这个是因为作者认为计算机的性能总是越来越强,因此效率应该不是程序关心的第一优先级,但是向更高性能计算机的移植性却是程序应该关注的。这个我觉得倒是有所保留,近年来随着处理数据的海量化、什么云啊,大数据啊,就说明现在的计算机的性能增长速度还赶不上我们要处理的数量量增长速度。而且最近几年并发编程为什么这么火,因为硬件生产商已经无法保持业界的摩尔增长率了,所以只能通过软件的方式来保持性能的增长。而并发多核的编程就是在这种情况被程序员所追捧。

  1. FLAT

即“使用纯文本来存储数据”。为什么呢?因为这样可以使数据具有最大的可移植性。

  1. REUSE

即“充分利用软件的杠杆效应”。在编程中有一个原则尽量去重用别人已有的代码,来提高自己的工作效率。但是有时候你不得不承认,去读懂一些人写得代码,远没有自己重写来得简单。因此如果你要重用别人的代码,那么尽量重用一些优质的代码吧,并且要时刻提醒自己,别人的代码是不安全的。。。

  1. SCRIPT

即“尽量使用shell脚本”。普适地来讲,就是我们在实现程序时尽量选择一些可移植性高的编程语言,比如说java。

  1. NOCUI

即“避免强制性用户界面”(Captive User Interface)。这个不是太好理解,也没遇到过这种场景,以后再说。。。

  1. FILTER

即“将程序用途过滤器”。就是使程序拥有输入、输出和异常处理,这样就可以将一个大型程序分解成若干个过程,并相互约定沟通方式,这不就是我们多人并发完成某项功能所需要做的事情吗?

十大次原则

  1. custom

即“让用户定制系统”。这个对于选择linux的人来说很有感受,什么东西都想控制在自己手中。但是说到底还是要了解你的产品所面向的群体对这个产品的定位,有些人就是想傻瓜一点,那就傻瓜一点吧。

  1. kernel

即“使内核最小化”。这也符合了小即是美的原则,如果将系统内核设计得小而轻量,然后其他人可以自由的在此基础上扩展,这就是一个完美的方案了。成为其他人的平台,进而构建一个生态圈,是由小变大,立于不败的不二法则。

  1. lcase

即“统一小写”。这个属于个人风格了。

  1. trees

即“不要将数据固化到纸上,节约树林”。

  1. silence

即“沉默是金”。指有些场景不需要向用户反馈信息时,就不要反馈。

  1. parallel

即“并行思考”。多核计算机总要发挥它的作用吧。

  1. sum

即“部分之和大于整体”。这个取决于一个好的设计,如果将各部分组合成整体的成本太大,则有可能部分之和小于整体了。

  1. 90percent

即“寻找90%的解决方案”。这个大家估计深要体会,有时候为了解决那10%甚至1%才会出现的问题,往往要投入巨大的精力,这个时候舍弃那1%的杞人忧天也许未尝不是一个好的抉择。不过如果你要冲向巅顶的话,你势必要面临这1%的挑战,这也是程序员乐此不疲的事情。

  1. worse

即“更坏就是更好”。不过理解起来似乎是与其做不好,还不如选择一个已有较差的方案。

  1. hier

即 “层次化思考”。对系统、软件分层,有利于维护开发。

Linux and the Unix Philosophy.pdf

好的设计特点之一:让问题浮现在水面之上

image
(图中的石头好比问题)

今天改造了一下瑜伽系统的权限设计,可以使用注解的形式,直接在代码上面声明权限了,如果不声明,则访问会被显式拒绝,比如:

/**
 * 查询员工优惠权限.
 *
 * @return 员工优惠权限.
 */
@YogaFunctionRight(roles={RoleIds.MERCHANT_100, RoleIds.MANAGER_108}, pkgs = {Pkgs.企业版_10011200, Pkgs.创业版_10011100})
@RequestMapping(value = "/query-staff-preferential-right", method = GET)
public StaffPreferentialVo queryStaffPreferentialRight() {
    return mbrCardService.queryPreferentialRight();
}

界面如下:
image

之前则需要给平台工程师张存鑫发送邮件,在表中添加权限,比如:

insert into tt_d_funcright(RIGHT_CODE, RIGHT_NAME, RIGHT_TYPE, RIGHT_DESC, SERVPACK_ID, `URL`, CHAIN_NAME, CHAIN_DEFINITION, `ORDER`) values
('CARD-0036', '员工开续卡权限设置', '2', '员工开续卡权限设置', '10011200', null, '/mbr-card/update-staff-preferential-right', 'perms[CARD-0036]', '1'),
('CARD-0036', '员工开续卡权限设置', '2', '员工开续卡权限设置', '10011100', '', '/mbr-card/update-staff-preferential-right', 'perms[CARD-0036]', '1'),
('CARD-0037', '员工开续卡权限查询', '2', '员工开续卡权限查询', '10011200', null, '/mbr-card/query-staff-preferential-right', 'perms[CARD-0037]', '1'),
('CARD-0037', '员工开续卡权限查询', '2', '员工开续卡权限查询', '10011100', '', '/mbr-card/query-staff-preferential-right', 'perms[CARD-0037]', '1');

insert into tt_f_role_right(ROLE_ID, RIGHT_CODE, BEGIN_TIME, END_TIME, OPERATOR_ID, GRANT_TIME) values
('100', 'CARD-0036', '2017-07-14 16:27:55', '2117-02-24 16:27:55', '0', '2017-02-24 16:27:55'),
('108', 'CARD-0036', '2017-07-14 16:27:55', '2117-02-24 16:27:55', '0', '2017-02-24 16:27:55'),
('100', 'CARD-0037', '2017-07-14 16:27:55', '2117-02-24 16:27:55', '0', '2017-02-24 16:27:55'),
('108', 'CARD-0037', '2017-07-14 16:27:55', '2117-02-24 16:27:55', '0', '2017-02-24 16:27:55');

然后等到在测试环境部署时,在测试环境上执行一下;等到再生产环境部署时,在生产环境执行一下。

这里面有几个问题,容易出错:

  1. 开发人员忘了发送权限维护邮件;
  2. 平台人员忘了执行权限维护邮件;

这两个问题不止出现过一次,而是多次发生。顾聪和冯雨都说,某个没有配置的URL我发了权限变更邮件啊,还翻出邮件记录。我相信他们发了,也相信张存鑫在版本发布的时候,各种问题忙起来很容易就漏掉了。我跟冯雨说,换你来搞平台,也很有可能在忙的时候漏掉变更邮件的执行。所以最好从技术上把这个人的容易出错的因素降到最低。

最大的问题的是,改造前的系统,如果没有在tt_d_funcright和tt_f_role_right中配置,则默认为登录即可访问。造成的结果是,本来应该只能给馆主的权限,结果漏掉配置,实际上所有登录用户都可以访问了。这个问题在改造前的设计里面被隐藏起来了(水面之下),所以没人会注意到。而在新的设计中,只要漏掉了,那么就直接无法访问,让问题立即浮出水面了,即使忘记了,也会有立即反馈。

所以,好的设计,不是去掩盖问题,而是让问题立即浮出水面,从而形成一种良好的“负反馈”机制。

想到之前我一直讲的挑水的例子,大水缸离水源有20米,有10个人一起领到一个小任务,就是把大水缸的水满上。有两种方式来完成这个任务:第一种是10个人各自拧着水桶去水源地取水,然后各自拧到水缸去倒水,直到水满;第二种是10个人在水缸和水源之间排成一条长队,然后接力传输满水桶到水缸,再接力传回空的水桶。两种方式各有优劣,但是从“让问题浮出水面”的设计思路来看,显然第二种方式更好。第二种方式显得更加有秩序,一旦有人生病了、摔跤或者偷懒了,那么马上水就无法传递,接力就停止了,问题马上显现出来了。第一种方式显得就混乱,有人偷懒有人摔跤有人生病就很难看出来。

回到权限系统的设计改造上来,改造完了以后,除了能让权限问题立即浮出水面之外,而且更加省事了(再也不用发邮件,不用平台人员执行邮件了),不亦乐乎。
股神巴菲特(Warren Buffett)的话说:"只有退潮了,才知道谁在裸泳"(You only find out who is swimming naked when the tide goes out.)。放在技术上,如果问题被掩盖了,那么等黑客攻击安全问题爆发的时候(退潮),那就惨了(裸泳暴露)了。

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.