技术杂谈-Shell是个好东西

发布: 2022-09-05   作者: Youcai   分类: Thinking 标签: shell  可转载但要注明出处!

原创,手机阅读体验不错!大屏电脑放大到125%阅读效果更佳!

写在前面

Shell语言还是挺强大的,它不像其它语言,要求你全面去学习,你完全可以零零散散,随意的学几条,那就怕是一条,活用起来,都能感受到它的强大,特别是后端开发技术,如果掌握适当的Shell命令,对开发、测试、上线,甚至能达到起死回生的作用。记得某周会,我就这样跟团队小伙伴说,他们还不完全相信,但后面出事故,我用Shell来处理手尾,他们才真正感受到什么叫强大。后来为了让团队全部学会Shell,做了整体分享,然后,每新招一名技术进来,我都会亲自培训他们三节课,其中有一节就是Shell语言程序开发,也就花上1小时,但后面的回报却是完全超出我想象,物有所值,Shell是个好东西。


码农流水帐

2020年9月底,秋高气爽,风轻轻的在窗边掠过,成群的大雁正向南飞,一会排成人字,一会排成一字。想那河边的芦苇也黄了,舌尖上的螃蟹也应该结满了糕黄,家乡的人民可能也正庆祝着水稻丰收的喜悦,确实是团建聚大餐的好日子。最让人恨的,团队还刚刚拿了一笔丰厚的奖金,总统套餐肯定少不了。上班后,早早就跟大伙说了,最近这么辛苦,赶项目赶进度,今晚去顺德佬大饭店,杀一餐爽的,好好放松,大伙欢呼雀跃,都加快了手上的速度,就为今晚的举杯。

说回那天,虽然没有重要发版,但也有一个直播间排行榜要对外,不过都预演了一天,现在是万事具备,只欠东风,就等时间到,功能自动开放就好,风险很小,快下班前的1个小时,也跟开发花神讨论了一会,确保没有问题,提前五分钟下楼Book位了。

不一会,大伙陆陆继继的下来,到顺德佬大酒店包间,气氛好轻松,强哥慢慢拿出香烟抽了起来,边抽边慢吞吞的说:”咦,这女孩子太漂亮了,真不知要不要追。“,我走过来说:”强哥拍拖啦?发型都没有变,没有看出来啊?什么情况啊?说来听听。“,大伙听到,全起了精神,个个靠近来,对强哥说:”快说,肥水不流外人田,是不是鲜总?还是花神?我好早点跟大家宣布,不要跟你争。“,强哥笑着说:”那里,那里,鲜总,花神这么优秀,那里看得上我。我最近去健身,喜欢上一位女孩子,发现就在旁边公司做前台。“,九爷听后,大笑说:“强哥天天健身,腰好,脚好,身体好,肾更好啊,牛啊!”,这时候鲜总走了进来轻快的说:”我支持你追,女孩子喜欢口红,送一支吧,可以来我微店上买,打八折。“,强哥笑得更开心说:”那一定,不帮你买,帮谁买?要不?你有男朋友不?如果...,我帮你买十支“,鲜总听后,说:“滚!”,哈哈哈,大伙笑了起来,突然,一向少语的司令,冷不惊人地说了一句:”我支持你追,程序员工资高,养得起!“,强哥笑着说:”司令你工资是不是很高,发个红包出来贺一贺,哈哈!“,雨神与团长,接着分析了娶个前台的好处,说,“你想下,如果有了小孩子,程序猿天天加班,那有时间带,前台文员好啊,有时间,给你慢慢带慢慢教,享受小资般的生活,打扮的漂漂亮亮,有面子,气质一下子就上来了!”,花神一个人在那里按着嘴笑,没有发表任何意见。就这样,你一言,我一句,一直说到菜上桌,给足了信心强哥,人到兴奋处,恨不得马上跑出去,把那女生抱过来一起吃饭,就不知,他最后有没有泡到手。

