golang客户端取消http请求
Tag golang, cancelable, http, on by view 3432

首先,创建带有Cancel Context的http请求

var cx context.Context
cx, req.cancel = context.WithCancel(context.Background())

if req.httpreq, err = http.NewRequest(req.method, req.url, reqbody); err != nil {
    return
}
req.httpreq = req.httpreq.WithContext(cx)
req.httpreq.Header = req.Headers
req.httpreq.ContentLength = reqbodyLength

然后,需要停止请求的时候调用req.cancel()方法

if req.cancel != nil {
    req.cancel()
    fmt.Println("running cancel...")
}

return nil

这样才能够客户端强行主动取消正在进行的http连接。就是这么简单


webp动图循环n+1的问题
Tag webp, 循环, on by view 3761

最近需要用ffmpeg将视频截取转为webp动图,但是发现截取后的视频在Chrome浏览器上打开播放循环次数不对

./ffmpeg -i xxx.mp4 -map_metadata -1 -s 640x360 -r 15 -t 00:00:02 -loop 1 xxx1.webp

以上命令中 -loop 1 指定动画循环1次,但是所生成的图片在Chrome上播放却是循环2次,同时发现截取 gif 也存在这个问题

./ffmpeg -i xxx.mp4 -map_metadata -1 -s 640x360 -r 15 -t 00:00:02 -loop 1 xxx1.gif

但是后来我发现将 -loop 指定为-1却可以截取一次循环的gif,但是 -loop -1 这个参数如果用在截取 webp 就会报错。

最后我找到了 libwebp 项目,在其压缩包目录中找到了官方的 webp 查看工具 vwebp.exe ,用它播放 webp 发现却是正常的,Chrome上播放循环两次的图用它播放只有一次。这说明是 Chrome 的bug,并非 ffmpeg 转码的问题。有人也发现了这个问题。 链接  



linux打开文件数太多
Tag linux, 打开文件数, on by view 2965

今天dus服务并发量稍高,然后程序内部不停地报错 too many open files ,linux 系统对打开文件数是有限制的。介绍这个的文章很多。查看服务器上系统设置的最大限制,运维设置的是 102400,列出所有进程的打开文件数目 lsof -n|awk '{print $2}'|sort|uniq -c |sort -nr|more ,服务程序打开文件数最大时也才1w多,所有进程打开文件最大值加起来也才五六万,远远没有达到 102400,一直不解为何报这个错误,最后查看程序进程的限制 cat /proc/32523/limits ,发现限定值居然是 1024

Limit                     Soft Limit           Hard Limit           Units     
Max cpu time              unlimited            unlimited            seconds   
Max file size             unlimited            unlimited            bytes     
Max data size             unlimited            unlimited            bytes     
Max stack size            8388608              unlimited            bytes     
Max core file size        0                    unlimited            bytes     
Max resident set          unlimited            unlimited            bytes     
Max processes             31170                31170                processes 
Max open files            1024                 4096                 files     
Max locked memory         65536                65536                bytes     
Max address space         unlimited            unlimited            bytes     
Max file locks            unlimited            unlimited            locks     
Max pending signals       31170                31170                signals   
Max msgqueue size         819200               819200               bytes     
Max nice priority         0                    0                    
Max realtime priority     0                    0                    
Max realtime timeout      unlimited            unlimited            us

服务是通过 systemd 启动的,原来在 service 中有个设置项可以设置程序能打开的最大文件数,可是我的 service 文件中没有设置,于是默认限定为 1024,很容易就超了。修改添加配置项

[Service]
LimitCORE=infinity
LimitNOFILE=102400
LimitNPROC=102400

重启服务,再次查看 cat /proc/32523/limits ,Max open files 成功变为 102400,再也没有报这个错,解决。



2016阶段总结
Tag 工作, 总结, on by view 4203

随着时间的推移,我的上一份工作已经告一段落,今后的一段时间离我将会作为Golang软件工程师而存在,而不是现在的Java程序员。

关于语言

我由初入职场时的Java语言开发到现在的转向Golang开发,这并不意味着我觉得Golang比Java好,只是单纯的改变了一下技术方向而已。对于编程语言,我并不像大多数人那样有固定的偏好,而且部分人在语言之争上比较偏激;对于我来说我接触过的所有的编程语言我都喜欢,同时,我也不像大多数人那样只会一门编程语言,比如Java或PHP,在某些技术社区上Java开发者与PHP开发者总是为了诸如“谁是最好的编程语言”这一议题争得不可开交。

