如何摆平我的41万个用户
业务背景:众所周知,淘宝网作为一个开放平台为广大用户开辟了一块开放的空间,而图片作为一种非常有表现力的一种表现形式被广泛应用于淘宝的广大用户中。为了管理庞大的图片数据(20亿图片,有5亿被逻辑删除,15亿可用),我们图片空间(http://tu.taobao.com/)应运而生。在淘宝tfs中保存的图片我们自然会为广大用户提供优质的保障服务,哪怕使用删除功能也只是执行逻辑删除。然而,在这20亿意外还有很多外链图片,这些图片的维护可能就没有我们tfs做得那么细致而优质了。于是,很多用户在使用淘宝业务的时候会时常发现网页上有很多“X”,因为那些被引用的外链图片已经不能被引用了。虽然在图片添加的时候我们已经会主动剔除那些非法连接(即img标签中src不能匹配为“http://xxx.xxx.xxx/xxx.jpg”等链接),但是仍然有很多不可用的外链充斥在各网页中。为了解决这样一个问题,小弟我最近负责的一个任务就是去主动查出不可用图片并告知图片使用者。
业务流程:小二会获取一份用户id列表文件,我手中的那份列表有41万个用户id,通过上传此用户id文件,我们的图片爬虫程序会去找到每个id对应的前十个宝贝,再找到前十个宝贝中的十个外链,逐个检查外链是否可以访问,如果不能访问则向数据库中添加一项纪录,等扫描结束以后,我们会定期根据扫描结果去通知“坏外链”拥有者修改此外链。
故事从这里开始了…..
作为一个在淘宝实习的小二本来对这项任务不是很感冒的我,居然做着做着做出感情来了。由于刚刚进入一家公司工作,而自己的学业还有一年完成,所以工作进度很慢,如此简单的一个任务已经做了两周多了,到发表这篇文章的时候是第三周的周三了。而明天只是发布外链检查功能,所以小弟我甚为惭愧。
这个任务是从一个比较忙的师兄手上接下来的,师兄已经完成了一个版本,但是完成的版本对业务背景理解不是很透彻结果无法应对如此大的数据量(虽然只有41万,但是我们也只有一台服务器在处理这项工作)。其实,我要做的事情很简单“单线程→多线程,防止内存溢出”。于是,我的做法如下(想法很符合我这样的工作经验):
(假定允许线程最大值50个)
把41万数据平均分配若干份,分配到足够多的线程(50个)中进行处理,处理效率50条/单位时间,内存中的数据为50*10*10条+4.1M*2(数据切割的时候在内存中会出现2分用户ID),这里是以用户ID为标志量来分配任务的。
然而,就在我的代码在预发环境上运行了一段时间以后,老大找到了我——代码重构,因为代码实在是不怎么好看,线程都是通过new Thread(){}.start来开出来的,线程的管理比较混乱,有些做法甚至不安全。
老大的做法是:遍历41万用户,给一个阻塞队列,队列大小最大线程数(50)*10也就是500,遍历用户的过程和我的方案没什么区别,写个循环就跑呗,但是标志量不同,老大的方案用的是图片链接,每检测到一个外链就扔进队列进行处理,内存中的数据为500条+4.1M,减少内存占用4500条+4.1M,而他的线程管理是交给jvm做的,用到了线程池来自动分配。但是,这个方案还是有问题的,我和老大两人花了2天才解决那个问题。为了减少数据的冗余,我们需要进行去重,首先,如果链接重复就直接不检查后面送入的重复链接,然后,如果是有个宝贝中出现了“坏外链”,那么这个宝贝的其他链接就不在检测。既然是通过外链这个标志量来进行任务分配的,那么根据宝贝ID进行的去重工作自然可能出问题(不同的线程操作的不同外链来自同一个宝贝就会发生冲突),于是,我们需要在根据宝贝ID去重用的那个set上面加一把锁。
不难看出,虽然我们2个方案的处理效率在理论上是一致的,但是老大的方案减少了内存的使用,并且不用自己去管理线程,当需要紧急终止的时候,老大的方案中也有很多亮点,至于还有一些在编码上的技巧我就不赘述了,因为我还没弄明白,因此,我通过这次的任务还是学到不少东西的。(话说去重的工艺居然还是这次任务才学到的Orz惭愧啊~)可惜,由于涉及到公司的业务,我不能共享代码,希望各位看官多多包涵。