接着,我们开始聊了一会股票,雨神说他最近买入阿里股票赚了一个星期的饭钱,司令说他A股中了一签,今天赚了5000元,大家起哄说,今晚让司令买单算了,花神终于开口,说她最近不敢动中概股,全仓买了AWS与GOOGLE,好像也小赚了,我说我就惨了,买空京东,亏损了10%,哭都哭不出来,不过总体上看,当年股票还是赚钱的,大家心里也踏实,聊得很HI,几个不会抄美股的同学,问这问那,非常仔细,相信某次招商银行服务日都去开了香港帐户。

再过一会,我们点的大餐就上桌了,第一个上来的是顺德大盘菜,这个菜盘真够大,直径足足有半米宽,分量足,色泽好,香味浓,有:新鲜盐焗花螺,大扇贝托粉丝,半开蒜蓉大海虾,阳江豆豉脆鱼肚,生摘花椒黑鱼片,潮汕濑水牛肉丸,还有麦香芋头糕与干煎番薯片,盘底还有芝麻陈村粉,口水都流了一地,大快人心。二位美女优先动手,基本不够10分钟,整大盘基本一扫光,就剩下一个扇贝托粉丝,人才啊,二十一世纪的人才啊,吃得真快,这就是Stay Hungry Stay Foolish中的Hungry。

不过,菜还在嘴里咀着,雨神跟强哥就因为技术问题又干了上来,强哥直接了当地说雨神昨天写的一个开发模式代码不够好,太苦涩太难懂。雨神当然不服,Bili,Bili的说了一大段,左引右证,说阿里啊,唯品会啊,欢聚时代啊都这样搞,我们格局要打开。还是谁也没说服谁,不过每次技术上的深度讨论,往往又加深了我们的相互了解与友谊。

像这种情况,我得补上二句,说技术这东西,没有绝对的好与坏之分,只有合适与不合适之分,然后举个例子。话说当年,同专业好友在平安银行工作,星期天他还在家里狂补Hibernate,他跟我说,数据库操作全用这个类库,改起来,一堆你想不到的问题。我跟他讨论了一会,说,这东西我也深入学习过,那些什么lazy初始化,一对一,多对多依赖fitch、update、cache,真是无比复杂。我说我很反感这样做,我们应该把有限的精力放到业务本身,而不是耗在这里,为什么不直接使用SQL写代码,用更简单的方式来实现呢,基本没有学习成本。

后面再深入讨论,知道他们老大是清华大学毕业的,才二年,就做了架构师。所以问题就出在这里,清华大学的学生是什么水平,然后他手下那帮给他写代码的人又是什么水平,对清华学生来说,hibernate实在是太简单了,但维护那帮人可能花了60%的精力来维护hibernate本身,而40%的精力放在业务上,所以说,效率能提升吗?

还有毕业二年的架构师可能也缺少那种软件能跑十年的眼光,我就见非常多互联网项目,生命周期都很短,就1年2年这样,快餐式开发,也没有错,存在即合理麻,但可维护性方面的问题,肯定少考虑,相信后面得再折腾。

如我前二家公司,管理后台都运行与维护超过十年以上,所以这次负责海外管理后台时,可维护性问题是有考虑过的,估计管理后台至少能跑五年吧,所以在框架上做了如下决定。高性能的主站对外项目全部使用SpringJdbc来操作Msyql,但管理后台全部使用JPA。为什么这样搞?直接使用SpringJdbc来操作,虽然简单但要写好多SQL,开发效率低点,但使用Hibernate的话又太重,所以使用了JPA,即有JDBC的轻,又没有hibernate的重,最主要是能自动生成SQL(运营后台真的好多好多SQL语句),这就是适合的框架选择,我相信,如果项目真能顶够五年,后面的人会感谢我。