从我接触编程到现在先后接触了一些语言,最早在高中的时候是Visual Basic,后来进入大学了正式学习编程了,在大一上学期自学入门的语言是JavaScript,因为我在高中时候就有一个建网站的梦想,所以了解前端技术栈相对于我的同学要早,因此我进入大学的时候就知道了JavaScript在前端技术中的地位。大一下学期大学课程开始学习C++了,与此同时我开始自学PHP,开始了解并使用MySQL数据库,也是此时我正式开始理解“面向对象”这一概念,后来大二学校课程学习了C#这门语言,同时这一年我在学校学生组织里面开始为学校建设网站,用的是PHP语言,MySQL数据库以及Linux服务器,对Linux服务器的了解也是从此时开始的。用PHP写网站大概写了一年多的时间,期间由于我对前端非常了解,那时的我算是个比较菜的全栈工程师了。大二是疯狂的一年,每天晚上至少凌晨2点才睡觉,因此导致一些课程挂科了,大三是反省的一年。同时大三也是技术沉淀的一年,大三我开始重新学习C语言,这回是认认真真的学习,在解决了一个个的内存错误(segment fault)之后,对编程有了新的理解,对于内存,指针,文件,网络这些东西有了进一步的了解。那一年我开始写了一些小项目以及pinyin-php,同时学习了Golang,当时我觉得Golang太好学了,从完全不了解Golang到用Golang的Beego框架写出一个博客系统只用了两周时间,或许是因为之前在学C语言,初学Golang感觉自己如同解放了一般。

总之,我对任何语言都是一视同仁,我并没有觉得Java不好,相反,我觉得Java是各方面都比较完备的一门语言。而且,就我目前来看,我更想了解的是语言的本身,因此我将编译原理作为我之后要研究的领域之一。

关于项目

论项目经验,我从大学时开始做第一个真正的项目到现在也有大约4年了吧。最初无人领路不框架为何物,靠着自己的毅力不停的堆代码,发现最后自己堆砌的PHP代码的复杂都已经超出了我的理解范围了,项目开始混乱得我已经无法维护了,于是,第一个项目以失败告终。后来,了解并学习了Web框架发现使用框架写代码更加得心应手。而最近的项目中的经历再次印证了一个好的框架是多么的重要;由于目前的项目的创始人打着“验证无框架开发的思路”的旗号搞了现在这么个项目——底层封装度非常底,该封装的都没有封装,而他所写的简单的封装并没有做到一个“框架”所应当做的事情,导致现在这个项目面临了许多比较坑的问题,比如数据库不能使用联查,这一问题同时导致了大家尽量避免联查,因此,项目在建数据表的时候也尽量的避免联查,于是数据表中出现了大量的冗余,再加上项目的各种改需求,数据结构已经变得不能看了。

这些项目经历告诉我“无框架”是扯淡,当项目复杂到一定程度“无框架”的项目将会复杂到无法维护,一个比较好的项目首先是要有一个比较好的框架,而不是无框架,好的框架应该是低耦合的,而且该有的基础功能都是有的,并且应该是可以扩充的,比如某些框架中的中间件概念。

关于工作

有时候在一个岗位上待久了并不是一件好事。首先,时间一长,在技术上就难以有新的收获,每天过着写业务逻辑的日子早就让我厌倦。然后,呆久了就不知道自己现在的价值如何了,也不了解市场的行情如何,所以我觉得不管是否有换工作的打算,每年都应该出去面试一波,一来了解一下自己目前的价值,二来了解一下市场行情。

关于生活

这一年,我有了她……


用latex排版中文书籍
Tag latex, ctex, 排版, on by view 7499

由于最近在翻译《parsing techniques》,所以想着待翻译完毕将其打印成标准书册自己研习,于是学习了如何用latex排版书籍出版物。