到这里,适当的地点,适当的时间,大伙又响起了掌声,特别是强哥,特别激动,大声说:“好,老大说得好,简单就是美,Get到了”。刚说完,后面的菜就要上来了,有广东姜葱白切鸡,卤水鸡金拼烧鹅,红烧铁板猪大肠,还有清蒸黄花鱼夹榨菜与两碟炒时令青菜,旁边的备桌端来了虾米蠔士粥和鹰麦牛奶小馒头。“放不下了,放不下了,太丰富了,清掉这个大盘吧”,雨神机灵的一说。但上面还有一个扇贝托粉丝,刚才雨神与强哥还争着要吃这个,这时候,他们二个相互礼让,都说你先来。最后九哥说了句,来盘石头剪头布吧,然后快乐的进行,强哥吃下了这块心头肉。

正要开吃的时候,花神的电话响了起来,电话那边响起了产品焦急的声音,花神,花神,在哪里,我们想清掉一些榜单数据,非常急,能马上回来吗?我问花神吃饱没有,她说差不多了,我说打包个馒头回去处理吧。然后花神简单收拾下,就急冲冲往公司赶去。不过我也不担心,当时花神跟了我快三年,成长还挺快的,现在写代码已经很熟悉很小心,应该不会搞出大头佛,我们继续聊天。

聊着聊着,说到了程序员的未来,九爷问我有没有职业危机,或者以后不做程序员了,做些什么。我说也还没有想过这个问题呢,感觉45岁前还挺能扛吧,如果45岁前公司还有我的位置,还能发光发热,就一直做咯,等有了足够的钱,就环游全世界,没有的话,那就继续奋斗到50岁,然后退休,陪下老人小孩子也挺不错的,你呢,怎么这么年轻就想这么老成的问题了。九爷表达了他挺喜欢编程的,但就是太累了,家里不愿意他这样搞,希望他能再做一二年后,回家考公务员过上朝九晚五的生活,他也不知道后面的路怎样。这时候雨神借着一杯冰浸奶皮,使起劲说,“考公务员没有出色,钱少,关系复杂,别啦,要立大志向,要向腾讯这些大厂靠,做一年顶你做十年!年轻人要有点志向!那些小资生活不适合我们!”,后面大家都说了些自己的想法,不过有种感觉,他们也带着一丝丝的不确定性顾虑。那先不谈这个话题吧,我把问题扯回到团长的婚嫁问题,什么时候结婚办喜酒啊,什么时候要小孩啊,大家情绪一下又起来,觥筹交错。

谈得正兴的时候,花神打电话过来说,“老大,我把Redis数据误删了,现在对外的榜单没有数据了,我要写Java程序发版一下,最快40分钟左右,最迟1.5个小时,你知会一下。”,我说处理时间还是有点久,还有没有其它方案啊,小妹说了一通,有点紧张,其中有提到一点,Redis的数据全部按我要求落盘到Mysql,那就是说,如果逻辑不是超复杂的话,使用Shell脚本处理可能更快。我跟她说,不要急燥,小心操作,你先按你方案搞,我现在马上回去跟你救火。

火急赶回公司,理清了花神的数据结构,共花了20分全部写好Shell脚本并上线,见下面说解,也让他们感受到Shell脚本的强大。然后监控半小时,其它未走的同学,也一起,顺便讨论了一下快速数据恢复方案,这是后话了。

晚上11点从公司出来,园区还是灯火通明,但路人很少,冷不丁有几个相互说再见的甲乙丙丁,一片祥和。皎洁的月亮洒满了大地,又想起了润土晚上捉猹的场景,回到家,看了半小时书,不久就进入了梦乡,准备迎接,看似一样,但实际上又不一样的明天。


Shell救火方案

在讲解救火方案前,先理一下需求是什么,然后技术上是怎样实现的,把这个流程讲清楚了,对后面理解救火流程就是小菜一碟了。为了问题更简洁,更清晰,更有代表性,对原问题进行了简化,如多国家变成一个国家,多类榜单变成一类榜单,就叫爽口mini版吧。

先了解一下数据表结构。字段有:用户(userId)、主播(anchorId)、活动(activityId)、美元(points)、国家(country)、时间(createTime)。那么一条真实的打赏记录像:用户于19号在某场活动中给印尼主播打赏了N元。则对应的数据记录应该像下面这样:

id  activityId  userId  anchorId    points  country createTime
307    20001     600      145        100     IN     1600525411
308    20001     840      145        99      IN     1600525412
309    20001     120      339        56      IN     1600525413
310    20001     570      817        80      IN     1600525414

正常的情况下,打赏行为用一张表存储数据即可,如叫:user_anchor_point(用户打赏主播表),但考虑到数据量非常大,这里采用分表存储技术,就先分十张表吧,如使用userId求余(userId % 10) 。那么用户Id是862,则在user_anchor_point_02这张表(862 % 10 = 2)。

user_anchor_point_00
user_anchor_point_01
user_anchor_point_02
user_anchor_point_03
user_anchor_point_04
user_anchor_point_05
user_anchor_point_06
user_anchor_point_07
user_anchor_point_08
user_anchor_point_09

技术点大概是这样,现在有一个新需求,做一个页面,可以查看某个活动最新打赏TOP100用户,其中列表打赏项,要显示三个数据,打赏用户、被打赏主播与时间,如下图。

需求

怎么把这个需求转换成技术实现呢?首先,榜单的请求量比较大,不能使用mysql直接查询,马上能想到的就是使用缓存,那就使用redis吧,然后就是使用那种数据结构,经过一番分析,我猜你已经想到,使用Redis的Zset结构(答案不唯一哈),确定key、member、score即可。

key = rank + : + 国家 + : + 活动 //如上面的第一条数据,key应该是:rank:IN:20001
member = 用户 + : + 主播 //同一用户对主播多次打赏,只显示最后一条
score = 时间 //使用score对结果排序,方便TOP 100

确定数据结构后,对上面每发生的一笔打赏交易,就往Redis里插入一条数据,要显示时,就使用zrange命令读出数据再解析展示即可。我们关心的是插入数据部分,转换成redis命令是:

zadd "rank:IN:20001" 1600525411 "600:145"
zadd "rank:IN:20001" 1600525412 "840:145"
zadd "rank:IN:20001" 1600525413 "120:339"
zadd "rank:IN:20001" 1600525414 "570:817"

开始救火

那现在问题是什么?现在的问题就是,花神把缓存redis的数据删除了,对外显示没有排行榜。那就是怎样恢复redis数据的问题,通常的做法,就是写一份Java代码,完全测试好,再发布,是行的,但效率不会很高,而且你调试的时候也不方便,出个错,你得发版多次,可能还涉及到测试等上线流程。那就看一下使用Shell脚本是怎样搞的吧。

第一步:连接数据库函数定义。就这么一个函数,是不是比写Java要简单好多啊,-N是去掉表头,因为这里只关心数据。

#连接mysql数据库的函数定义
function exeSql(){
    mysql -h127.0.0.1 -Drank -urank -p2022 -N
}

第二步:导出所有数据。要把10张表的数据全部导出来,实际情况你可能还要加上where语句(如一个月的数据)。我喜欢把结果保存到文件,可以多次利用,存取文件不会再影响Mysql服务器。下面程序结果会输出到user_anchor_point.txt文件。

#定义导出所有数据的函数定义
function loadTable(){
  > user_anchor_point.txt
  echo "show tables like '%user_anchor_point%'" | exeSql | while read tableName;do
     echo "select * from $tableName"  | exeSql >> user_anchor_point.txt
  done;
}
#调用函数
loadTable

第三步:生成相应的Redis命令。直接从user_anchor_point.txt文件读取数据,假设是处理印度国家的数据,再加入了印度判断,最后就是字redis命令拼接。