之前也曾用过两次latex,第一次是大学期间想着使用latex排版毕业论文,于是想用latex做一个毕业论文模板,奈何,长江大学的论文只能用Word写,还在格式上死扣,时间紧迫也只好作罢,模板残篇在这儿 https://github.com/duguying/report  。第二次使用latex是在自己的简历上,因为毕业了需要找工作,在网上找了一波简历模板,发现Modern CV这套模板很漂亮,于是也就用了,Modern CV是基于latex的模板,简历在这儿欢迎勾搭 https://github.com/duguying/resume 。经历三次使用latex(尤其是最近一次)对latex中文书籍排版也有了一定的了解。

  • latex工具选择

我选用的是texlive套装,里面包含了latex、xelatex等工具,我们编译中文书籍主要用到的是xelatex,因为xelatex原生支持UTF8,不需要任何的额外包。

  • 包选择

中文支持最常用的自然是ctex,我使用的是ctex中的ctexbook类包,他专为长篇书籍排版而生。

  • 编辑器

只要有latex语法高亮支持即可,依个人的爱好选择,我用的是sublime text作为编辑器。

  • 书籍结构

ctexbook中,一般有如下结构:chapter(章), section(节), subsection(子节), subsubsection(三级子节), paragraph(段落), subparagraph(子段落)等,用得最多的是前五种。最基础的文章结构为段落,分段常用空行的方式进行,之前我误以为 \paragraph{内容} 是分段的方式,结果发现段落全部默认粗体字了,后来才明白,\paragraph{} 应当作为“段落标题”或是成为“段落首句加粗”,其起着重作用。上一层结构就是三级子节了,如果书籍中没有那么细的分节,三级子节可以不要,三级子节默认加粗,左对齐,\subsubsection{三级子节} 被\subsubsection命令括起来的部分就是三级子节的标题了,如下图;

subsubsection.png

再上一层的结构就是子节,\subsection{子节} 子节标题默认左对齐,\subsection命令中括起来的就是子节名,带章节号,如下图

subsection.png

再上一层就是节,\section{节} 节标题默认居中对齐,\section命令括起来的就是节名,带章节号,如下图

section.png

再上一层就是章,\chapter{章} 章标题居中对齐,\chapter命令括起来的就是章名,章节号为汉字,如下图

chapter.png

  • 内容结构

内容结构可分为,前序言正文后序言 这三者。\frontmatter 命令表示接下来的内容是前序言部分,\mainmatter 命令表示接下来的内容是正文部分,\backmatter 表示接下来的部分是后序言部分。一般来说,序言部分的页码系统是与正文部分的页码系统区分的,例如前序言部分的内容,页码会以罗马数字标注(i,ii,iii,iv), 而正文部分的页码会以常规的阿拉伯数字标注(1,2,3,4),下面是示例代码展示了如何使用该三个命令定义内容区

\documentclass[hyperref,UTF8]{ctexbook}
% \documentclass{amslatex}
\usepackage{src/pt}

\begin{document}

% \pagestyle{plain}

% 前言开始
\frontmatter

\include{data/preface2}
\include{data/preface1}

% 正文开始
\mainmatter

\tableofcontents

\include{data/chap1}
\include{data/chap2}

% 尾言开始
\backmatter

\end{document}
  • 前言中定义章节

我们有时候会有多个前言,或者书籍请多个名人来作序,于是可能对每个前言/序需要区分为一章节,但是又不希望出现“第x章”这样的字样。其实在序言部分的章节默认是不带章节号的,因此,可以如同正文一般在序言中做章节处理,无须顾忌其他。如下图

preface_chapter.png

不过,如有节(section)应当用如下命令以不产生节号

\section*{关于练习和问题}

如下图

preface_section.png

若没有*号将会产生章名为0的节号。

  • 绘制表格

绘制表格常用的命令对是 \begin{tabular}\end{tabular},详细的使用可以在谷歌上查阅,示例颇多不再赘述,下面只给一段示例代码

\begin{figure}[!htbp]
\centering
\begin{tabular}{llp{5cm}ll}
    & \textbf{S}      &  &    & \textbf{S}      \\
 1b &                 &  & 1b &                 \\
    & \textbf{L\&N}   &  &    & \textbf{L\&N}   \\
 2a &                 &  & 0c &                 \\
    & \textbf{N,L\&N} &  &    & \textbf{L\&h}   \\
 0b &                 &  & 2a &                 \\
    & \textbf{d,L\&N} &  &    & \textbf{N,L\&h} \\
 2b &                 &  & 2b &                 \\
    & \textbf{d,N\&N} &  &    & \textbf{N,N\&h} \\
 0c &                 &  & 0c &                 \\
    & \textbf{d,h\&N} &  &    & \textbf{N,h\&h} \\
 0c &                 &  & 0c &                 \\
    & \textbf{d,h\&h} &  &    & \textbf{d,h\&h} \\