#产生相应的redis命令的函数定义
function generateScript(){
  cat user_anchor_point.txt | awk -F"\t" '{ if($6 == "IN"){
    printf("ZADD rank:IN:%s %s %s:%s\n",$2,$7,$3,$4);  
    }  
  }'
}

如果现在就运行generateScript函数,你将得到下面的结果,检查输出没有问题后,再把命令放到redis-cli终端执行就解决问题了。

$ generateScript
ZADD rank:IN:20001 1600525411 600:145
ZADD rank:IN:20001 1600525412 840:145
ZADD rank:IN:20001 1600525413 120:339
ZADD rank:IN:20001 1600525414 570:817

到此,完整的救火流程就完成,用Shell处理数据是不是很简单啊?实际上,这次救火,共有10个国家的榜单要处理,上面的if语句改下就好。共有五个类型的榜单要处理,如救火这个是国家用户最新打赏榜,还有国家土豪榜,国家主播收礼榜等。就我经验来看,像这种数据处理类,Java救火,效率还是跟不上了,所以说Shell是个好东西。


常用Shell命令

本文算是杂谈,先让你感受下Shell的强大,希望能在后面的开发过程中,发挥一定的作用。但想完整的掌握这门语言,建议看下书或找些专项资料,可能收获更大。这里就先顺便放些常用的命令或代码片段,当是抛砖引玉,或提高你学习的兴趣。

1、最简单的命令。如 tail head cat sort uniq awk grep vi |,不要轻视这几条命令,确实是最常用,可能80%的程序员到退休接触的还是这几条命令,换名话来说,足够我们玩一辈子了。

就以我服务器的nginx日志文件为例吧,用tail可以实时跟踪最新的日志,特别是那种大流量日志系统,更爽,日志像时光机一样流啊流,外行人一看,嘿,像电影里的黑客,是个高手。相信90%的老成程序员已经学会使用:

$ tail -f access.log
223.104.67.162 - - [06/Sep/2022:09:52:13 +0800] "GET /search-by-es.html HTTP/1.1" 304 0 "http://goodrain.cc/data-lake.html?s=maimai" "Mozilla/5.0 (iPhone; CPU iPhone OS 14_5_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Mobile/15E148/{iPhone13,2} [iOS 14.5.1]/MaiMai 6.2.22(6.2.22.2)"

上面打印了一条访问记录,但记录的内容太多了,我可能只关心最近访问的100条记录,仅包括用户IP与访问URL就行。那么可以用awk命令来完成这个功能,其中$1表示第一列,$7表示第7列(列与列默认是空格分隔),awk好强大的,建议真读下文档。

$ tail -100 access.log | awk '{print $1,$7}' | grep html
207.46.13.85 /category/bigproject.html
111.7.100.24 /category/aboutme.html
111.7.100.23 /tag/java.html
111.7.100.23 /tag/flink.html
111.7.100.23 /tag/elasticsearch.html
111.7.100.27 /tag/springboot.html
111.7.100.27 /camel-framework-cn.html
111.7.100.22 /tag/java.html
36.99.136.140 /data-lake.html
111.7.100.22 /tag/java.html
111.7.100.23 /tag/flink.html
36.99.136.142 /category/bigproject.html

上面的URl与IP打印出来了,但好像除了看,也没有解决什么问题啊?我现在想知道那个HTMl的页面访问最多,嘿,想法不错,我们实现吧,从结果发现,朋友圈的力量还是挺大的,所以有好文章得多发朋友圈(这里的参数s,懂行的朋友不要乱造):

$ cat access.log | awk '{print $7}' | grep html | sort | uniq -c | sort -nk1 | tail -20
 32 /camel-framework.html
 44 /search-user-by-elasticsearch.html
 62 /tag/architecture.html
 66 /tag/java.html
 67 /tag/es.html
 69 /tag/springboot.html
 70 /tag/elasticsearch.html
153 /data-lake.html
159 /category/uaboutme.html
167 /search-by-es.html?s=weixin
176 /data-lake.html?s=weixin
193 /camel-framework-cn.html
219 /category/treading.html
256 /data-lake.html?s=pengyouquan
256 /search-user-by-es.html
312 /category/zaboutme.html
312 /search-by-es.html?s=pengyouquan
391 /search-by-es.html
939 /category/aframework.html
1182 /category/bigproject.html

2、基本数据处理命令。如 while awk mysql redis-cli > >>等,使用它能处理非常多的文本数据,捣搞数据可是得心应手,爽。下面以一个真实的项目实践来说明一下吧。有一个语音房的功能,这里理解成一个表吧,这个表有三个字段吧,如:

表名:family_chat_room
字段:roomId(房间id), ownerUerId(创建者id), desc(描述)

如有二条数据大概是这样子:

roomId    ownerUerId    desc
 120        600          阿龙风趣幽默语音房
 121        840          阿凤有唱有笑语音房

刚开始,语音房的roomId与ownerUerId是一对一的关系,而且一个用户只能创建一个语音房,并约定了ownerUserId才是有意义的字段,所以下游很多系统是以ownerUserId作为关键字(先不管为什么刚开始不用roomId)。 如某个排行榜的redis的key就是以这个ownerUerId来组成,如 r:family: + ownerUerId + 其它。但事情发展得太快,不久,产品就允许一个用户可以创建多个语音房,而且房主可以转让。那么我们排行榜的key就得变化了,变成 r:family: + roomId + 其它,这就涉及到数据迁移与升级。这个需求给组内同学做的时候,说要3-5天,涉及到好多代码与数据要处理或升级,他也跟产品谈好了,4天后上线。我同意上线时间,但也以身作则写程序给小伙看吧,前前后后,就花了1小时,又是一个见证shell强大的好例子,我就不详细说明了,如果你能看得明白,证明你的shell之旅已经上路了,祝好运。

#连接mysql数据库
function exeSql(){
    mysql -h127.0.0.1 -uroot -p2022 -Droom -N
}

#连接redis数据库
function exeRedis(){
    redis-cli -h 127.0.0.1
}

#导出redis里的旧key
function loadFamilyRedisData(){
    echo "keys r:family:v1:*" | exeRedis
}

#把key保存成文件
loadFamilyRedisData > allFamilyRedisData.txt


#旧key转换成新key
function parseNewKey(){
    cat allFamilyRedisData.txt | awk -F":" '{print $3,$4}' | while read oldKey other;do
        newKey=`echo "select roomId from family_chat_room where ownerUerId = '$oldKey'" | exeSql`      
        if [[ "$newKey" != "" ]];then
            echo "r:family:v1:$oldKey:$other r:family:v2:$newKey:$other"  
        fi
    done;
}

parseNewKey > allKeys.txt


#执行更新操作
function main(){
    cat allKeys.txt | awk -F" " '{printf "RENAME %s %s\n", $1,$2}' | while read command;do
        echo "$command" | exeRedis
    done;
}

#执行
main

3、基本系统类命令。如 ps netstat iostat top htop chmod。这部分内部自己上网查,具体不再展开。


总结

第一次接触shell命令是计算机网络这门课,反而不是操作系统,当时在学校国防实验室,听老师吹他搞的一个牛B的网络流量监控系统,演示时用到了tail -f命令,感觉挺好玩的,顺便还学了cat与grep,感觉已经很高大上了。但毕业第一间公司,用的是Microsoft Server这一套,没有机会玩Shell。不过第二间公司开始,线上环境全是Linux环境,所以当时全面的学了Shell基础,那本书好像叫什么鸟叔的什么编程及另一本英文写的。不过引我全面使用的还是珍爱网的大佬Only You,他Shell玩得很溜,特别是数据处理方面,出神入化的地步,学习他写的脚本,进步很大,令人佩服。从那后,项目开发已经离不开Shell,特别是数据,救火,测试等,起到非常关键的作用。一句话总结:Shell是个好东西。