\end{tabular}
\caption{句子形式变为\textbf{d,h\&h}, 与最左边和最右边替换}
\label{fig:method}
\end{figure}
  • 绘制插图

这里只介绍常见的普通线条插图绘制。普通的无规律线条插图我们可以借助Adobe Illustrator绘制矢量图,然后将其另存为eps格式的矢量图。接下来用如下命令转化为pdf图(假设eps文件名fig2_19.eps)

epstopdf fig2_19.eps

如此,同目录下将会生成名为 fig2_19.pdf 的图片。然后在正文中按如下引用

\begin{figure}[!htbp]
\centering
\includegraphics[scale=0.6]{figure/fig2_19.pdf}
\caption{Production tree for a context-free grammar}
\label{fig:method}
\end{figure}

其中\caption为图片介绍,图片会自动编号。

若是规律性比较强的图片可以使用latex代码画图,其中会用到tikz等包,可自行查找研究如何使用。若是插入位图的话,引用graphicx包后可以直接插入,将上述路径中的pdf图改为相应的位图便可以轻松插入。

  • 页面结构

页面结构一般纵向会分为页眉主体页脚三部分,横向会分为左边距主体右边距测注。默认是页面布局会给正面(奇数页)的右侧预留侧注的空间,因此,看起来右边会比左边宽。

许多初接触ctexbook的人会奇怪,这个空白多的一侧应该是装订预留白吧,可是为啥会在右侧呢,其实人家仅仅只是为侧注留的空间而已。侧注是个啥?如下图左侧注

side_note.png

那么如何调整使得看不到为侧注所预留的空间呢,我们可以自定义边距即可

\usepackage[left=2.2cm,right=1.8cm,top=2.0cm,bottom=2.5cm]{geometry}
\geometry{papersize={18.00cm,23.00cm}}

上例调整左边距2.2cm,右边距1.8cm,上边距2.0cm,底边距2.5cm。


关于kill进程链
Tag 进程, 子进程, kill, on by view 2772

之前在做 Goj 项目的时候执行服务器部分使用 go 语言 kill 进程,发现无法 kill 该进程的子进程,后来发现貌似别的语言也无法直接 kill 掉当前进程的子进程链。kill 子进程链的应用场景是这样的,我在做 judger服务器的时候发现我调用 gcc 编译源码会在某些极端的情况下卡死,然后卡死后我需要 kill 掉 gcc 的进程,我使用 go 语言的相关 api kill 掉 gcc (因为我拿得到 gcc 的进程 id),然而,我发现 gcc 会调用别的命令,记忆中貌似是一个叫做 ar 的命令,实际上是这个子进程卡死了,于是我 kill 掉身为父进程的 gcc 之后,它的子进程依然存在,变成了孤儿进程,继续卡着。但是我没有 api 可以拿到 gcc 卡死的子进程的进程 id,所以我也无法 kill 掉它。我当时的临时解决方案是调用 killall 命令去按照名称杀掉它。

之后一次在某个群里提到这件事情,随口问了一句如何获取某个进程的子进程,linux api 中是没有这种 api 的,一位同学提醒了我,你可以从 /proc 目录中查。今天我用 go 语言实现了它,linux 系统中 /proc 目录记录了所有的进程信息,但是这些信息中依然没有记录某个信息的子进程,不过它记录了某个进程的父进程,于是我遍历了 /proc 目录中的所有进程的进程信息,然后根据进程的父代关系构成了一颗进程树,这颗树的顶端必定是 init 进程,于是,我便可以从这颗进程树上从上往下搜索查找出某个进程的子进程了。这样一来,一个简单的进程树便可以让我轻松的查找出某进程的子进程,同样也可以 kill 掉某颗进程树,将当前进程及其子进程 kill 掉已经不再是一件难事了。

如果你对此感兴趣,欢迎参阅源码 https://github.com/gojudge/proc 

最后,我要说明的一点是:世界上没有什么事情是一棵树解决不了的,如果有,那么两颗树一定能够解决。


Hibernate中自动建表unique无效的问题
Tag Hibernate, 建表, unique, on by view 3339

使用Hibernate自动建表,User实体中的username字段希望给个唯一约束,但是自动建表却一直没有unique约束

@Entity
@Table(name = "user")
public class User {
	private int id;
	private String username;

	@Id
	@GeneratedValue(strategy = GenerationType.IDENTITY)
	public int getId() {
		return id;
	}

	public void setId(int id) {
		this.id = id;
	}

	@Column(unique = true, nullable = false)
	public String getUsername() {
		return username;
	}

	public void setUsername(String username) {
		this.username = username;
	}

}

键表的结果是索引中并没有唯一索引,多番尝试解决如下

@Entity
@Table(name = "user")
public class User {
	private int id;
	private String username;

	@Id
	@GeneratedValue(strategy = GenerationType.IDENTITY)
	public int getId() {
		return id;
	}

	public void setId(int id) {
		this.id = id;
	}

	@Column(unique = true, length = 20, nullable = false)
	public String getUsername() {
		return username;
	}

	public void setUsername(String username) {
		this.username = username;
	}

}

原因是我未指定 username 字段的长度,然后自动建表的字段长度是255字符,于是没有唯一索引,将长度指定为 20 ,删除表,重新执行建表便有了唯一索引。另需注意的是 nullable = false 。


浅谈软件众包
Tag 众包, 思考, on by view 2515

互联网在发展,也有众多的思维与新颖的产品逐步产生。譬如国外的 Airbnb, Uber,又如国内的 滴滴打车,这些都是众包这一概念衍生出的产品。近年来,国内又有一些公司开始了尝试软件众包。软件众包的难度却往往不是 Airbnb, Uber, 滴滴打车 这些“众包”产品所能比拟的。因为软件开发的复杂程度远远超出了Airbnb的临时住宿,Uber滴滴打车的租车业务。

要想做好软件众包已经不仅仅是简单的将需求方与开发者联系到一起,有人说 Airbnb 的成功在于将房屋出租者与有住宿需求的人联系到一起,Uber和滴滴打车他们的成功在于将身为有车族的临时司机或专业司机与乘客联系到一起,而做软件众包的人若是觉得他需要做好的仅仅是将软件需求方与开发者联系到一起那么便是大错特错了,因为将软件需求方与开发者联系到一起这种简单的事情是不会产生生产力,软件需求方不懂技术,热衷于技术的众多基础开发者也是不懂得与需求方“周旋”因为他们往往不具备项目经理、产品经理的能力,而缺了这些往往无法成功的将开发者与需求发黏在一起,被开发者吐槽的项目经理产品经理往往是开发者不可缺少的,他们之间存在一种微妙的共生关系。而对于一个项目并不是简单的需求方与开发者碰撞的产物,我个人认为,普通开发者与需求发的碰撞往往只有一个结果——双方都会被对方撞成渣渣。

若是有人觉得我将软件需求方与开发者联系到一起就能够实现软件众包,那么你趁早将“软件众包平台”改名为“软件外包竞标平台”。作为一个资深的开发者(原谅我的装逼),我是没有打算转行做项目经理或是产品经理,我有的是技术,靠技术吃饭,让我做与需求方“周旋”这种事情,那是浪费我写代码研究技术的时间,我不会跟需求方讨价还价也不希望需求方跟我讨价还价,多少钱买多少代码多少功能,只要我觉得合适我就干,少一分也不干。所以,众包平台若是简单的将需求方与开发者联系到一起,绝对会发生碰撞,而且一定会撞得头破血流。

假如我是一个开发者:我心目中的众包一定要有分得足够细致的需求,而且还是明码标价的,完成某个ISSUE给多少钱,而我只需要接收那个我觉得出价合适的ISSUE(或称为细分的需求),在指定的时间内完成便可以拿到上面所标注的价格。接收需求后我所需要做的只是将所有的精力放在我所钟爱的编码上即可。

假如我是一个测试人员:我心目中的众包应该是有一个个的功能模块和一个个的需求供我选择测试,明码标价。按照ISSUE分的测试任务,按照模块分的模块测试任务,甚至是整项目业务逻辑回归测试,安全漏洞悬赏测试。不同粒度的测试任务价格也是不同的,当然,因为我是资深的测试人员,你也可以雇佣我为当前项目的测试顾问,负责维护整个项目的整体可靠性保障。

假如我是一个项目经理……

我不想写那么多假如了,上面的所有假如必须有众多的幕后工作才可能实现,否则一切皆空。

项目经理、首席技术总监,者两种角色便是上面假设成真的保障,谁来跟需求方确立最终的需求,谁来跟需求方“周旋”,这是项目经理所应当做的事情。谁来讲上面假设变为现实,需要将需求拆分为如此细致的开发任务,并且保障整体的有效性与可用性,这些便是技术负责人该项目的首席技术总监所要做的事情。

我不知道我为什么要写这篇文章,或许是在众包项目组待了这么长时间,也或许是别的原因。纵观国内的软件众包平台,根本没有能够达到这个要求的,我也不愿意一一点评。倒是我之前在某在线视频教育平台找的兼职讲师有点这种感觉,他们让我签什么狗屁协议,上面写的如果违约对方有权利罚款5w,我特么当然不签,最终还是他们妥协了,事实如我所料,我并没有太多时间去录制他们要求的“高质量”的视频,干了三课就休业了,这种想干就干,想不干就不干的感觉才是自由职业者所追求的自由。或许有人说,你这么不负责,撒手不干了,岂不是打乱了别人的课程计划?你想多了,从来都不缺会录课会教学的人,即便没人接我的班,我所录的课程也会一直为他们产生价值创造财富,他们甚至会将我所录的三节课编入别的分类体系中。

不管是举例还是装逼,假如我决定做自由职业者的话,这种自由都没有,我特么还是去别人公司老老实实的上班去吧。


Git团队开发中PR工作模式的反思
Tag git, pr, 工作流, on by view 3204

在公司团队协作用了大半年的Git了,PR工作模式也用了半年。由一个用Git装逼的假老手成长为真正用Git进行团队协作开发的老油条。这其中也有一些值得反思的东西。我很崇拜PR的工作模式,因为它基本上是优秀的开源软件工作模式的代表,众多的开发者自发的给开源项目发送PR,以前总是盼望着自己的开源项目会有人发送PR,若是接收到PR就像是收到别人的礼物一般高兴。

我之前赞同的PR工作模式是这样的,开发者拿到开发需求之后,在自己的分支开发,然后向主仓库的相关分支发送PR,之后由测试人员在测试机上拉取主仓库相关分支的代码,然后fetch PR所在仓库分支的代码合并(merge)到本地主仓库相关分支,进行测试,测试完毕通过之后才合并PR到分支。我这一想法的灵感来源于 travis-ci 单元测试的工作模式:开发人员发送PR,travis-ci自动进行单元测试,PR管理者参考PR在 travis-ci 上单元测试的结果进行初步判定是否可以合并。但是我忽略了一点,测试人员并非是 travis-ci 上的单元测试。在工作的过程中往往是这样的:

开发人员初步开发出某个功能,然后经过自己的初步测试,发送PR到主仓库,测试人员看到有PR了,需要测试了,于是开始测试,但是测试有问题,于是开发人员就必须继续修改提交,然后再次测试,还有一些问题被发现依然存在,继续开发……可是,你突然发现,你的PR被合并了或者是被关闭了,因为要下班了;或者是要下班了,PR管理员会问这个PR能否合并,测试人员暂时没发现新的问题,于是测试人员告诉PR管理员“没有问题”,于是PR被合并了。或者没有上面的“PR管理员的询问”,但不管怎样,最后的结果往往是PR被草草的合并了。于是,没有充分测试的PR进入了仓库,甚至进入了发布分支,最终的结果可想而知。

在这个过程中,人们往往没有意识到其实是被PR给影响到了。因为开发者一个PR创立之后,在他会有一个潜意识希望PR被合并,测试人员同样也有这么一个潜意识,因为测试PR就是他的工作之一,他也希望PR被合并,对于PR管理员来说同样也有这么一个潜意识,希望PR被处理,不管是合并还是关闭。就是这些潜意识推动这个PR尽早被合并。

我个人感觉正确的做法应该是当测试人员测试充分之后再发起PR,避免这些潜意识促使PR过早的被合并。事实上,不管PR是否存在,测试人员都能够从开发者的分支上拉取到开发者的代码合并到主仓库,PR不过是一个形式而已,给合并者带来方便。但是那些错误的理解了PR的人们往往会搞出诸多莫名其妙的东西,比如PR用来人工做代码审核,比如搞个CheckList让人工去查代码中哪儿写得不规范,这些应该是让 Sonar 自动扫描PR中存在的不规范,谁又会有时间去为别人的PR人工做Sonar该做的事情。经过测试人员的充分测试之后,开发者创建PR,这时单元测试测试一遍,然后Sonar扫描一遍,后两者自动化测试通过之后代码才能入库。

总之,测试人员功能测试通过之后开发者才能创建PR,自动化工具进行单元测试和Sonar扫描通过后,PR管理员才能允许合并。这才是比较靠谱的PR工作流。


结构化数据缓存管理设计
Tag 数据库, 缓存, 管理, on by view 3332

现代web开发中,在数据库和应用程序服务器之间使用缓存是常见的手段,它有效的减轻了后端数据库服务器的压力。结构化数据库中的数据是按照表、字段这种结构存储的,而作为缓存系统的memcached或是redis-server是以key-value的方式存储数据的。如何将结构化的数据存储到非结构化的缓存系统是一个问题,对于MySQL等结构化数据库我们可以按照以下方式存储。

首先,对于表中单条数据我们可以按照Bean存储在缓存中,key为“表名+id”,value为Bean对象。

然后,对于列表查询,我们可以使用“select id from article where status=1 limit 10”这种语句查出article表的id列表 List<Long> 。然后通过id从缓存中逐条加载 Bean ,最终形成我们需要的 List<Bean>。而我们可以将通过sql查出来的List<Long>结构的id数组作为一个缓存对象,其key可以按照某种规则生成,例如“方法名+override_index+参数...”。

上面说的两种数据对象的存储方式都没有问题,而其中的难点在于如何管理缓存,我们知道,一个列表查询的结果List<Long>在这张表发生增删改的时候都会发生变化,因此,详细来说我们应该在列表的某个字段发生增删改的时候清理掉这个id列表缓存,等下次查询的时候再自动重建新的缓存,以保证缓存的正确性。不过,若是联表查询呢?联表查询“select user.id from article, user where article.userid=user.id and article.status=1”查出的是一个id列表,我们将其缓存假设key为key1,但是一旦article表和user表其中之一发生增删改,那么这个缓存数据应当失效,也就是说,Article在进行 Update()、Save()、Delete() 的时候我们应该清理掉这个key为key1的缓存,同样对于User在进行 Update()、Save()、Delete() 的时候我们也应当清理掉 key1 的缓存。在这儿只是列举了两个表联查,并且只是提到了一个联表查询,假如有N个表联表,而且有M个联表查询语句那么我们应该在多少个方法中调用清理这种缓存的方法呢M*N*3,好多好麻烦呀。

为了管理list的缓存,定义一个树形结构如下

cache_manage_struct.png

t开头的节点表示表名,tA、tB 表示 A 表和 B 表,f开头的表示字段,fAa 表示 A 表 a 字段,fAb 表示 A 表 b 字段,k开头的表示列表的缓存键。这个数据结构表达了我们应该如何清理缓存,比如A表中的a字段发生的改变(增删改),那么我应该清理掉键名为k1, k2, k3的缓存,A表字段b发生增删改时应该清理掉键名为k1, k2的缓存,B表a字段发生增删改时应该清理掉键名为 k1 的缓存。然后将该树形结构拆分为三级,分别用三类的key-value存储起来就好,也不必担心结构过于庞大,上图可以拆为6个key-value:

[0]    _init->(tA, tB)
[1]    tA->(fAa, fAb)
[1]    tB->(fBa, fBb)
[2]    fAa->(k1, k2, k3)
[2]    fAb->(k1, k2)
[2]    fBa->(k1)

上图的结构来自于如下三个SQL查询

[K1]   select id from A, B  where  A.fAa = ? and A.fAb = ? and B.fBa = ? ...
[K2]   select id from A     where  A.fAa = ? and A.fAb = ? ...
[K3]   select id from A     where  A.fAa = ? ...