-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathcontent.json
More file actions
1 lines (1 loc) · 326 KB
/
Copy pathcontent.json
File metadata and controls
1 lines (1 loc) · 326 KB
1
{"meta":{"title":"张小超的博客","subtitle":"超超不会飞","description":null,"author":"张小超","url":"https://www.chaohang.top"},"pages":[{"title":"","date":"2018-11-03T03:12:33.780Z","updated":"2018-11-03T03:12:33.780Z","comments":true,"path":"404.html","permalink":"https://www.chaohang.top/404.html","excerpt":"","text":""},{"title":"","date":"2020-11-06T02:27:30.465Z","updated":"2020-11-06T02:27:30.465Z","comments":true,"path":"about/index.html","permalink":"https://www.chaohang.top/about/index.html","excerpt":"","text":"我的网站主页:博客园CSDN掘金InfoQ 我的公众号【超超不会飞】,欢迎大家扫码关注。"},{"title":"tags","date":"2018-11-03T03:04:47.000Z","updated":"2018-11-03T03:10:30.620Z","comments":true,"path":"tags/index.html","permalink":"https://www.chaohang.top/tags/index.html","excerpt":"","text":""}],"posts":[{"title":"为什么 redo log 具有 crash-safe 的能力,是 binlog 无法替代的?","slug":"为什么redo log具有crash-safe的能力","date":"2020-11-18T01:49:20.000Z","updated":"2020-11-18T01:46:04.000Z","comments":true,"path":"2020/11/18/为什么redo log具有crash-safe的能力/","link":"","permalink":"https://www.chaohang.top/2020/11/18/%E4%B8%BA%E4%BB%80%E4%B9%88redo%20log%E5%85%B7%E6%9C%89crash-safe%E7%9A%84%E8%83%BD%E5%8A%9B/","excerpt":"昨天在复习 MySQL 日志相关的知识,学的东西过一段时间后就会遗忘,遗忘后再重新思考,往往会有新的收获。想到几个问题,把它记录下来。 为什么 redo log 具有 crash-safe 的能力,而 binlog 没有?redo log 是什么? 一个固定大小,“循环写”的日志文件,记录的是物理日志——“在某个数据页上做了某个修改”。 binlog 是什么? 一个无限大小,“追加写”的日志文件,记录的是逻辑日志——“给 ID=2 这一行的 c 字段加1”。 redo log 和 binlog 有一个很大的区别就是,一个是循环写,一个是追加写。也就是说 redo log 只会记录未刷盘的日志,已经刷入磁盘的数据都会从 redo log 这个有限大小的日志文件里删除。binlog 是追加日志,保存的是全量的日志。 当数据库 crash 后,想要恢复未刷盘但已经写入 redo log 和 binlog 的数据到内存时,binlog 是无法恢复的。虽然 binlog 拥有全量的日志,但没有一个标志让 innoDB 判断哪些数据已经刷盘,哪些数据还没有。","text":"昨天在复习 MySQL 日志相关的知识,学的东西过一段时间后就会遗忘,遗忘后再重新思考,往往会有新的收获。想到几个问题,把它记录下来。 为什么 redo log 具有 crash-safe 的能力,而 binlog 没有?redo log 是什么? 一个固定大小,“循环写”的日志文件,记录的是物理日志——“在某个数据页上做了某个修改”。 binlog 是什么? 一个无限大小,“追加写”的日志文件,记录的是逻辑日志——“给 ID=2 这一行的 c 字段加1”。 redo log 和 binlog 有一个很大的区别就是,一个是循环写,一个是追加写。也就是说 redo log 只会记录未刷盘的日志,已经刷入磁盘的数据都会从 redo log 这个有限大小的日志文件里删除。binlog 是追加日志,保存的是全量的日志。 当数据库 crash 后,想要恢复未刷盘但已经写入 redo log 和 binlog 的数据到内存时,binlog 是无法恢复的。虽然 binlog 拥有全量的日志,但没有一个标志让 innoDB 判断哪些数据已经刷盘,哪些数据还没有。 举个栗子,binlog 记录了两条日志: 给 ID=2 这一行的 c 字段加1 给 ID=2 这一行的 c 字段加1 在记录1刷盘后,记录2未刷盘时,数据库 crash。重启后,只通过 binlog 数据库无法判断这两条记录哪条已经写入磁盘,哪条没有写入磁盘,不管是两条都恢复至内存,还是都不恢复,对 ID=2 这行数据来说,都不对。 但 redo log 不一样,只要刷入磁盘的数据,都会从 redo log 中抹掉,数据库重启后,直接把 redo log 中的数据都恢复至内存就可以了。这就是为什么 redo log 具有 crash-safe 的能力,而 binlog 不具备。 当数据库 crash 后,如何恢复未刷盘的数据到内存中?根据 redo log 和 binlog 的两阶段提交,未持久化的数据分为几种情况: change buffer 写入,redo log 虽然做了 fsync 但未 commit,binlog 未 fsync 到磁盘,这部分数据丢失。 change buffer 写入,redo log fsync 未 commit,binlog 已经 fsync 到磁盘,先从 binlog 恢复 redo log,再从 redo log 恢复 change buffer。 change buffer 写入,redo log 和 binlog 都已经 fsync,直接从 redo log 里恢复。 (全文完) 本文首发于我的个人博客 https://chaohang.top作者 张超航,公众号【超超不会飞】转载本站文章请注明作者和出处 超超不会飞 ,请勿用于任何商业用途 欢迎关注我的微信公众号 【超超不会飞】,获取第一时间的更新。","categories":[],"tags":[{"name":"MySQL","slug":"MySQL","permalink":"https://www.chaohang.top/tags/MySQL/"}]},{"title":"我的第一台咖啡机","slug":"我的第一台咖啡机","date":"2020-11-03T01:49:20.000Z","updated":"2020-11-03T01:46:04.000Z","comments":true,"path":"2020/11/03/我的第一台咖啡机/","link":"","permalink":"https://www.chaohang.top/2020/11/03/%E6%88%91%E7%9A%84%E7%AC%AC%E4%B8%80%E5%8F%B0%E5%92%96%E5%95%A1%E6%9C%BA/","excerpt":"自从开始早起,每天一杯咖啡就成了标配,在我的另一篇文章《咖啡+奶》中聊过,从一开始的3+1速溶进阶到黑咖啡+鲜牛奶,这次我的咖啡体验又升级啦。入了一台胶囊咖啡机 Nespresso Inissia C40,爱喝咖啡星人终于有了一台属于自己的咖啡机啦。","text":"自从开始早起,每天一杯咖啡就成了标配,在我的另一篇文章《咖啡+奶》中聊过,从一开始的3+1速溶进阶到黑咖啡+鲜牛奶,这次我的咖啡体验又升级啦。入了一台胶囊咖啡机 Nespresso Inissia C40,爱喝咖啡星人终于有了一台属于自己的咖啡机啦。 起因在这之前我对胶囊咖啡一无所知,在我的认知范围内,想要快速喝到黑咖啡,要么就是各种速溶黑咖啡,要么就是便利店或者咖啡店。 偶然在公司看到同事在做咖啡,一个小小的胶囊下去,几秒钟就出来一杯香喷喷的咖啡,超级香。之前我一直以为那是一台净水器🙂。 之后我自己去看各种安利,开箱视频,选了这台主要做意式浓缩的 Nespresso 入门款。 来一杯 Espresso趁着双十一有优惠,1号下单,2号晚上就到了。按捺住激动的心,让新机器过几遍水,清洗一下,毕竟大晚上的,喝咖啡下去就睡不着了,等明早起床再来做。事实证明晚上睡得也不太安稳,半夜醒来,以为睡得差不多,该起床了,结果一看表,3点半。 自打天气变冷,我的早起就变得有点拖延,往往会赖床20多分钟。但是今天不一样,闹钟一响就起了。 加水,加热。先来一杯温和的口味,110ml,产地埃萨俄比亚,介绍说有淡淡的花香。和速溶美式不一样的地方在于,咖啡机是通过高压萃取,表面有一层厚厚的油脂,专业术语叫 Crema,是咖啡豆里萃取出来的植物油脂。咖啡的味道和美式有点像,喝起来很润,不会涩涩的。等我再研究点咖啡知识,看以后能不能喝出不同的豆的香味。 又做了一杯拿铁,用专门做拿铁的胶囊,萃取出40ml的浓缩咖啡液,加上加热好的热牛奶,因为没有买奶泡机的预算,就直接加鲜牛奶了。味道比较淡,不知道是不是牛奶加多了,还是说 Nespresso 对于奶咖的理解就是奶味大于咖啡味? 总得来说,花两分钟就能出来一杯品质很好的浓缩咖啡,非常适合我早起喝咖啡的场景,有各种各样不同产地不同烘培程度的胶囊可供挑选,可以顺便研究一下咖啡的各种知识。 (全文完) 本文首发于我的个人博客 https://chaohang.top 作者 张超航,公众号【超超不会飞】 转载本站文章请注明作者和出处 超超不会飞 ,请勿用于任何商业用途 欢迎关注我的微信公众号 【超超不会飞】,获取第一时间的更新。","categories":[],"tags":[{"name":"随笔","slug":"随笔","permalink":"https://www.chaohang.top/tags/%E9%9A%8F%E7%AC%94/"},{"name":"咖啡","slug":"咖啡","permalink":"https://www.chaohang.top/tags/%E5%92%96%E5%95%A1/"}]},{"title":"高效学习","slug":"高效学习","date":"2020-10-19T00:28:52.000Z","updated":"2020-10-19T00:28:52.000Z","comments":true,"path":"2020/10/19/高效学习/","link":"","permalink":"https://www.chaohang.top/2020/10/19/%E9%AB%98%E6%95%88%E5%AD%A6%E4%B9%A0/","excerpt":"左耳听风《高效学习篇》笔记 端正学习态度学习所需要具备的能力: 行动力:缺乏行动力的人只会嘴上说说,很少真实践行; 方向和目标:要在前进过程不断修正自己的方向,确立和实现自己的目标; 方法:学习的方法很重要,没有方法论的支持,可能耗费大量的时间却收获很少; 自律和坚持:严格的自律,坚持,有效学习不使蛮力,可持续发展。","text":"左耳听风《高效学习篇》笔记 端正学习态度学习所需要具备的能力: 行动力:缺乏行动力的人只会嘴上说说,很少真实践行; 方向和目标:要在前进过程不断修正自己的方向,确立和实现自己的目标; 方法:学习的方法很重要,没有方法论的支持,可能耗费大量的时间却收获很少; 自律和坚持:严格的自律,坚持,有效学习不使蛮力,可持续发展。 主动学习和被动学习 被动学习:如听讲、阅读、视听、演示; 主动学习:如通过讨论、实践、教授他人。 比如阅读或收听一个极客时间的专栏就属于被动学习,留存率 20% 左右,如果我们参与了评论,做课后练习,在实际项目中运用和实践,与他人分享,撰写博客,可以把留存率提升到 50%、75% 和 90%。 所以,学习不是努力读更多书,盲目追求阅读速度和数量,这会让人产生低层次的勤奋和成长的感觉,这只是在使蛮力。要思辨,要实践,要总结和归纳,否则,你只是在机械地重复某件事,而不会有质的成长。 不要使蛮力,低效而又耗费很多时间和精力的浅度学习,会给自己一种我这么努力为什么没有成效的挫败感。要思辨,要践行,要总结和归纳。 浅度学习和深度学习移动时代所谓的碎片化学习,有营养的信息少之又少。在这种浮躁的环境下,我们只需要踏实一点,像以前那样看书,阅读英文资料,就可以超过身边绝大多数人。 深度学习的关键: 高质量的信息源和第一手的知识。像极客时间的专栏,博客除了少数精品课以外,很多都是二手知识。很多博客文章更是三四手知识了; 把知识连成地图,将自己的理解反述出来; 不断地反思和思辨,与不同年龄段的人讨论; 举一反三,并践行之,把知识转化为技能。 换言之,学习有三个步骤: 知识采集:获取信息源头,理解内在的本质; 知识缝合:把知识关联起来,成为一个体系,而不是一个个知识孤岛; 技能转换:举一反三、实践和练习。 学习是为了找到方法学习是为了找到方法而不仅仅是为了找到答案。 学习是为了找到通往答案的路径和方法,是为了拥有无师自通的能力。 学习是为了找到原理学习不仅仅是为了知道,而更是为了思考和理解。 一旦理解和掌握了这些本质的东西,你就会发现,整个复杂多变的世界在变得越来越简单。你就好像找到了所有问题的最终答案似的,一通百通了。 学习是为了了解自己,改变自己学习是为了改变自己的思考方式,改变自己的思维方式,改变自己与生俱来的那些垃圾和低效的算法。总之,学习让我们改变自己,行动和践行,反思和改善,从而获得成长。 挑选知识和信息源对计算机知识来说,英文对于我们至关重要。我们至少要能够使用英文关键词,Google 出自己想要的知识。更进一步,如果能在社区里跟大牛交流得到答案,那就算是领先于这个时代了。 好的信息源要具备下面几个特质: 应该是第一手资料,不是被别人理解过、消化过的二手资料。尤其对于知识性的东西来说,更是这样。应该是原汁原味的,不应该是被添油加醋的。 应该是有佐证、有数据、有引用的,或者是权威人士或大公司生产系统背书的资料。应该是被时间和实践检验过的,或是小心求证过的,不是拍脑袋野路子或是道听途说出来的资料。 应该是加入了一些自己的经验和思考,可以引发人深思的,是所谓信息的密集很大的文章。 注重基础和原理基础知识和原理性的东西和技术,都是经过长时间的考验的,所以,这些基础技术也有很多人类历史上的智慧结晶,会给你很多启示和帮助。 学习一项技术,要研究支撑这项技术的底层技术,而不是表面的 API。比如学习 Java NIO, Node.js, Nginx,底层都是 Socket 编程,如果你了解 Socket 编程,了解多路复用,了解各种 I/O 模型的话,学习这些上层的知识将毫不费力。 有时候,学习就像拉弓蓄力一样,学习基础知识感觉很枯燥很不实用,工作上用不到,然而学习这些知识是为了未来可以学得更快。基础打牢,学什么都快,而学得快就会学得多,学得多,就会思考得多,对比得多,结果是学得更快……这种感觉,对于想速成的人来说,很难体会。 实用的学习方法知识地图在学习一项技术的时候,使用脑图等工具,画出这项技术的主干,思考类似的技术和涉及到的底层技术,让你的知识变成一棵由主干构成的,有枝叶有果子的树。于是,我通过“顺藤摸瓜”的方式,从知识树的主干开始做广度或深度遍历,于是我就得到了一整棵树。最重要的是,当出现一些我不知道的知识点时,我就会往这棵知识树上挂,而这样一来,也使得我的学习更为系统和全面。 这种方法我和陈皓老师不谋而合,我在学习 Java 的时候,也画了一棵简单的知识树,当初我只能画出我所知道的。随着时间的推移,知识树变得繁茂,我会给我学习过的知识打上标记,遇到不懂的会挂在树上,等合适的时间学习它,直到现在,打上完成标记的枝干越多,挂在树上还未成熟(学习)的果子(知识)也越多。 这种画知识图的方式可以让你从一个技术最重要最主干的地方出发开始遍历所有的技术细节,也就是画地图的方式。如果你不想在知识的海洋中迷路,你需要有一份地图,所以,学习并不是为了要记忆那些知识点,而是为了要找到一个知识的地图,你在这个地图上能通过关键路径找到你想要的答案。 系统地学习,填实学习模板学习一门技术时,我都有一个学习模板,只有把这个学习模板中的内容都填实了,我才罢休。这个模板如下: 这个技术出现的背景、初衷、和要达到什么样的目标或是要解决什么样的问题。(非常关键) 这个技术的优势和劣势分别是什么,或者说,这个技术的 trade-off (权衡,要什么和不要什么) 是什么。 这个技术使用的场景。所谓场景一般分为两个:业务场景和技术场景。 技术的组成部分和关键点。(技术的灵魂所在) 技术的底层原理和关键实现。 已有的实现和它之间的对比。 这是陈皓老师提出的学习 6 大点,学习一项技术时,可以用这 6 大点来输出一篇博客。 举一反三举一反三的能力,可以分解成如下三种基本能力: 联想能力。 抽象能力。(基本技能) 自省能力。 如何训练举一反三的能力: 对于一个场景,制造出各种不同的问题或难题。 对于一个问题,努力寻找尽可能多的解,并比较这些解的优劣。 对于一个解,努力寻找各种不同的测试案例,以图让其健壮。 总结和归纳需要注意的是,如果只学了部分知识或者还没有学透,就开始对知识进行总结归纳,那么总结归纳出来的知识结构也只能是混乱和幼稚的。因此,学习的开始阶段,可以不急于总结归纳,不急于下判断,做结论,而应该保留部分知识的不确定性,保持对知识的开放状态。当对整个知识的理解更深入,自己站的位置更高以后,总结和归纳才会更有条理。总结归纳更多是在复习中对知识的回顾和重组,而不是一边学习一边就总结归纳。 总结归纳的方法:把你看到和学习到的信息,归整好,排列好,关联好,总之把信息碎片给结构化掉,然后在结构化的信息中,找到规律,找到相通之处,找到共同之处,进行简化、归纳和总结,最终形成一种套路,一种通用方法。 写博客是一种很好的训练总结归纳的方式。 实践出真知Eat your own dog food. 吃自己的狗粮,改进和反思。要有一种翻来覆去不断找答案又否定自己的求知精神。 Grow up through the pain. 在痛苦中成长。 坚持不懈坚持是一件反人性的事,所以,它才难能可贵,也更有价值。人不怕笨,怕的是懒,怕的是找到各种理由放弃。 坚持也不是要苦苦坚持,有循环有成就感的坚持才是真正可以持续的: 可以把你坚持的成果晒出来,让别人来给你点赞。 把坚持变成一种习惯,就像吃饭喝水一样,你感觉不到太多的成本付出。 参考资料96 | 高效学习:端正学习态度 97 | 高效学习:源头、原理和知识地图 98 | 高效学习:深度,归纳和坚持实践 (全文完) 本文首发于我的个人博客 https://chaohang.top作者 张超航,公众号【超超不会飞】转载本站文章请注明作者和出处 超超不会飞 ,请勿用于任何商业用途 欢迎关注我的微信公众号 【超超不会飞】,获取第一时间的更新。","categories":[],"tags":[{"name":"随笔","slug":"随笔","permalink":"https://www.chaohang.top/tags/%E9%9A%8F%E7%AC%94/"},{"name":"方法论","slug":"方法论","permalink":"https://www.chaohang.top/tags/%E6%96%B9%E6%B3%95%E8%AE%BA/"}]},{"title":"WordPress添加页面导航","slug":"WordPress添加页面导航","date":"2020-10-17T06:46:40.000Z","updated":"2020-10-17T06:46:40.000Z","comments":true,"path":"2020/10/17/WordPress添加页面导航/","link":"","permalink":"https://www.chaohang.top/2020/10/17/WordPress%E6%B7%BB%E5%8A%A0%E9%A1%B5%E9%9D%A2%E5%AF%BC%E8%88%AA/","excerpt":"WordPress 原生的页面导航不好用,我们可以利用插件改成分页导航。 我们使用 WP-PageNavi 插件来替代原生的页面导航。 安装结束后进入主题编辑器,找到首页 index.php,找到 navigation 或者 nav 相关的代码,注释掉相关代码,并在位置上添加如下代码: <?php wp_pagenavi(); ?>","text":"WordPress 原生的页面导航不好用,我们可以利用插件改成分页导航。 我们使用 WP-PageNavi 插件来替代原生的页面导航。 安装结束后进入主题编辑器,找到首页 index.php,找到 navigation 或者 nav 相关的代码,注释掉相关代码,并在位置上添加如下代码: <?php wp_pagenavi(); ?> 如果注释掉原有的导航代码会导致页面错乱,可以使用一个隐藏标签把原有的页面导航隐藏掉。比如我使用的主题直接注释掉原导航代码,页面就会乱,隐藏掉原导航代码即可。 123<div style=\"display:none; visibility:hidden\"> <?php nisarg_posts_navigation(); ?></div> 效果如下: (全文完) 本文首发于我的个人博客 https://chaohang.top 作者 张超航,公众号【超超不会飞】 转载本站文章请注明作者和出处 超超不会飞 ,请勿用于任何商业用途 欢迎关注我的微信公众号 【超超不会飞】,获取第一时间的更新。","categories":[],"tags":[{"name":"随笔","slug":"随笔","permalink":"https://www.chaohang.top/tags/%E9%9A%8F%E7%AC%94/"},{"name":"blog","slug":"blog","permalink":"https://www.chaohang.top/tags/blog/"}]},{"title":"WordPress自动代码高亮","slug":"WordPress自动代码高亮","date":"2020-10-17T03:32:12.000Z","updated":"2020-10-17T03:32:12.000Z","comments":true,"path":"2020/10/17/WordPress自动代码高亮/","link":"","permalink":"https://www.chaohang.top/2020/10/17/WordPress%E8%87%AA%E5%8A%A8%E4%BB%A3%E7%A0%81%E9%AB%98%E4%BA%AE/","excerpt":"WordPress 自带的代码块是没有代码高亮的,我们可以通过安装插件,设置代码高亮。 因为都是使用 Markdown 写博客,再用 MWeb 发布到博客上,Enlighter 这种需要依赖谷腾堡编辑器插入额外模块的插件,就不太适合我了,虽然这个插件很强大,有兴趣的朋友可以尝试下。 我只能选择自动美化谷腾堡代码块的插件,我找到了 Code Prettify 这个简单的插件,直接安装,不需要额外的配置,就能自动渲染代码块,目前我还是挺满意的,简单方便。大家要是有更好用的插件,也可以在评论区告诉我。 效果如下:","text":"WordPress 自带的代码块是没有代码高亮的,我们可以通过安装插件,设置代码高亮。 因为都是使用 Markdown 写博客,再用 MWeb 发布到博客上,Enlighter 这种需要依赖谷腾堡编辑器插入额外模块的插件,就不太适合我了,虽然这个插件很强大,有兴趣的朋友可以尝试下。 我只能选择自动美化谷腾堡代码块的插件,我找到了 Code Prettify 这个简单的插件,直接安装,不需要额外的配置,就能自动渲染代码块,目前我还是挺满意的,简单方便。大家要是有更好用的插件,也可以在评论区告诉我。 效果如下: Java123public class Hello() { System.out.println(\"Hello World\");} JavaScript123function hello() { console.log(\"Hello World\")} (全文完) 本文首发于我的个人博客 https://chaohang.top 作者 张超航,公众号【超超不会飞】 转载本站文章请注明作者和出处 超超不会飞 ,请勿用于任何商业用途 欢迎关注我的微信公众号 【超超不会飞】,获取第一时间的更新。","categories":[],"tags":[{"name":"随笔","slug":"随笔","permalink":"https://www.chaohang.top/tags/%E9%9A%8F%E7%AC%94/"},{"name":"blog","slug":"blog","permalink":"https://www.chaohang.top/tags/blog/"}]},{"title":"如何给WordPress添加阅读计数","slug":"如何给WordPress添加阅读计数","date":"2020-10-17T02:49:20.000Z","updated":"2020-10-17T02:49:20.000Z","comments":true,"path":"2020/10/17/如何给WordPress添加阅读计数/","link":"","permalink":"https://www.chaohang.top/2020/10/17/%E5%A6%82%E4%BD%95%E7%BB%99WordPress%E6%B7%BB%E5%8A%A0%E9%98%85%E8%AF%BB%E8%AE%A1%E6%95%B0/","excerpt":"显示文章的阅读次数对 SEO 是有很大好处的,因为每次点击都会对文章进行一次小小的更新,我们可以用 WP-PostViews 这个插件来实现这个功能。 安装插件在博客后台安装 WP-PostViews 插件。 修改主题代码方法一:简单的修改。 在外观 -> 主题编辑器 中,找到 index.php 找到 <?php while (have_posts()) : the_post(); ?> 这行代码 在循环内添加代码 <?php if(function_exists('the_views')) { the_views(); } ?> 这样就可以在首页添加一个默认样式,简易的阅读计数。","text":"显示文章的阅读次数对 SEO 是有很大好处的,因为每次点击都会对文章进行一次小小的更新,我们可以用 WP-PostViews 这个插件来实现这个功能。 安装插件在博客后台安装 WP-PostViews 插件。 修改主题代码方法一:简单的修改。 在外观 -> 主题编辑器 中,找到 index.php 找到 <?php while (have_posts()) : the_post(); ?> 这行代码 在循环内添加代码 <?php if(function_exists('the_views')) { the_views(); } ?> 这样就可以在首页添加一个默认样式,简易的阅读计数。 方法二:修改样式我们一般喜欢在标题下方添加阅读计数,如下图所示: 以我使用的主题 Nisarg 为例。 找到 index.php 和 single.php,分别代表首页和文章页。 再找到想要放入阅读计数的具体的位置,比如我的首页,需要到 content-excerpt.php 去修改,找到合适的位置,填入下面代码: 123456<!-- 添加阅读计数 --><span> <i class=\"fa fa-eye\"></i> <a class=\"entry-author-link\" rel=\"author\" href=\"<?php echo esc_url( get_author_posts_url( get_the_author_meta( 'ID' ), get_the_author_meta( 'user_nicename' ) ) ); ?>\"> <span class=\"entry-author-name\"><?php if(function_exists('the_views')) { the_views(); } ?></span></a></span> 具体是哪个位置,每个主题都不太一样,可以在博客首页打开浏览器开发工具,定位到想要放的位置的父级标签,比如 <h5 class="entry-date">...</h5>,可以更快的定位到位置。 文章页也是同样的道理,以 single.php 作为入口文件,找到 content.php,再找到具体的位置。如下图是我对文章页的修改: 设置在博客后台,设置,浏览次数选项,可以配置插件,可以设置个性化的文案,有个 忽略来自机器的浏览 的选项可以选择否,如果我们的博客访客寥寥,机器可以避免 0次阅读 的尴尬。 (全文完) 本文首发于我的个人博客 https://chaohang.top 作者 张超航,公众号【超超不会飞】 转载本站文章请注明作者和出处 超超不会飞 ,请勿用于任何商业用途 欢迎关注我的微信公众号 【超超不会飞】,获取第一时间的更新。","categories":[],"tags":[{"name":"随笔","slug":"随笔","permalink":"https://www.chaohang.top/tags/%E9%9A%8F%E7%AC%94/"},{"name":"blog","slug":"blog","permalink":"https://www.chaohang.top/tags/blog/"}]},{"title":"LeetCode25-N次幂","slug":"LeetCode25-N次幂","date":"2020-10-03T23:49:20.000Z","updated":"2020-10-03T23:46:04.000Z","comments":true,"path":"2020/10/04/LeetCode25-N次幂/","link":"","permalink":"https://www.chaohang.top/2020/10/04/LeetCode25-N%E6%AC%A1%E5%B9%82/","excerpt":"题目实现 pow(x, n) ,即计算 x 的 n 次幂函数。 示例 1: 输入: 2.00000, 10输出: 1024.00000示例 2: 输入: 2.10000, 3输出: 9.26100示例 3: 输入: 2.00000, -2输出: 0.25000解释: 2-2 = 1/22 = 1/4 = 0.25说明: -100.0 < x < 100.0n 是 32 位有符号整数,其数值范围是 [−231, 231 − 1] 。","text":"题目实现 pow(x, n) ,即计算 x 的 n 次幂函数。 示例 1: 输入: 2.00000, 10输出: 1024.00000示例 2: 输入: 2.10000, 3输出: 9.26100示例 3: 输入: 2.00000, -2输出: 0.25000解释: 2-2 = 1/22 = 1/4 = 0.25说明: -100.0 < x < 100.0n 是 32 位有符号整数,其数值范围是 [−231, 231 − 1] 。 解法一使用内置函数 1Math.pow(x, n); 解法二暴力相乘 x 的 n 次幂,递归为 x x x … x 运行结果:超时 123456789101112131415161718192021class Solution { public double myPow(double x, int n) { if (n > 0) { return pow(x, n); } return 1 / pow(x, n); } private double pow(double x, int n) { if (n == 0) { return 1; } if (n == 1) { return x; } if (n < 0) { return x * pow(x, n + 1); } return x * pow(x, n - 1); }} 解法三快速幂 : 利用分治法的思想,把 n 次相乘一分为二,转成求 x 的 n / 2 次方,再让结果相乘,递归拆分问题,让问题最小化,直到 n=1。 如果n为偶数,则 res = x^n/2^ * x^n/2^ 如果n为奇数,则 res = x^n/2^ x^n/2^ x 优化: 对2取余,可以用二进制 n % 2 == n & 1 除以2,n / 2 == n >> 1 递归 1234567891011121314151617181920class Solution { public double myPow(double x, int n) { if (n > 0) { return pow(x, n); } else { return 1 / pow(x, -n); } } private double pow(double x, int n) { if (n == 0) return 1; if (n == 1) return x; double y = pow(x, n / 2); if (n % 2 == 0) { return y * y; } else { return y * y * x; } }} 循环 1234567891011121314151617class Solution { public double myPow(double x, int n) { if (x == 0) return 0; long b = n; double res = 1.0; if (b < 0) { x = 1 / x; b = -b; } while (b > 0) { if ((b & 1) == 1) res *= x; x *= x; b >>= 1; } return res; }} 本文首发于我的个人博客 https://chaohang.top 作者张小超 转载请注明出处 欢迎关注我的微信公众号 【超超不会飞】,获取第一时间的更新。","categories":[],"tags":[{"name":"算法","slug":"算法","permalink":"https://www.chaohang.top/tags/%E7%AE%97%E6%B3%95/"},{"name":"LeetCode","slug":"LeetCode","permalink":"https://www.chaohang.top/tags/LeetCode/"}]},{"title":"LeetCode235&236-二叉树的最近公共祖先","slug":"LeetCode235-236二叉树的最近公共祖先","date":"2020-10-03T11:46:04.000Z","updated":"2020-10-03T11:46:04.000Z","comments":true,"path":"2020/10/03/LeetCode235-236二叉树的最近公共祖先/","link":"","permalink":"https://www.chaohang.top/2020/10/03/LeetCode235-236%E4%BA%8C%E5%8F%89%E6%A0%91%E7%9A%84%E6%9C%80%E8%BF%91%E5%85%AC%E5%85%B1%E7%A5%96%E5%85%88/","excerpt":"235.二叉搜索树的最近公共祖先题目给定一个二叉搜索树, 找到该树中两个指定节点的最近公共祖先。 百度百科中最近公共祖先的定义为:“对于有根树 T 的两个结点 p、q,最近公共祖先表示为一个结点 x,满足 x 是 p、q 的祖先且 x 的深度尽可能大(一个节点也可以是它自己的祖先)。” 例如,给定如下二叉搜索树: root = [6,2,8,0,4,7,9,null,null,3,5] 示例 1: 输入: root = [6,2,8,0,4,7,9,null,null,3,5], p = 2, q = 8输出: 6解释: 节点 2 和节点 8 的最近公共祖先是 6。示例 2: 输入: root = [6,2,8,0,4,7,9,null,null,3,5], p = 2, q = 4输出: 2解释: 节点 2 和节点 4 的最近公共祖先是 2, 因为根据定义最近公共祖先节点可以为节点本身。 说明: 所有节点的值都是唯一的。p、q 为不同节点且均存在于给定的二叉搜索树中。","text":"235.二叉搜索树的最近公共祖先题目给定一个二叉搜索树, 找到该树中两个指定节点的最近公共祖先。 百度百科中最近公共祖先的定义为:“对于有根树 T 的两个结点 p、q,最近公共祖先表示为一个结点 x,满足 x 是 p、q 的祖先且 x 的深度尽可能大(一个节点也可以是它自己的祖先)。” 例如,给定如下二叉搜索树: root = [6,2,8,0,4,7,9,null,null,3,5] 示例 1: 输入: root = [6,2,8,0,4,7,9,null,null,3,5], p = 2, q = 8输出: 6解释: 节点 2 和节点 8 的最近公共祖先是 6。示例 2: 输入: root = [6,2,8,0,4,7,9,null,null,3,5], p = 2, q = 4输出: 2解释: 节点 2 和节点 4 的最近公共祖先是 2, 因为根据定义最近公共祖先节点可以为节点本身。 说明: 所有节点的值都是唯一的。p、q 为不同节点且均存在于给定的二叉搜索树中。 解析该题给的是一棵二叉搜索树,利用二叉搜索树的特点——左子树小于父节点,右子树大于父节点——我们可以提前判断 p, q 位于哪个子树中。 如果 p, q 都小于父节点,最近公共祖先一定在左子树,继续递归左子树; 如果 p, q 都大于父节点,最近公共最先一定在右子树,继续递归右子树; 递归终止条件:p, q 一个大于父节点,另一个小于父节点。证明位于左右两边,此时可知最近公共祖先就是当前父节点,返回父节点。 代码123456789public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) { if (root.val > p.val && root.val > q.val) { return lowestCommonAncestor(root.left, p, q); } if (root.val < p.val && root.val < q.val) { return lowestCommonAncestor(root.right, p, q); } return root;} 时间复杂度:O(N) 遍历整棵树; 空间复杂度:O(N) 递归占用栈空间。 除了使用递归的写法,也可以改写为 while 循环的写法: 123456789101112public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) { while (root != null) { if (root.val > p.val && root.val > q.val) { root = root.left; } else if (root.val < p.val && root.val < q.val) { root = root.right; } else { return root; } } return null;} 时间复杂度:O(N) 遍历整棵树; 空间复杂度:O(1) 没有其他空间占用。 236.二叉树的最近公共祖先题目给定一个二叉树, 找到该树中两个指定节点的最近公共祖先。 百度百科中最近公共祖先的定义为:“对于有根树 T 的两个结点 p、q,最近公共祖先表示为一个结点 x,满足 x 是 p、q 的祖先且 x 的深度尽可能大(一个节点也可以是它自己的祖先)。” 例如,给定如下二叉树: root = [3,5,1,6,2,0,8,null,null,7,4] 示例 1: 输入: root = [3,5,1,6,2,0,8,null,null,7,4], p = 5, q = 1输出: 3解释: 节点 5 和节点 1 的最近公共祖先是节点 3。示例 2: 输入: root = [3,5,1,6,2,0,8,null,null,7,4], p = 5, q = 4输出: 5解释: 节点 5 和节点 4 的最近公共祖先是节点 5。因为根据定义最近公共祖先节点可以为节点本身。 说明: 所有节点的值都是唯一的。p、q 为不同节点且均存在于给定的二叉树中。 解析该题给的是一棵二叉树,我们的思路是递归遍历整棵树: 递归终止条件:如果 root 节点是空,或者等于 p 或者 q,直接返回 root 节点; 递归左子树:在左子树中查找 p 或 q,如果能找到就返回 p 或 q,找不到就返回 null; 递归右子树:在右子树中查找 p 或 q,如果找到就返回 p 或 q,找不到就返回 null; 返回值判断: 如果左子树和右子树都有返回值,证明两个节点分别位于左右两棵树,公共祖先为当前节点; 如果左子树中找不到 p 和 q,证明两个节点都在右子树,公共祖先在右子树; 如果右子树中找不到 p 和 q,证明两个节点都在左子树,公共祖先在左子树; 如果两个子树都找不到,证明找不到 p 和 q,返回 null。 代码1234567891011121314public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) { if (root == null || root == p || root == q) { return root; } TreeNode left = lowestCommonAncestor(root.left, p, q); TreeNode right = lowestCommonAncestor(root.right, p, q); if (left == null) { return right; } else if (right == null) { return left; } else { return root; }} 时间复杂度:O(N) 遍历整棵树 空间复杂度:O(N) 递归占用栈空间 本文首发于我的个人博客 https://chaohang.top 作者张小超 转载请注明出处 欢迎关注我的微信公众号 【超超不会飞】,获取第一时间的更新。","categories":[],"tags":[{"name":"算法","slug":"算法","permalink":"https://www.chaohang.top/tags/%E7%AE%97%E6%B3%95/"},{"name":"LeetCode","slug":"LeetCode","permalink":"https://www.chaohang.top/tags/LeetCode/"}]},{"title":"LeetCode98-验证二叉搜索树","slug":"LeetCode98-验证二叉搜索树","date":"2020-09-30T10:00:23.000Z","updated":"2020-09-30T10:00:23.000Z","comments":true,"path":"2020/09/30/LeetCode98-验证二叉搜索树/","link":"","permalink":"https://www.chaohang.top/2020/09/30/LeetCode98-%E9%AA%8C%E8%AF%81%E4%BA%8C%E5%8F%89%E6%90%9C%E7%B4%A2%E6%A0%91/","excerpt":"题目给定一个二叉树,判断其是否是一个有效的二叉搜索树。 假设一个二叉搜索树具有如下特征: 节点的左子树只包含小于当前节点的数。节点的右子树只包含大于当前节点的数。所有左子树和右子树自身必须也是二叉搜索树。示例 1:12345输入: 2 / \\ 1 3输出: true示例 2:123456789输入: 5 / \\ 1 4 / \\ 3 6输出: false解释: 输入为: [5,1,4,null,null,3,6]。 根节点的值为 5 ,但是其右子节点值为 4 。","text":"题目给定一个二叉树,判断其是否是一个有效的二叉搜索树。 假设一个二叉搜索树具有如下特征: 节点的左子树只包含小于当前节点的数。节点的右子树只包含大于当前节点的数。所有左子树和右子树自身必须也是二叉搜索树。示例 1:12345输入: 2 / \\ 1 3输出: true示例 2:123456789输入: 5 / \\ 1 4 / \\ 3 6输出: false解释: 输入为: [5,1,4,null,null,3,6]。 根节点的值为 5 ,但是其右子节点值为 4 。 分析二叉搜索树(Binary Search Tree)定义: 左子树上所有节点的值均小于它的根节点的值; 右子树上所有节点的值均大于它的根节点的值; Recursivly,左、右子树也分别为二叉查找树。 特点: 二叉搜索树中序遍历依次输出的值是有序的; 空树也是二叉搜索树。 解法一:中序遍历判断是否有序根据二叉搜索树的特点,中序遍历依次遍历每个节点,会得到从小到大排好序的数组。 优化:不需要把整颗树保存在数组后,再判断是否有序,而是每次只保留前一个节点,遍历过程中比较,当前节点是否大于前一个节点。 代码如下: 12345678910111213141516171819202122232425262728/** * Definition for a binary tree node. * public class TreeNode { * int val; * TreeNode left; * TreeNode right; * TreeNode(int x) { val = x; } * } */class Solution { // 题目中有一例测试用例小于 Integer 的最小值 // 只能使用 double 保存前一个节点的值 private double pre = -Double.MAX_VALUE; public boolean isValidBST(TreeNode root) { if (root == null) { return true; } // 左子树是否满足二叉搜索树的定义 boolean l = isValidBST(root.left); if (root.val <= pre) { return false; } pre = root.val; // 右子树是否满足二叉搜索树的定义 boolean r = isValidBST(root.right); return l && r; }} 复杂度分析: 时间复杂度:O(N),每个节点最多被访问一次; 空间复杂度:O(N),使用了递归,递归也可以用栈代替,每次递归都占用 1 个额外空间,最多进行 n 次递归。 解法二:递归设计一个递归函数 helper(root, lower, upper),root 为当前节点,lower 为当前节点的下限,upper 为当前节点的上限。 当递归左子树时,上限为父节点的值,因为左子树中任意一个节点都要小于父节点。下限不变。 当递归右子树时,下限为父节点的值,因为右子树任意一个节点都要大于父节点。上限不变。 12345 5 (null, null) / \\(null, 5) 1 6 (5, null) / \\ (5, 6) 3 6 如上图,括号中分别表示的是该节点的上限和下限: root 节点都为 null ; 左节点 1 的上限为父节点的值 5,1 < 5 满足条件; 右节点 6 的下限为父节点的值 5,5 < 6 满足条件; 节点 6 的左孩子上限为父节点的值 6,下限不变,3 < 6 满足条件,但是 3 < 5 不满足下限条件。 代码如下: 123456789101112131415161718192021222324252627282930313233343536/** * Definition for a binary tree node. * public class TreeNode { * int val; * TreeNode left; * TreeNode right; * TreeNode(int x) { val = x; } * } */class Solution { public boolean isValidBST(TreeNode root) { return helper(root, null, null); } private boolean helper(TreeNode node, Integer lower, Integer upper) { if (node == null) { return true; } int v = node.val; if (lower != null && v <= lower) { return false; } if (upper != null && v >= upper) { return false; } // 如果左孩子不是二叉搜索树,返回false if (!helper(node.left, lower, v)) { return false; } // 如果右孩子不是二叉搜索树,返回false if (!helper(node.right, v, upper)) { return false; } return true; }} 复杂度分析: 时间复杂度:O(N),每个节点最多访问一次; 空间复杂度:O(N),使用了递归函数,每次递归占用 1 个栈空间,最多递归 n 次。 结语两种方法都是用了递归,理解起来比较困难。如果知道中序遍历是有序的这个特点,第一种方法就比较容易理解,前提是要先理解中序遍历。第二种方法就不是普通想想就能想到的了,这是一种代码思维,多用笔在纸上写写画画,有助于理解。 本文首发于我的个人博客 https://chaohang.top 作者张小超 转载请注明出处 欢迎关注我的微信公众号 【超超不会飞】,获取第一时间的更新。","categories":[],"tags":[{"name":"算法","slug":"算法","permalink":"https://www.chaohang.top/tags/%E7%AE%97%E6%B3%95/"},{"name":"LeetCode","slug":"LeetCode","permalink":"https://www.chaohang.top/tags/LeetCode/"}]},{"title":"咖啡 + 奶","slug":"奶","date":"2020-09-26T00:08:48.000Z","updated":"2020-09-26T00:08:48.000Z","comments":true,"path":"2020/09/26/奶/","link":"","permalink":"https://www.chaohang.top/2020/09/26/%E5%A5%B6/","excerpt":"速溶咖啡起初,刚开始尝试早起,天刚蒙蒙亮,困意十足。起床第一件事,先烧一壶水,冲一杯速溶咖啡。冬天的早晨,一杯甜甜腻腻的咖啡下肚,清醒。","text":"速溶咖啡起初,刚开始尝试早起,天刚蒙蒙亮,困意十足。起床第一件事,先烧一壶水,冲一杯速溶咖啡。冬天的早晨,一杯甜甜腻腻的咖啡下肚,清醒。 后来觉得实在是太甜了,都是糖,不健康,就买了无糖的速溶咖啡,也挺好喝的,咖啡味道更浓。那时候买了一大盒,一百包,结果自己很快就不喜欢喝速溶了。这一大盒现在放办公室里,偶尔想喝的时候,冲一杯,只加四分之一水,浓郁。 咖啡 + 奶一勺奶粉 + 两勺速溶黑咖啡,用来替代速溶。和速溶一样方便,就是水不能太热,不然奶粉容易化不开。想要奶味浓一点,就多加点奶粉,想要咖啡味就多加咖啡,不想加奶粉,也可以直接冲黑咖啡喝。每天早上一杯,提神醒脑清肠胃,自带减肥效果。喝完往往几分钟后就奔厕所了,舒畅。 夏天的时候,在冰箱里放一瓶矿泉水,天气热的时候,用冰矿泉水泡黑咖啡,一杯冰美式就诞生了。没有热水泡的苦涩,淡淡的咖啡味道,不苦不涩,也不浓郁,清爽。 也可以用冰鲜牛奶代替冰水,先倒一杯当天的鲜牛奶,再加两勺咖啡,勺子搅拌均匀,成了一杯拿铁。一定要选当地的巴氏奶,高温灭菌奶好喝的也可以,蒙牛伊利的那种不行,没有奶味,也不丝滑,很咸,毁了我的好咖啡。 咖啡+冰鲜牛奶,绝配。 写在后面为什么都是速溶咖啡?方便。 喝咖啡让人上瘾,小心。 本文首发于我的个人博客 https://chaohang.top 作者张小超 转载请注明出处 欢迎关注我的微信公众号 【超超不会飞】,获取第一时间的更新。","categories":[],"tags":[{"name":"随笔","slug":"随笔","permalink":"https://www.chaohang.top/tags/%E9%9A%8F%E7%AC%94/"}]},{"title":"LeetCode703-数据流中第K大元素","slug":"LeetCode703-数据流中第K大元素","date":"2020-09-21T06:33:27.000Z","updated":"2020-09-21T06:33:27.000Z","comments":true,"path":"2020/09/21/LeetCode703-数据流中第K大元素/","link":"","permalink":"https://www.chaohang.top/2020/09/21/LeetCode703-%E6%95%B0%E6%8D%AE%E6%B5%81%E4%B8%AD%E7%AC%ACK%E5%A4%A7%E5%85%83%E7%B4%A0/","excerpt":"题目设计一个找到数据流中第K大元素的类(class)。注意是排序后的第K大元素,不是第K个不同的元素。 你的 KthLargest 类需要一个同时接收整数 k 和整数数组nums 的构造器,它包含数据流中的初始元素。每次调用 KthLargest.add,返回当前数据流中第K大的元素。 示例: 12345678int k = 3;int[] arr = [4,5,8,2];KthLargest kthLargest = new KthLargest(3, arr);kthLargest.add(3); // returns 4kthLargest.add(5); // returns 5kthLargest.add(10); // returns 5kthLargest.add(9); // returns 8kthLargest.add(4); // returns 8","text":"题目设计一个找到数据流中第K大元素的类(class)。注意是排序后的第K大元素,不是第K个不同的元素。 你的 KthLargest 类需要一个同时接收整数 k 和整数数组nums 的构造器,它包含数据流中的初始元素。每次调用 KthLargest.add,返回当前数据流中第K大的元素。 示例: 12345678int k = 3;int[] arr = [4,5,8,2];KthLargest kthLargest = new KthLargest(3, arr);kthLargest.add(3); // returns 4kthLargest.add(5); // returns 5kthLargest.add(10); // returns 5kthLargest.add(9); // returns 8kthLargest.add(4); // returns 8 分析题目的意思是说,给一个初始数组,每次往数组中插入一个元素后,返回数组中第K大的元素。 解法一简化一下题目,先假设要求的是最大值,而不是第K大的值。 我们的思路很简单,遍历初始数组,用一个变量 max 保存数组的最大值。每次插入新的元素,比较是否大于 max ,如果大于,则替换当前 max,如果小于则不做处理。 上面是数据流中求最大值的思路,当我们要求第K大的值时,可以设置一个数组,保存K个最大值,这组元素中最小的值,就是数据流中第K大的元素。每次插入新的元素,比较新插入的元素是否大于数组中的最小值,如果大于,则最小值出队,当前值入队,重新排序该数组。如果小于或等于,则丢弃新插入的元素。 时间复杂度 N*KlogK 空间复杂度 K 解法二简化解法一。可以使用优先队列(小顶堆)来代替解法一中的数组,可以把排序的时间复杂度降低为 N*logK。 12345678910111213141516171819202122232425262728293031/** * @author zhangchaohang * @date 2020/9/21 08:56 * leetcode 703 找出第k大的元素 **/public class KthLargest { /** * 优先队列,size与k相同 */ private PriorityQueue<Integer> pq; private int k; public KthLargest(int k, int[] nums) { this.k = k; this.pq = new PriorityQueue<>(k); for (int num : nums) { add(num); } } public int add(int val) { if (pq.size() < k) { pq.add(val); } else if (val > pq.peek()) { pq.poll(); pq.add(val); } return pq.peek(); }} 本文首发于我的个人博客 https://chaohang.top 作者张小超 转载请注明出处 欢迎关注我的微信公众号 【超超不会飞】,获取第一时间的更新。","categories":[],"tags":[{"name":"算法","slug":"算法","permalink":"https://www.chaohang.top/tags/%E7%AE%97%E6%B3%95/"},{"name":"LeetCode","slug":"LeetCode","permalink":"https://www.chaohang.top/tags/LeetCode/"}]},{"title":"对比 Redis 中 RDB 和 AOF 持久化","slug":"对比Redis中RDB和AOF持久化","date":"2020-09-15T06:05:39.000Z","updated":"2020-09-15T06:05:39.000Z","comments":true,"path":"2020/09/15/对比Redis中RDB和AOF持久化/","link":"","permalink":"https://www.chaohang.top/2020/09/15/%E5%AF%B9%E6%AF%94Redis%E4%B8%ADRDB%E5%92%8CAOF%E6%8C%81%E4%B9%85%E5%8C%96/","excerpt":"概念Redis 是内存数据库,数据存储在内存中,一旦服务器进程退出,数据就丢失了,所以 Redis 需要想办法将存储在内存中的数据持久化到磁盘。 Redis 提供了两种持久化功能:","text":"概念Redis 是内存数据库,数据存储在内存中,一旦服务器进程退出,数据就丢失了,所以 Redis 需要想办法将存储在内存中的数据持久化到磁盘。 Redis 提供了两种持久化功能: RDB (Redis Database):生成 RDB 文件,保存的是 key-value 的形式。 AOF (Append Only File):保存 Redis 执行过程中的写命令。 生成RDB 的生成 SAVE 命令会阻塞 Redis 服务进程,直到 RDB 文件创建完毕为止,在服务器进程阻塞期间,服务器不能处理任何命令请求。 BGSAVE 命令会派生出一个子进程,然后由子进程负责创建 RDB 文件,服务器进程(父进程)继续处理命令请求。 如果两个 key 值的修改具有事务性,需要手动加事务,不然备份时可能会导致两个值不一致。 除了主动执行命令,我们还可以通过 save 选项设置多个保存条件,只要任意一个条件满足,服务器就会执行 BGSAVE 命令: 123save 900 1save 300 10save 60 10000 那么只要满足以下三个条件中的任意一个,BGSAVE 命令就会被执行: 服务器在900秒之内,对数据库进行了至少1次修改。 服务器在300秒之内,对数据库进行了至少10次修改。 服务器在60秒之内,对数据库进行了至少10000次修改。 AOF 的生成只要打开 AOF 持久化功能,服务器在执行完一个写命令后,会以协议格式将被执行的写命令追加到服务器状态的 aof_buf 缓冲区的末尾。 现代操作系统中,用户在写文件时,操作系统通常会将写入数据暂时保存在一个内存缓冲区里面,等到缓冲区被填满,或者超过了指定时限之后,才真正将缓冲区中的数据写入磁盘。这就有可能导致缓冲区内的数据还未写入磁盘,计算机发生停机,导致数据丢失。 appendfsync 选项的值可以决定 AOF 持久化功能的效率和安全性: always 每次都同步到磁盘,效率低,安全性高; everysec 每隔一秒同步到磁盘,效率足够快,安全性高,只丢失一秒的数据; no 由操作系统控制同步到磁盘的时机,速度最快,安全性最低。 AOF 的重写因为 AOF 保存的是写命令,随着服务器的运行,同一个键值被操作的次数越多,单个键值就会产生多条写命令,AOF 文件就会越大,还原的时间就会越久。 为了解决 AOF 体积膨胀的问题,Redis 提供了 AOF 文件重写(rewrite)功能。 AOF 重写并不是对旧的 AOF 文件进行压缩。Redis 会从数据库中读出数据,生成对应的写命令,并写入新的 AOF 文件中,当新的 AOF 文件重写了所有数据的写命令,就可以替换掉旧 AOF 文件。 AOF 重写可以在后台进行,在重写过程中新产生的数据,会写入 AOF 重写缓冲区中,当重写结束再把缓冲区的写命令追加到新的 AOF 文件中即可。 载入RDB 的载入RDB 文件的载入工作是在服务器启动时自动执行的,所以 Redis 没有专门用于载入的命令。 因为 AOF 文件的更新频率通常比 RDB 文件的更新频率高,所以: 如果服务器启动了AOF 持久化功能,那么服务器会优先使用 AOF 文件来还原数据库状态。 只有在 AOF 持久化功能处于关闭状态时,服务器才会使用 RDB 文件来还原数据库状态。 AOF 的载入AOF 中包含了所有的写命令,服务器只要读入并重新执行一遍AOF文件里保存的写命令,就可以还原服务器关闭前的状态。 Redis 读取 AOF 文件并还原数据库状态的详细步骤: 创建一个不带网络连接的伪客户端:因为 Redis 的命令只能在客户端上下文中执行,而载入 AOF 文件时所使用的命令直接来源于 AOF 文件而不是网络连接,所以用来还原数据的伪客户端不需要网络连接; 从 AOF 文件中分析并读取出一条写命令; 使用伪客户端执行读出的写命令; 循环执行步骤2和步骤3,直到 AOF 文件中的所有命令都被执行。 资料Redis设计与实现-黄健宏-微信读书 本文首发于我的个人博客 https://chaohang.top 作者张小超 转载请注明出处 欢迎关注我的微信公众号 【超超不会飞】,获取第一时间的更新。","categories":[],"tags":[{"name":"Redis","slug":"Redis","permalink":"https://www.chaohang.top/tags/Redis/"}]},{"title":"《搞定1》读书笔记","slug":"《搞定1》读书笔记","date":"2020-08-27T08:26:14.000Z","updated":"2020-08-27T08:26:14.000Z","comments":true,"path":"2020/08/27/《搞定1》读书笔记/","link":"","permalink":"https://www.chaohang.top/2020/08/27/%E3%80%8A%E6%90%9E%E5%AE%9A1%E3%80%8B%E8%AF%BB%E4%B9%A6%E7%AC%94%E8%AE%B0/","excerpt":"前言 GTD 作为一种方法论对于个人提升是非常重要的,作为一种个人成长基础设施,可谓越早掌握越能够发挥作用,复利效果显著。","text":"前言 GTD 作为一种方法论对于个人提升是非常重要的,作为一种个人成长基础设施,可谓越早掌握越能够发挥作用,复利效果显著。 我花了5天左右的时间粗略翻完这本书,主要看了前三章,后面的章节是对前三章介绍的思想的实践和扩展,并不适合我现有的情况,简略的看完了。 我从这本书中获得了三个主要的思想或者说方法论: 收集所有你认为有必要收集的信息,并放入收集箱中; 清空大脑,决策下一步行动,一次只做一件事; 成功的关键:每周回顾。 收集所有的想法焦虑来源于对生活失去掌控。 我们常常在做某件事情的时候惦记着另外一件事情。比如在写博客的时候惦记着晚上要买什么菜,在开会的时候惦记着有个bug没有改,在写代码的时候惦记着有部番更新了。这些念头有时候一闪而过,有时候一直占据在脑子里,让人坐立不安,左右为难。 每当这些念头飘过时,就应该拿起笔或者打开备忘录把这些念头收集起来,让脑子放松,从而能够集中应对当前的工作。如果我们总是被这些事情占据着脑子,思维就会受阻,容易走神。 收集只是第一步,只是单纯的收集还是避免不了我们的脑子无时无刻不停的瞎琢磨,我们需要找个时间清空我们的“收集箱”,并对这些简单的念头进行决策,分类,安排任务。 下一步行动 为什么有些事情总是萦绕在你的心头: 你还没有明确地认定他们的预期结果是什么; 你还没有确定下一步的具体行动是什么; 你还没有把关于预期结果和即将采取行动的提示信息存入你所依赖的系统中去。 我们需要找个时间,把收集箱中的念头想法,通过决策加工成具体的可执行的任务。书中有个流程图,是GTD的精髓所在: 基于这张流程图,我们的GTD系统需要具备以下几个清单: 收集箱(必要)—— 用于收集所有的想法和念头,定期清空; 下一步行动(必要)—— 具体的任务 项目清单(必要)—— 需要多个步骤才能完成的任务 将来/也许清单(必要)—— 未来可能要做的任务 等待清单(必要)—— 等待他人完成 日程表(必要)—— 指定时间完成 参考资料(可选) 当我们收集了所有的想法和念头之后,通过上面这张流程图进行决策,清空收集箱,填入对应的清单中,执行时从“下一步行动”中取出一个任务,完成它,删除它。 与我们之前的认知不一样的地方在于,我们之前总是喜欢给每件事加个时间点,写着几点到几点完成此事。但事实是,往往会有突发的状况打乱你的计划,回头我们再看,1点十分应该完成的任务没有完成,现在已经2点十分了,是该做2点十分安排的事还是1点十分安排的事? 如果我们对一件事做出了承诺,在无法兑现承诺时,会让人倍感压力。 除非在某个时间点必须完成的任务需要记录到日程表中,其他任务都不需要定固定的时间节点。下一步行动的好处显而易见,我们省去了精细计划每个时间节点所花费的时间,也不会因为没有在固定时间节点完成任务而感到压力,也不会失去掌控感。 成功的关键:每周回顾以周为单位规划我们的生活是最合理的。 总体检视工作的“六层次法” 5楼视野:目的和原则 4楼视野:愿景 3楼视野:目标 2楼视野:关注点及责任范围 1楼视野:当前项目 地面:当前行动 回顾整个系统,确保他朝着正确的方向前进。清空我们的收集箱,加工还未决策的想法和念头,离开地面,站在更高的视野展望和思考当前所有任务的优先次序。做到彻底、清楚、实时和完整的掌控我们的生活和工作。 然后再回到地面,继续前行。 自然计划模式作者提出了“自然计划模式”的思考方式,值得一试: 定义目标和原则 展望结果 头脑风暴,集思广益 组织整理 明确下一步的行动方案 有些点子和细节往往在头脑风暴的时候迸发出来,我们不需要按部就班的去计划第一步第二步,写具体的方案前,先来次头脑风暴,收集我们脑袋中冒出来的点子和想法,再组织和整理这些想法,往往就能顺畅的写出行动方案。 参考资料《搞定Ⅰ》 本文首发于我的个人博客 https://chaohang.top 作者张小超 转载请注明出处 欢迎关注我的微信公众号 【超超不会飞】,获取第一时间的更新。","categories":[],"tags":[{"name":"阅读","slug":"阅读","permalink":"https://www.chaohang.top/tags/%E9%98%85%E8%AF%BB/"},{"name":"方法论","slug":"方法论","permalink":"https://www.chaohang.top/tags/%E6%96%B9%E6%B3%95%E8%AE%BA/"}]},{"title":"如何理解被 protected 修饰的成员变量?","slug":"如何理解被 protected 修饰的成员变量","date":"2020-08-11T08:48:48.000Z","updated":"2020-08-11T08:48:48.000Z","comments":true,"path":"2020/08/11/如何理解被 protected 修饰的成员变量/","link":"","permalink":"https://www.chaohang.top/2020/08/11/%E5%A6%82%E4%BD%95%E7%90%86%E8%A7%A3%E8%A2%AB%20protected%20%E4%BF%AE%E9%A5%B0%E7%9A%84%E6%88%90%E5%91%98%E5%8F%98%E9%87%8F/","excerpt":"被 protected 修饰的成员对于本包和其子类可见: 基类的 protected 成员在包内可见 若继承了基类的子类与基类不在同一个包中,那么在子类中,子类实例可以访问其从基类继承而来的 protected 方法,不能访问基类中的 protected 方法。","text":"被 protected 修饰的成员对于本包和其子类可见: 基类的 protected 成员在包内可见 若继承了基类的子类与基类不在同一个包中,那么在子类中,子类实例可以访问其从基类继承而来的 protected 方法,不能访问基类中的 protected 方法。 1234567891011121314151617181920212223/** * 基类 */package com.example.demo.base;public class ProFather { protected void f() { System.out.println(\"protected father\"); }}/** * 与基类处于同一个包的子类 */package com.example.demo.base;public class ProSon1 extends ProFather {}/** * 与基类处于不同包的子类 */package com.example.demo.base2;public class ProSon2 extends ProFather {} 在基类所在的包中,基类,子类1和子类2都可以访问函数 f() 12345678910111213141516171819/** * 与基类处于同一个包的测试类 */package com.example.demo.base;public class BaseTest { public static void main(String[] args) { // 基类可以访问函数f ProFather father = new ProFather(); father.f(); // 子类1可以访问函数f ProSon1 son1 = new ProSon1(); son1.f(); // 子类2可以访问函数f ProSon2 son2 = new ProSon2(); son2.f(); }} 在不同的包下,基类,子类1和子类2都不能访问函数 f(), 12345678910111213141516171819/** * 与基类处于不同包的测试类 */package com.example.demo.base2;public class Base2Test { public static void main(String[] args) { ProFather father = new ProFather(); // f 方法被保护,在不同的包不可见 father.f(); // error ProSon1 son1 = new ProSon1(); // f 方法被保护,在不同的包不可见 son1.f(); // error ProSon2 son2 = new ProSon2(); // f 方法被保护,在不同的包不可见 son2.f(); // error }} 在子类2中: 基类无法访问函数 f() 子类2可以访问函数 f() 123456789101112131415161718/** * 与基类处于不同包的son2 */package com.example.demo.base2;public class ProSon2 extends ProFather { protected void f1() { super.f(); } public static void main(String[] args) { ProFather father = new ProFather(); // f 方法被保护,在不同的包不可见 father.f(); // error ProSon2 son2 = new ProSon2(); // f 方法被 son2 继承,在子类可见 son2.f(); // ok }} 如果子类2重写了父类的 f 方法,并用 protected 修饰,那么在子类2所在的包下可以访问。 123456789101112131415161718192021222324252627/** * 与基类处于不同包的子类2 */package com.example.demo.base2;public class ProSon2 extends ProFather { @Override protected void f() { super.f(); } public static void main(String[] args) { ProSon2 son2 = new ProSon2(); // f 方法被 son2 重写,在子类可见 son2.f(); }}/** * 与基类处于不同包的测试类 */package com.example.demo.base2;public class Base2Test { public static void main(String[] args) { // f 方法来自于ProSon2在同一个包内可见 son2.f(); // ok }} 参考资料Java protected 关键字详解 本文首发于我的个人博客 https://chaohang.top 作者张小超 转载请注明出处 欢迎关注我的微信公众号 【超超不会飞】,获取第一时间的更新。","categories":[],"tags":[{"name":"Java","slug":"Java","permalink":"https://www.chaohang.top/tags/Java/"},{"name":"Java学习笔记","slug":"Java学习笔记","permalink":"https://www.chaohang.top/tags/Java%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0/"}]},{"title":"Java源码系列4——HashMap扩容时究竟对链表和红黑树做了什么?","slug":"Java源码系列4——HashMap扩容时究竟对链表和红黑树做了什么","date":"2020-06-04T03:27:53.000Z","updated":"2020-06-04T03:27:53.000Z","comments":true,"path":"2020/06/04/Java源码系列4——HashMap扩容时究竟对链表和红黑树做了什么/","link":"","permalink":"https://www.chaohang.top/2020/06/04/Java%E6%BA%90%E7%A0%81%E7%B3%BB%E5%88%974%E2%80%94%E2%80%94HashMap%E6%89%A9%E5%AE%B9%E6%97%B6%E7%A9%B6%E7%AB%9F%E5%AF%B9%E9%93%BE%E8%A1%A8%E5%92%8C%E7%BA%A2%E9%BB%91%E6%A0%91%E5%81%9A%E4%BA%86%E4%BB%80%E4%B9%88/","excerpt":"我们知道 HashMap 的底层是由数组,链表,红黑树组成的,在 HashMap 做扩容操作时,除了把数组容量扩大为原来的两倍外,还会对所有元素重新计算 hash 值,因为长度扩大以后,hash值也随之改变。","text":"我们知道 HashMap 的底层是由数组,链表,红黑树组成的,在 HashMap 做扩容操作时,除了把数组容量扩大为原来的两倍外,还会对所有元素重新计算 hash 值,因为长度扩大以后,hash值也随之改变。 如果是简单的 Node 对象,只需要重新计算下标放进去就可以了,如果是链表和红黑树,那么操作就会比较复杂,下面我们就来看下,JDK1.8 下的 HashMap 在扩容时对链表和红黑树做了哪些优化? rehash 时,链表怎么处理?假设一个 HashMap 原本 bucket 大小为 16。下标 3 这个位置上的 19, 3, 35 由于索引冲突组成链表。 当 HashMap 由 16 扩容到 32 时,19, 3, 35 重新 hash 之后拆成两条链表。 查看 JDK1.8 HashMap 的源码,我们可以看到关于链表的优化操作如下: 1234567891011121314151617181920212223242526272829303132333435// 把原有链表拆成两个链表// 链表1存放在低位(原索引位置)Node<K,V> loHead = null, loTail = null;// 链表2存放在高位(原索引 + 旧数组长度)Node<K,V> hiHead = null, hiTail = null;Node<K,V> next;do { next = e.next; // 链表1 if ((e.hash & oldCap) == 0) { if (loTail == null) loHead = e; else loTail.next = e; loTail = e; } // 链表2 else { if (hiTail == null) hiHead = e; else hiTail.next = e; hiTail = e; }} while ((e = next) != null);// 链表1存放于原索引位置if (loTail != null) { loTail.next = null; newTab[j] = loHead;}// 链表2存放原索引加上旧数组长度的偏移量if (hiTail != null) { hiTail.next = null; newTab[j + oldCap] = hiHead;} 正常我们是把所有元素都重新计算一下下标值,再决定放入哪个桶,JDK1.8 优化成直接把链表拆成高位和低位两条,通过位运算来决定放在原索引处或者原索引加原数组长度的偏移量处。我们通过位运算来分析下。 先回顾一下原 hash 的求余过程: 再看一下 rehash 时,判断时做的位操作,也就是这句 e.hash & oldCap: 再看下扩容后的实际求余过程: 这波操作是不是很666,为什么 2 的整数幂 - 1可以作 & 操作可以代替求余计算,因为 2 的整数幂 - 1 的二进制比较特殊,就是一串 11111,与这串数字 1 作 & 操作,结果就是保留下原数字的低位,去掉原数字的高位,达到求余的效果。2 的整数幂的二进制也比较特殊,就是一个 1 后面跟上一串 0。 HashMap 的扩容都是扩大为原来大小的两倍,从二进制上看就是给这串数字加个 0,比如 16 -> 32 = 10000 -> 100000,那么他的 n - 1 就是 15 -> 32 = 1111 -> 11111。也就是多了一位,所以扩容后的下标可以从原有的下标推算出来。差异就在于上图我标红的地方,如果标红处是 0,那么扩容后再求余结果不变,如果标红处是 1,那么扩容后再求余就为原索引 + 原偏移量。如何判断标红处是 0 还是 1,就是把 e.hash & oldCap。 rehash 时,红黑树怎么处理?1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071// 红黑树转链表阈值static final int UNTREEIFY_THRESHOLD = 6;// 扩容操作final Node<K,V>[] resize() { // .... else if (e instanceof TreeNode) ((TreeNode<K,V>)e).split(this, newTab, j, oldCap); // ...}final void split(HashMap<K,V> map, Node<K,V>[] tab, int index, int bit) { TreeNode<K,V> b = this; // Relink into lo and hi lists, preserving order // 和链表同样的套路,分成高位和低位 TreeNode<K,V> loHead = null, loTail = null; TreeNode<K,V> hiHead = null, hiTail = null; int lc = 0, hc = 0; /** * TreeNode 是间接继承于 Node,保留了 next,可以像链表一样遍历 * 这里的操作和链表的一毛一样 */ for (TreeNode<K,V> e = b, next; e != null; e = next) { next = (TreeNode<K,V>)e.next; e.next = null; // bit 就是 oldCap if ((e.hash & bit) == 0) { if ((e.prev = loTail) == null) loHead = e; else // 尾插 loTail.next = e; loTail = e; ++lc; } else { if ((e.prev = hiTail) == null) hiHead = e; else hiTail.next = e; hiTail = e; ++hc; } } // 树化低位链表 if (loHead != null) { // 如果 loHead 不为空,且链表长度小于等于 6,则将红黑树转成链表 if (lc <= UNTREEIFY_THRESHOLD) tab[index] = loHead.untreeify(map); else { /** * hiHead == null 时,表明扩容后, * 所有节点仍在原位置,树结构不变,无需重新树化 */ tab[index] = loHead; if (hiHead != null) // (else is already treeified) loHead.treeify(tab); } } // 树化高位链表,逻辑与上面一致 if (hiHead != null) { if (hc <= UNTREEIFY_THRESHOLD) tab[index + bit] = hiHead.untreeify(map); else { tab[index + bit] = hiHead; if (loHead != null) hiHead.treeify(tab); } }} 从源码可以看出,红黑树的拆分和链表的逻辑基本一致,不同的地方在于,重新映射后,会将红黑树拆分成两条链表,根据链表的长度,判断需不需要把链表重新进行树化。 源码系列文章Java源码系列1——ArrayList Java源码系列2——HashMap Java源码系列3——LinkedHashMap 参考HashMap 源码详细分析(JDK1.8) 本文首发于我的个人博客 https://chaohang.top 作者张小超 转载请注明出处 欢迎关注我的微信公众号 【超超不会飞】,获取第一时间的更新。","categories":[],"tags":[{"name":"Java","slug":"Java","permalink":"https://www.chaohang.top/tags/Java/"},{"name":"Java源码系","slug":"Java源码系","permalink":"https://www.chaohang.top/tags/Java%E6%BA%90%E7%A0%81%E7%B3%BB/"}]},{"title":"MySQL是如何实现可重复读的?","slug":"MySQL是如何实现可重复读的","date":"2020-06-02T02:03:31.000Z","updated":"2020-06-02T02:03:31.000Z","comments":true,"path":"2020/06/02/MySQL是如何实现可重复读的/","link":"","permalink":"https://www.chaohang.top/2020/06/02/MySQL%E6%98%AF%E5%A6%82%E4%BD%95%E5%AE%9E%E7%8E%B0%E5%8F%AF%E9%87%8D%E5%A4%8D%E8%AF%BB%E7%9A%84/","excerpt":"《MySQL实战45讲》笔记。 简单理解一下可重复读可重复读是指:一个事务执行过程中看到的数据,总是跟这个事务在启动时看到的数据是一致的。","text":"《MySQL实战45讲》笔记。 简单理解一下可重复读可重复读是指:一个事务执行过程中看到的数据,总是跟这个事务在启动时看到的数据是一致的。 我们可以简单理解为:在可重复读隔离级别下,事务在启动的时候就”拍了个快照“。注意,这个快照是基于整个库的。 这时,你可能就会想,如果一个库有 100G,那么我启动一个事务,MySQL就要拷贝 100G 的数据出来,这个过程得多慢啊。可是,我平时的事务执行起来很快啊。 实际上,我们并不需要拷贝出这 100G 的数据。我们来看下”快照“是怎么实现的。 拍个快照InnoDB 里面每个事务都有一个唯一的事务 ID,叫作 transaction id。它在事务开始的时候向 InnoDB 的事务系统申请的,是按申请顺序严格递增的。 每条记录在更新的时候都会同时记录一条 undo log,这条 log 就会记录上当前事务的 transaction id,记为 row trx_id。记录上的最新值,通过回滚操作,都可以得到前一个状态的值。 如下图所示,一行记录被多个事务更新之后,最新值为 k=22。假设事务A在 trx_id=15 这个事务提交后启动,事务A 要读取该行时,就通过 undo log,计算出该事务启动瞬间该行的值为 k=10。 在可重复读隔离级别下,一个事务在启动时,InnoDB 会为事务构造一个数组,用来保存这个事务启动瞬间,当前正在”活跃“的所有事务ID。”活跃“指的是,启动了但还没提交。 数组里面事务 ID 为最小值记为低水位,当前系统里面已经创建过的事务 ID 的最大值加 1 记为高水位。 这个视图数组和高水位,就组成了当前事务的一致性视图(read-view)。 这个视图数组把所有的 row trx_id 分成了几种不同的情况。 如果 trx_id 小于低水位,表示这个版本在事务启动前已经提交,可见; 如果 trx_id 大于高水位,表示这个版本在事务启动后生成,不可见; 如果 trx_id 大于低水位,小于高水位,分为两种情况: 若 trx_id 在数组中,表示这个版本在事务启动时还未提交,不可见; 若 trx_id 不在数组中,表示这个版本在事务启动时已经提交,可见。 InnoDB 就是利用 undo log 和 trx_id 的配合,实现了事务启动瞬间”秒级创建快照“的能力。 举个栗子初始化语句 123456CREATE TABLE `t` ( `id` int(11) NOT NULL, `k` int(11) DEFAULT NULL, PRIMARY KEY (`id`)) ENGINE=InnoDB;insert into t(id, k) values(1,1),(2,2); 下表为事务A, B, C 的执行流程 事务A 事务B 事务C START TRANSACTION WITH CONSISTENT SNAPSHOT; START TRANSACTION WITH CONSISTENT SNAPSHOT; UPDATE t SET k=k+1 WHERE id=1; UPDATE t SET k=k+1 WHERE id=1; SELECT k FROM t WHERE id=1; SELECT k FROM t WHERE id=1; COMMIT; COMMIT; 我们假设事务A, B, C 的 trx_id 分别为 100, 101, 102。事务A开始前活跃的事务 ID 只有 99,并且 id=1 这一行数据的 trx_id=90。 根据假设,我们得出事务启动瞬间的视图数组:事务A:[99, 100],事务B:[99, 100, 101],事务C:[99, 100, 101, 102]。 事务C通过更新语句,把 k 更新为 2,此时trx_id=102; 事务B通过更新语句,把 k 更新为 3,此时trx_id=101; 事务B通过查询语句,查询到最新一条记录为3,trx_id=101,满足隔离条件,返回 k=3; 事务A通过查询语句: 查询到最新一条记录为3,trx_id=101,比高水位大,不可见; 通过 undo log,找到上一个历史版本,trx_id=102,比高水位大,不可见; 继续找上一个历史版本,trx_id=90,比低水位小,可见。 提出问题:为啥事务B更新的时候能看到事务C的修改?我们假设事务B在更新的看不到事务C的修改,是什么个情况? 事务B查询到最新一条记录为2,trx_id=102,比高水位大,不可见; 通过 undo log,找到上一个版本,trx_id=90,比低水位小,可见; 返回记录 k=1,执行 k=k+1,把 k 更新为2,此时 trx_id=101。 如果是这种情况,事务C可能就蒙了:“啥子情况,我的更新怎么就丢了”。事务B覆盖了事务C的更新。 所以,InnoDB在更新时运用一条规则:更新数据都是先读后写的,而这个读,只能读当前的值,称为“当前读“ (current read)。 因此,事务B在更新时要拿到最新的数据,在此基础上做更新。紧接着,事务B在读取的时候,查询到最新的记录为3, trx_id=101 为当前事务ID,可见。 我们再假设另一种情况: 事务B在更新之后,事务C紧接着更新,事务B回滚了,事务C成功提交。 事务B 事务C START TRANSACTION WITH CONSISTENT SNAPSHOT; START TRANSACTION WITH CONSISTENT SNAPSHOT; UPDATE t SET k=k+1 WHERE id=1; UPDATE t SET k=k+1 WHERE id=1; SELECT k FROM t WHERE id=1; ROLLBACK; COMMIT; 如果按照当前读的定义,会发生以下事故,假设当前 K=1: 事务B把 k 更新为 2; 事务C读取到当前最新值,k=2,更新为3; 事务B回滚; 事务C提交。 这时候,事务C发现自己想要执行的是 +1 操作,结果变成了 ”+2“ 操作。 InnoDB 肯定不允许这种情况的发生,事务B在执行更新语句时,会给该行加上行锁,直到事务B结束,才会释放这个锁。 小结 InnoDB 的行数据有多个版本,每个版本都有 row trx_id。 事务根据 undo log 和 trx_id 构建出满足当前隔离级别的一致性视图。 可重复读的核心是一致性读,而事务更新数据的时候,只能使用当前读,如果当前记录的行锁被其他事务占用,就需要进入锁等待。 参考03 | 事务隔离:为什么你改了我还看不见?-极客时间 08 | 事务到底是隔离的还是不隔离的?-极客时间 本文首发于我的个人博客 https://chaohang.top 作者 张小超 公众号【超超不会飞】 转载请注明出处 欢迎关注我的微信公众号 【超超不会飞】,获取第一时间的更新。","categories":[],"tags":[{"name":"MySQL","slug":"MySQL","permalink":"https://www.chaohang.top/tags/MySQL/"},{"name":"笔记","slug":"笔记","permalink":"https://www.chaohang.top/tags/%E7%AC%94%E8%AE%B0/"}]},{"title":"MySQL的各种日志","slug":"MySQL的各种日志","date":"2020-06-01T03:38:46.000Z","updated":"2020-06-01T03:38:46.000Z","comments":true,"path":"2020/06/01/MySQL的各种日志/","link":"","permalink":"https://www.chaohang.top/2020/06/01/MySQL%E7%9A%84%E5%90%84%E7%A7%8D%E6%97%A5%E5%BF%97/","excerpt":"《MySQL实战45讲》笔记。","text":"《MySQL实战45讲》笔记。 1. redo log——只是一块粉板孔乙己又来酒馆喝酒,兜里没钱手机也没电了,只能向掌柜的赊账。掌柜有一块粉板,当客人要赊账的时候就往上写一笔,等客人少的时候或者粉板写满了就记到账本里去。还好有这块粉板,不然每次客人要赊账,掌柜都要翻看账本,在密密麻麻的账本里找到赊账客人的名字绝对不是一件容易的事,有了粉板,掌柜只要往粉板上记一笔:“孔乙己 赊 两文”,空闲的时候再更新到账本里去,简单多了。 同样的,MySQL也有一块“粉板”—— redo log。更新的时候,先写到 redo log 和内存里,这次更新就算是结束了。等到合适的时机再写到磁盘里,大大减小了写磁盘的次数。 redo log 是固定大小、“循环写”的,就像粉板一样,顶多也就记个十几二十条,多了就记不下了,这时会把粉板上的帐都写到账本里,再擦掉粉板,从头开始记。假设 redo log 配置了4组文件,每个文件 1G ,一共可记录 4G 的操作,写满了就会擦掉一部分记录。 redo log 是物理日志,记录的是“在某个数据页上做了什么修改”。 有了 redo log,InnoDB 就可以保证即使数据库发生了异常重启,之前提交的记录都不会丢失,这个能力称为 crash-safe。 2. binlogbinlog 是 MySQL 的 Server 层实现的,所有引擎都可以使用。 binlog 是逻辑日志,记录的是这个语句的原始逻辑,比如”给 ID=2 这一行的 c 字段加1“。 binlog 是“追加写”的,一个文件写完了会切换到下一个,不会覆盖以前的日志。 为什么有了 redo log 还需要 binlog? 其实 redo log 才是那个新来的仔。MySQL 自带了 binlog 日志用于归档,没有 crash-safe 的能力。InnoDB 引擎以插件的形式引入 MySQL 时,为了能够实现 crash-safe 的能力,引入了 redo log 。 一般我们用 binlog 做主从复制,数据恢复等操作。 binlog 是如何做数据恢复的? 一般我们做数据库备份是一周一备,一天一备,也可能一月一备。 假设今天中午12点,我们发现部分数据被误删了。需要恢复到昨天晚上8点这个时间段。但是数据库是每天凌晨3点的时候备份,离我们最近的一份备份数据已经缺失,只能恢复到昨天凌晨3点。这个时候我们就可以拿出昨天凌晨3点到晚上8点这个时间段的 binlog,重放到数据缺失前的那个时刻。在把这份数据恢复到线上数据库去。 3. 更新操作的执行流程了解了 redo log 和 binlog 这两个日志的概念,我们再来看看执行器和 InnoDB 引擎在执行这个简单的 update 语句时的内部流程。 执行器先找引擎取 ID=2 这一行。如果数据在内存就直接返回,如果不在内存就先从磁盘读入内存,再返回。 执行器拿到数据,给这行的 c 值加 1。 引擎将这行数据的改动更新到内存中,同时将这个更新操作记录到 redo log 里面,此时 redo log 处于prepare 状态。然后告知执行器执行完成了,随时可以提交事务。 执行器生成这个操作的 binlog,并把 binlog 写入磁盘。 执行器调用引擎的提交事务接口,引擎把刚刚写入的 redo log 改成 commit 状态,更新完成。 下图出自《MySQL实战45讲》,浅色框表示是在 InnoDB 内部执行的,深色框表示实在执行器中执行的。 4. redo log 和 binlog 的两阶段提交为什么需要两阶段提交? 我们先假设没有两阶段提交时,可能会有以下两种情况: redo log 提交成功了,这时候数据库挂掉导致 binlog 没有成功写入。数据库重启之后通过 redo log 把数据恢复回来,但是 binlog 没有成功写入,导致我们在做主从复制或者数据恢复的时候,数据不一致。 binlog 提交成功了,这时候数据库挂掉导致 redo log 没有成功写入。数据库重启之后,无法恢复崩溃之前提交的那个事务,这部分数据更改在主库缺失。但是 binlog 已经成功写入了,从库反而有了该事务的改动,导致数据不一致。 综上我们知道,redo log 和 binlog 必须同时成功或同时失败,才能保证数据一致性。 两阶段提交是如何保证 redo log 和 binlog 同时成功或同时失败的? 假设已经有了两阶段提交,分析一下以下两种情况: 假设在上图的时刻A,redo log 处于 prepare 之后,写 binlog 之前,数据库挂掉了。由于此时 binlog 还没有写,redo log 也还没有提交,所以崩溃恢复后,这个事务会回滚。这时候 binlog 还没写,所以也不会传到备库。 假设在上图的时刻B,写 binlog 之后,redo log 还没有 commit 前发生 crash。那么崩溃恢复时,MySQL 会做以下判断: 如果 redo log 里面的事务是完整的,也就是已经有了 commit 标识,则直接提交; 如果 redo log 里面的事务只有完整的 prepare,则判断对应的事务 binlog 是否存在并完整:a. 如果是,则提交事务;b. 否则,回滚事务。 那么 MySQL 是怎么知道 binlog 是否完整的? 一个事务的 binlog 是有完整的格式的: statement 格式的 binlog,最后会有 COMMIT; row 格式的 binlog,最后会有一个 XID event。 5. change buffer什么是 change buffer ? 当需要更新一个数据时,如果数据页在内存里就直接更新了,如果数据页不在内存里,InnoDB 会将这些更新操作缓存在 change buffer 中,这样就不需要读磁盘了。在下次查询需要访问到这个数据页的时候,将数据页读入内存,然后执行 change buffer 中与这个页有关的操作。 如果能够将更新操作先记录在 change buffer, 减少读磁盘,更新操作变快。而且数据读入内存是需要占用 buffer pool 的,所以这种方式还能够避免占用内存,提高内存利用率。 change buffer 是可以持久化的数据,change buffer 在内存中有拷贝,也会被写入到磁盘中。 将 change buffer 中的操作应用到原数据页,得到最新结果的过程称为 merge。以下情况会触发 merge: 访问数据页 系统有后台线程定期 merge 数据库正常关闭也会触发 merge 为什么普通索引比唯一索引效率高? 查询时: 普通索引查出数据页,数据页读入内存,判断是否有相等的数据,返回数据。 唯一索引查出数据页,数据页读入内存,直接返回数据。 虽然普通索引多了一步判断,但是数据是以页为单位读入内存的,判断大概率是内存操作,消耗很小,可以忽略。 更新时: 普通索引直接更新内存或者缓存到 change buffer 中,结束。 唯一索引更新时需要判断是否有数据冲突,所以无法利用 change buffer,当数据页不在内存时,必须读磁盘写入内存再做判断,效率低于普通索引。 什么情况下不适合使用 change buffer? 如果某个业务更新后马上做查询,即使我们把更新先记录在 change buffer,读取操作也会马上把数据读入内存,而且立即触发 merge 操作。这种情况下,随机访问磁盘的次数没有减少,反而增加了 change buffer 的维护代价。所以对于这种业务,change buffer 反而起到了反作用。 6. change buffer 和 redo log插入时 插入的数据页刚好在内存中,直接更新内存中的数据页(上图1)。 数据页不在内存中,在 change buffer 里记录下对该数据页的改动(上图2)。 将上述两个动作记入 redo log 中(上图3,4)。 我们可以看到,执行这条语句的成本很低,写了两处内存(内存和change buffer),写了一处磁盘(redo log,两次操作合在一起写磁盘),而且还是顺序写(直接写日志文件)。 同时,图中两个虚线箭头,是后台操作(异步操作,空闲时间就刷的那种),不影响该语句的响应时间。 查询时 数据在内存时,直接读取。 数据不在内存时,从磁盘读入内存,然后应用 change buffer 里的操作日志,在内存生成一个最新的数据。 比较从上面两个案例我们可以看出: redo log 主要节省的是随机写磁盘的 IO 消耗(把更新时的随机写磁盘转成顺序写)。 change buffer 主要节省的是随机读磁盘的 IO 消耗(减少更新时读磁盘的次数)。 7. binlog 和 redo log 的持久化binlog 的写入机制binlog 的写入逻辑:事务执行过程中,先把日志写到 binlog cache,事务提交的时候,再把 binlog cache 写到 binlog 文件中。 一个事务的 binlog 是不能被拆开写的,因此不论这个事务多大,也要确保一次性写入。 系统给 binlog cache 分配了一片内存,每个线程一个,参数 binlog_cache_size 用于控制单个线程内 binlog cache 所占内存的大小。如果超过了这个参数规定的大小,就要暂存的磁盘中。 事务提交时,执行器把 binlog cache 里的完整事务写到 binlog file 和 磁盘中,并清空 binlog cache。状态如下图所示: 图中的 write,指的是日志写入到文件系统的 page cache,并没有把数据持久化到磁盘,速度比较快。 图中的 fsync,指的是日志最终持久化到磁盘,速度慢。 write 和 fsync 的时机,由参数 sync_binlog 控制: sync_binlog=0 时,表示每次提交事务都只 write,不 fsync; sync_binlog=1 时,表示每次提交事务都会执行 fsync; sync_binlog=N(N>1) 时,表示每次提交事务都 write,但累积 N 个事务后才 fsync。 将 sync_binlog 设置为 N,对应的风险是:如果主机发生异常重启,会丢失最近 N 个事务的 binlog 日志(没有持久化到磁盘,主机挂了就丢失了)。 redo log 的写入机制 事务在执行过程中,生成的 redo log 会先写到 redo log buffer 中。 写入到 page cache 的速度也很快,写入到磁盘的速度慢。 innodb_flush_log_at_trx_commit 参数用来控制 redo log 的写入策略: 设为 0 时,表示每次事务提交时都只是把 redo log 留在 redo log buffer 中; 设为 1 时,表示每次事务提交时都将 redo log 直接持久化到磁盘; 设为 2 时,表示每次事务提交时都只是把 redo log 写到 page cache。 InnoDB 有一个后台线程,每隔 1 秒,就会把 redo log buffer 中的日志,调用 write 写到文件系统的 page cache,然后调用 fsync 持久化到磁盘。 事务执行过程中写入 redo log buffer 的记录,也会随着其他事务的提交或者定时写入过程持久化到磁盘中。也就是说有些还未提交的事务的 redo log 也会被持久化。 redo log buffer 占用的空间即将达到 innodb_log_buffer_size 一半的时候,也会触发持久化操作。 分组提交为了降低写磁盘的次数,redo log 把 write 和 fsync 拆成两个步骤,当有并发时,事务A写完 page cahce,事务B也写完了 page cache,事务A触发 fsync 的时候,会把两个事务的 redo log 并在一组,一起写磁盘。 并且为了能让更多的事务加入同一个组,InnoDB 让 redo log 和 binlog 的 write 和 fsync 交替执行,分组提交的优化,redo log 和 binlog 都有。 WAL 机制是减少磁盘写,但每次提交事务都要写 redo log 和 binlog,写磁盘的次数好像没有减少? redo log 和 binlog 都是顺序写,磁盘的顺序写比随机写速度快;(日志写磁盘都是顺序写的,事务提交后直接把数据写磁盘就是随机访问); 组提交机制可以大幅降低磁盘的 IOPS 消耗。 MySQL 是如何保证 crash-safe 的。redo log 是如何保证 crash-safe 的。写到 redo log buffer 不能保证 crash-safe,写到 fs cache 也不能保证 crash-safe,只有 redo log 写入磁盘之后,数据库异常重启,从磁盘中的 redo log 拿出未执行的日志进行恢复,才算是 crash-safe。这也说明了多个事务提交之后才写磁盘,还是会有事务丢失。只有每个事务提交后都进行写磁盘才能保证数据完全不丢失。 binlog 为什么无法保证 crash-safe? 如果 binlog 写入成功了,数据还没写入磁盘,数据库异常崩溃,重启后主库没有这部分数据,而通过 binlog 同步的从库却有了这部分配置,导致主从数据不一致。 如果 数据写入磁盘,binlog 写入失败了,数据库异常崩溃,重启后主库有这部分数据,而通过 binlog 同步的从库没有这部分数据,导致主从数据不一致。 能否只使用 binlog 或 redo log 单个日志保证 crash-safe? 我的理解是:并不是单个 log 无法保证 crash-safe,而是 binlog 本身无法保证 crash-safe,因为 InnoDB 无法重新设计 binlog,所以引入了 redo log。并且花了很大力气来保证 redo log 和 binlog 的一致性。如果重新设计 MySQL,可以使用 redo log 实现 binlog 的功能,也可以把 binlog 设计成 crash-safe 的,这样就只需要一种 log 了。 参考02 | 日志系统:一条SQL更新语句是如何执行的?-极客时间 09 | 普通索引和唯一索引,应该怎么选择?-极客时间 12 | 为什么我的MySQL会”抖”一下?-极客时间 15 | 答疑文章(一):日志和索引相关问题-极客时间 23 | MySQL是怎么保证数据不丢的?-极客时间 本文首发于我的个人博客 https://chaohang.top 作者 张小超 公众号【超超不会飞】 转载请注明出处 欢迎关注我的微信公众号 【超超不会飞】,获取第一时间的更新。","categories":[],"tags":[{"name":"MySQL","slug":"MySQL","permalink":"https://www.chaohang.top/tags/MySQL/"},{"name":"笔记","slug":"笔记","permalink":"https://www.chaohang.top/tags/%E7%AC%94%E8%AE%B0/"}]},{"title":"早起实操手册","slug":"早起实操手册","date":"2020-05-07T03:38:46.000Z","updated":"2020-05-07T03:38:46.000Z","comments":true,"path":"2020/05/07/早起实操手册/","link":"","permalink":"https://www.chaohang.top/2020/05/07/%E6%97%A9%E8%B5%B7%E5%AE%9E%E6%93%8D%E6%89%8B%E5%86%8C/","excerpt":"从2019年大概11月份左右,我开始尝试早起,一直坚持到现在已经有半年左右了。目前工作日和周日5点45分或者6点20分左右起床,具体时间看昨晚的入睡时间。周六关闭闹钟靠生物钟起床。","text":"从2019年大概11月份左右,我开始尝试早起,一直坚持到现在已经有半年左右了。目前工作日和周日5点45分或者6点20分左右起床,具体时间看昨晚的入睡时间。周六关闭闹钟靠生物钟起床。 起因最初的起因很偶然——女朋友考研到了冲刺阶段,每天5点起床背书,我想着干脆我也陪着早起,一起学习,这样女朋友就不用担心吵醒我了。 初期问与答 如何激励自己的? 我给自己画了一个表格,每次早起就打个勾勾,看着勾勾越来越多,很有成就感。我还约定每次早起就奖励5块钱,这些钱攒起来可以买自己喜欢的。 小贴士:最好是实体的表格,拿张4A纸,自己手绘的表格,贴在墙上,每天看着很有成就感。 早起做什么? 刚开始早起的时候,我并没有想好自己要做什么。我就想着早起就好了,先养成习惯,一开始可以开心一点,我在前半个月左右都是看小说,那段时间看完了《谷歌方法》。 困了怎么办? 喝咖啡啊!从一开始的普通速溶咖啡,到一勺黑咖啡,到两勺黑咖啡。现在几乎每天早起都是先来一杯咖啡,不过下午就不能喝了,怕晚上睡不着。 早起高效吗? 我是属于早晨专注度特别高,思路也清晰,学习效率高的那种。一到晚上就想睡觉,没办法思考复杂的问题。不过这个因人而异,尝试出真知。 这么早起,晚上要几点睡? 早起倒逼自己早睡! 比如早上6点起床,需要晚上10点睡觉才能睡满8个小时。我一般晚上10点半到11点左右睡觉,如果遇到有什么事情晚了也没事,照样早起,睡眠质量好才是关键,睡眠质量不好即使睡满8个小时,白天也困。 先早起,到晚上自然而然就困了,不存在什么晚上睡不着的幺蛾子。 早晨关掉闹钟倒下继续睡怎么办? 刚开始这个习惯的时候我有天然的地理优势——我住的loft小公寓,二楼只有床,一楼是生活区——手机在楼下,闹钟一响我就下楼关手机,自然就离开床了。后来搬家,住在只有床的小房间,关了闹钟屁股后面就是床,导致那段时间早起规律很受影响。 小贴士:尽量把手机放远点,起床关闹钟,起床了就去烧水洗漱干别的事情。 平衡生活与工作 早上想做饭怎么办? 我一般带饭到公司,不想吃隔夜的饭菜,只能在早晨做。晚上把菜和肉都切好洗好,把米洗好放电饭煲,起床第一件事,烧水+电饭煲点开始煮饭。泡杯咖啡先学习一个小时再开始做饭,半个小时搞定。 晚上公司加班怎么办? 一开始我们公司是不加班的,我每天大概7点下班,8点到家,收拾好后还能休闲一下再睡觉。过年回来后,借着疫情这个借口,公司开始强推1095,每天晚上必须9点才可以下班。 因为我早上到公司比较早(大概8点半到了,公司规定10点上班),晚上我就弹性一点,8点左右开溜,到家9点,洗漱完困了就直接睡觉,不困的话就玩两局游戏,也能保持在11点左右睡觉。 晚上回家还要洗衣服,洗碗,切菜做饭怎么办? 回家早的话还好,回家晚的话就不行了。这时候就要夸夸我可爱的女朋友了,承包了洗碗和切菜的业务,让我晚上回家可以直接洗漱,不用操心其他事情。 抖音快手刷着刷着就凌晨2点了,怎么办? 人不要与算法作斗争,赢不了的,卸载完事。Bilibili不香吗? 收获每天早晨都有实打实的一个小时学习时间,利用这个时间我看了一些想看的书,刷了买了没来得及看的极客时间课程(刚刷完《数据结构与算法之美》),难度大的学习内容特别适合放在早上,一天最清醒的时间里学习。 早上有时间做饭,在家和女朋友一起吃早餐,享受清晨的阳光、清新的空气和啾啾啾的鸟叫声。 本文首发于我的个人博客 https://chaohang.top 作者 张小超 公众号【超超不会飞】 转载请注明出处 欢迎关注我的微信公众号 【超超不会飞】,获取第一时间的更新。","categories":[],"tags":[{"name":"随笔","slug":"随笔","permalink":"https://www.chaohang.top/tags/%E9%9A%8F%E7%AC%94/"},{"name":"生活","slug":"生活","permalink":"https://www.chaohang.top/tags/%E7%94%9F%E6%B4%BB/"}]},{"title":"深入理解Java虚拟机3——垃圾回收","slug":"深入理解Java虚拟机3——垃圾回收","date":"2020-02-27T23:15:31.000Z","updated":"2020-02-27T23:15:31.000Z","comments":true,"path":"2020/02/28/深入理解Java虚拟机3——垃圾回收/","link":"","permalink":"https://www.chaohang.top/2020/02/28/%E6%B7%B1%E5%85%A5%E7%90%86%E8%A7%A3Java%E8%99%9A%E6%8B%9F%E6%9C%BA3%E2%80%94%E2%80%94%E5%9E%83%E5%9C%BE%E5%9B%9E%E6%94%B6/","excerpt":"《深入理解Java虚拟机》第3章读书笔记","text":"《深入理解Java虚拟机》第3章读书笔记 本文介绍了如何判断对象是否存活,三种垃圾回收算法,分析比较了几种垃圾收集器的特点。本文并非原创,是《深入理解Java虚拟机》第3章的整理、总结和补充。 对象已死?垃圾收集器在对堆进行回收前,要先判断哪些对象“存活”,哪些已经“死去”。 引用计数算法给对象中添加一个引用计数器,每当有一个地方引用它时,计数器就加1;当引用失效时,计数器就减1;任何时刻计数器为0的对象就是不可能再被使用的。 主流的Java虚拟机里面没有选用引用计数算法来管理内存。 优点:实现简单,效率高。 缺点:很难解决对象之间相互循环引用的问题。 循环引用问题,如下代码所示, 1234567891011121314151617181920212223242526272829303132/** * 源代码出自《深入理解Java虚拟机》P62-63 * 循环引用 **/public class ReferenceCountingGC { public Object instance = null; private static final int _1MB = 1024 * 1024; /** * 这个成员属性的唯一意义就是占点内存,以便能在GC日志中看清楚是否被回收过 */ private byte[] bigSize = new byte[2 * _1MB]; /** * 运行参数 * -XX:+PrintGCDetails -Xms20M -Xmx20M -Xmn10M */ public static void main(String[] args) { ReferenceCountingGC objA = new ReferenceCountingGC(); ReferenceCountingGC objB = new ReferenceCountingGC(); objA.instance = objB; objB.instance = objA; objA = null; objB = null; // 假设在这行发生GC,objA和objB是否能被回收? System.gc(); }} JVM参数设置了新生代为10MB,运行结果显示,在第一次触发GC时,“5120K->576K(9216K)”回收了约4MB内存。意味着虚拟机并没有因为两个对象相互引用就不回收它们,这也侧面说明了虚拟机并不是通过引用计数算法来判断对象是否存活 可达性分析算法通过一系列的称为“GC Roots”的对象作为起始点,从这些节点开始向下搜索,搜索走过的路径称为引用链(Reference Chain),当一个对象到GC Roots没有任何引用链相连(用图论的话来说,就是从GC Roots到这个对象不可达)时,则证明此对象是不可用的。 如图,object5、object6、object7 为可回收对象 主流的Java虚拟机使用可达性分析算法 在Java语言中,GC Roots包括以下几种: 虚拟机栈(栈帧中的本地变量表)中引用的对象 方法区中类静态属性引用的对象 方法区中常量引用的对象 本地方法栈中Native方法引用的对象 四种引用 强引用:代码中普遍存在,垃圾收集器不会回收强引用的对象。 软引用:有用但非必需,在系统将要发生内存溢出异常之前,会把这些对象列入回收范围。 弱引用:非必需,无论当前内存是否足够,下次垃圾回收都会回收掉这些对象。 虚引用:最弱的引用关系,是否有虚引用不对其生存时间构成影响。 垃圾收集算法标记-清除算法最基础的收集算法——“标记-清除”(Mark-Sweep)算法。一般用于老年代。 算法分为“标记”和“清除”两个阶段: 标记出需要回收的对象 清除被标记的对象 缺点:效率低,空间碎片化。 复制算法为了解决效率问题,出现了“复制”算法(Copying)。它将内存分为两块,每次只使用其中一块,当这一块内存用完了,就将还存活的对象复制到另一块上面,然后再把已使用过的内存空间一次清理掉。 缺点:内存缩小为原来的一半。 JVM虚拟机在新生代使用这种收集算法,并不是按照 1:1 的比例来划分内存空间。而是根据新生代中的对象98%是“朝生夕死”这一特点,将内存分为一块较大的 Eden(80%) 空间和两块较小的 Survivor(10%) 空间。每次只使用 Eden 和其中一块 Survivor,当回收时,将 Eden 和 Survivor 中还存活的对象,一次性地复制到另外一块 Survivor 空间上,最后清理掉 Eden 和刚才用过的 Survivor 空间。 HotSpot默认Eden和Survivor的大小比例为8:1,这样就只有10%的内存被“浪费”。当出现超过10%的对象存活时,就会使用老年代做分配担保,把Survivor空间放不下的对象,直接放入老年代。 标记-整理算法在Mark-Sweep算法的基础上做了改良,用于解决空间碎片化问题。标记-整理(Mark-Compact)算法在标记后不是简单做清除,而是让所有存活的对象都向一端移动,然后清理掉端边界以外的内存。一般用于老年代。 安全点和安全区域安全点在做可达性分析时,需要保持分析期间整个系统不会发生变化,这就导致GC进行时必须停顿所有Java执行线程(Stop The World),即使是在号称(几乎)不会发生停顿的CMS收集器中,枚举根节点时也必须要停顿。 程序执行时并非在所有地方都能停下来开始GC,只有在到达安全点(Safepoint)时才能暂停。Safepoint 的选定既不能太少以致于让GC等待时间太长,也不能过于频繁以致于过分增大运行时的负荷。所以,安全点的选定基本上是以程序“是否具有让程序长时间执行的特征”为标准进行选定的,例如方法调用,循环跳转,异常跳转等。 如何在GC发生时让线程都跑到最近的安全点再停顿下来? 抢先试中断:先把所有线程中断,发现不在安全点的线程恢复线程,让它跑到安全点。 主动式中断:设置一个不可读的内存位置作为中断标志,标志与安全点重合,当线程执行到这个标志时自己中断挂起。 安全区域安全区域(Safe Region)是指在一段代码片段中,引用关系不会发生变化。在这个区域的任何地方开始GC都是安全的。典型的安全区域比如线程处于Sleep状态或者Blocked状态。 在线程执行到Safe Region中的代码时,首先标识自己已经进入了Safe Region。当要发起GC时,就不用管标识为Safe Region状态的线程了。当线程要离开Safe Region时,要检查是否处于GC状态,如果是,就要继续等待,直到收到可以安全离开Safe Region的信号为止。 垃圾收集器并行与并发 并行(Parallel):指多条垃圾收集线程并行工作,但此时用户线程仍然处于等待状态。 并发(Concurrent):指用户线程与垃圾收集线程同时执行(但不一定是并行的,可能会交替执行),用户程序在继续运行,而垃圾收集程序运行于另一个CPU上。 吞吐量吞吐量就是CPU用于运行用户代码的时间与CPU总消耗时间的比值,即 吞吐量 = 运行用户代码时间 / (运行用户代码时间 + 垃圾收集时间)。 假设虚拟机总共运行了100分钟,其中垃圾收集花掉1分钟,那吞吐量就是99%。 Minor GC 和 Full GC 新生代GC(Minor GC):指发生在新生代的垃圾收集动作,因为Java对象大多都具备朝生夕灭的特性,所以Minor GC 非常频繁,一般回收速度也比较快。 老年代GC(Major GC / Full GC):指发生在老年代的GC,出现了Major GC,经常会伴随至少一次的Minor GC(但非绝对的,在Parallel Scavenge收集器的收集策略里就有直接进行Major GC的策略选择过程)。Major GC的速度一般会比Minor GC慢10倍以上。 HotSpot虚拟机的垃圾收集器对比 收集器 串行、并行、并发 新生代、老生代 算法 目标 使用场景 Serial 单线程,串行 新生代 复制算法 响应速度优先 单CPU环境下的Client模式 Serial Old 单线程,串行 老年代 标记-整理 响应速度优先 单CPU环境下的Client模式、CMS的后 ParNew 多线程,并行 新生代 复制算法 响应速度优先 多CPU环境时在Server模式下与CMS配合 Parallel Scanvenge 多线程,并行 新生代 复制算法 吞吐量优先 在后台运算而不需要太多交互的任务 Parallel Old 多线程,并行 老年代 标记-整理 吞吐量优先 在后台运算而不需要太多交互的任务 CMS 并发 老年代 标记-清除 响应速度优先 集中在互联网站或B/S系统服务端上的Java应用 G1 并发 both 复制算法+标记-整理 响应速度优先 面向服务端应用,将来替换CMS ParNew 收集器Serial收集器的多线程版本,Service模式下的首选新生代收集器,除了Serial收集器外,目前只有它能与CMS收集器配合工作。 ParNew 收集器是使用 -XX:+UseConcMarkSweepGC 选项后的默认新生代收集器,也可以使用 -XX:+UseParNewGC 选项来强制指定它。 运行示意图如下: CMS 收集器CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器。基于Mark-Sweep算法。运行过程分为四个部分: 初始标记 并发标记 重新标记 并发清除 其中,初始标记和重新标记仍然需要 Stop The World。初始标记只是标记一下 GC Roots 能直接关联到的对象,速度很快。并发标记就是进行 GC Roots Tracing 的过程,而重新标记阶段则是为了修正并发标记期间用户程序继续运作而导致标记产生变动的那一部分对象的标记记录,这个阶段的停顿时间比初始标记稍长一些,但远比并发标记的时间短。 由于整个过程中耗时最长的并发标记和并发清楚过程收集器线程都可以与用户线程一起工作,所以,从总体上来说,CMS收集器的内存回收过程是与用户线程一起并发执行的。 运行示意图如下: CMS 收集器有如下3个缺点: 对CPU资源非常敏感 因为并发阶段需要占用一个用户线程,如果CPU小于4个,则会导致用户程序的执行速度下降大于25%,如果只有2个CPU,用户程序执行速度则会下降50%,这是让人无法接收的。一般来说使用CMS收集器的服务器配置至少需要4个CPU。 无法处理浮动垃圾 在并发清理过程中产生的垃圾称为“浮动垃圾”。这些垃圾只能等待下次垃圾回收。因此,CMS 收集器不能像其他收集器那样等到老年代几乎被完全填满了再进行收集,需要预留一部分空间提供并发收集时的程序运作使用。 内存空间碎片化 CMS 收集器是基于Mark-Sweep算法,这个算法会产生内存空间碎片。CMS 收集器提供了一个 -XX:+UseCMSCompactAtFullCollection 开关参数(默认为开启),用于在CMS收集器顶不住要进行Full GC时开启内存碎片的合并整理过程。内存整理的过程是无法并发执行的,空间碎片问题没有了,但停顿时间不得不变长。 使用多个收集器配置,JVM会怎么处理?如果同时使用了四个组合配置,这时候就会报错 但是比如 -XX:+UseConcMarkSweepGC -XX:+UseParNewGC 这两个配置同时存在就不会报错。 翻看源码可知 有些配置项是可以并存的。 其实,在使用 UseConcMarkSweepGC 配置的时候,虚拟机默认开启了 UseParNewGC 所以在配置JVM时,我们尽量显式配置。比如要启用 ParNew + CMS 组合可以配置为 12-XX:+UseParNewGC-XX:+UseConcMarkSweepGC 理解GC日志 《深入理解Java虚拟机》阅读笔记系列深入理解Java虚拟机1——内存区域 深入理解Java虚拟机2——对象探秘 深入理解Java虚拟机3——垃圾回收 本文首发于我的个人博客 https://chaohang.top 作者 张小超 公众号【超超不会飞】 转载请注明出处 欢迎关注我的微信公众号 【超超不会飞】,获取第一时间的更新。","categories":[],"tags":[{"name":"Java","slug":"Java","permalink":"https://www.chaohang.top/tags/Java/"},{"name":"阅读","slug":"阅读","permalink":"https://www.chaohang.top/tags/%E9%98%85%E8%AF%BB/"},{"name":"JVM","slug":"JVM","permalink":"https://www.chaohang.top/tags/JVM/"},{"name":"深入理解Java虚拟机","slug":"深入理解Java虚拟机","permalink":"https://www.chaohang.top/tags/%E6%B7%B1%E5%85%A5%E7%90%86%E8%A7%A3Java%E8%99%9A%E6%8B%9F%E6%9C%BA/"}]},{"title":"深入理解Java虚拟机2——对象探秘","slug":"深入理解Java虚拟机2——对象探秘","date":"2020-02-24T23:24:38.000Z","updated":"2020-02-24T23:24:38.000Z","comments":true,"path":"2020/02/25/深入理解Java虚拟机2——对象探秘/","link":"","permalink":"https://www.chaohang.top/2020/02/25/%E6%B7%B1%E5%85%A5%E7%90%86%E8%A7%A3Java%E8%99%9A%E6%8B%9F%E6%9C%BA2%E2%80%94%E2%80%94%E5%AF%B9%E8%B1%A1%E6%8E%A2%E7%A7%98/","excerpt":"《深入理解Java虚拟机》第2章读书笔记","text":"《深入理解Java虚拟机》第2章读书笔记 本文介绍对象的创建过程,包括如何对内存进行分配和对象中内存的布局,还介绍了如何访问对象。本文并非原创,是《深入理解Java虚拟机》第2章的整理、总结和补充。 对象的创建 堆内存的分配取决于内存是否规整: 如果使用 Serial, ParNew 等带 Compact 过程的收集器时,系统采用的分配算法是指针碰撞(Bump the Pointer)——假设内存绝对规整,所有用过的内存都在一边,空闲的内存放在另一边,中间放着一个指针作为分界点的指示器,那所分配内存就仅仅把那个指针向空闲空间挪动一段与对象大小相等的距离; 而使用 CMS 这种基于 Mark-Sweep 算法的收集器时,通常采用空闲列表(Free List)——如果内存不规整,已使用的和空闲的内存相互交错,那就没办法简单地进行指针碰撞了,虚拟机就必须维护一个列表,记录上哪些内存块是可用的,在分配的时候从列表中找到一块足够大的空间划分给对象实例,并更新列表上的记录。 对象的创建是非常频繁的,可能出现正在给A对象分配内存,指针还没来得及修改,对象B又同时使用了原来的指针来分配内存的情况。如何保证并发下线程安全,有两种方案: 对分配内存空间的动作进行同步处理——实际上虚拟机采用CAS配上失败重试的方式保证更新操作的原子性; 把分配内存的动作按照线程划分在不同的空间之中进行,即每个线程在Java堆中预先分配一小块内存,称为本地线程分配缓冲(Thread Local Allocation Buffer, TLAB)。哪个线程要分配内存,就在哪个线程的 TLAB 上分配,只有 TLAB 用完并分配新的 TLAB 时,才需要同步锁定; 虚拟机是否使用 TLAB,可以通过-XX:+/-UseTLAB 参数来设定。 对象的内存布局在 HotSpot 虚拟机中,对象在内存中存储的布局分为3个区域: 对象头(Header) 实例数据(Instance Data) 对齐填充(Padding) 对象头包括两部分信息: 用于存储对象自身的运行时数据:哈希码,GC分代年龄,锁状态标志等; 类型指针:对象指向它的类元数据的指针。如果对象是一个Java数组,那在对象头还必须有一块用于记录数据长度的数据。 实例数据部分是对象真正存储的有效信息,也是在程序代码中所定义的各种类型的字段内容。无论是从父类继承下来的,还是在子类中定义的,都需要记录下来。 对齐填充并不是必然存在的,也没有特别的含义,仅仅起着占位符的作用。因为 HotSpot VM 的自动内存管理系统要求对象起始地址必须是8字节的整数倍,也就是说对象的大小必须是8的整数倍。而对象头刚好是8的整数倍,因此,当对象实例数据没有对齐时,就需要对齐填充来补全。 对象的访问目前有两种主流的访问方式: 句柄访问:Java堆中划分出一块内存作为句柄池,reference中存储的就是对象的句柄地址,而句柄中包含了对象实例数据(Java堆)与类型数据(方法区)各自的具体地址信息。 直接指针访问:reference中存储的直接就是实例对象(Java堆)的地址,由对象实例数据来存放指向对象类型数据(方法区)的指针。 句柄访问的优势:reference 中存储的是稳定的句柄地址,在对象被移动(垃圾收集时移动对象是非常普遍的行为)时只会改变句柄中的实例数据指针,而 reference 本身无需修改。 直接指针访问的优势:速度快,节省了一次指针定位的时间开销,由于对象的访问在Java中非常频繁,这类开销积少成多后也是非常可观的执行成本。 HotSpot 使用的是直接指针访问。 《深入理解Java虚拟机》阅读笔记系列深入理解Java虚拟机1——内存区域 深入理解Java虚拟机2——对象探秘 深入理解Java虚拟机3——垃圾回收 本文首发于我的个人博客 https://chaohang.top 作者 张小超 公众号【超超不会飞】 转载请注明出处 欢迎关注我的微信公众号 【超超不会飞】,获取第一时间的更新。","categories":[],"tags":[{"name":"Java","slug":"Java","permalink":"https://www.chaohang.top/tags/Java/"},{"name":"阅读","slug":"阅读","permalink":"https://www.chaohang.top/tags/%E9%98%85%E8%AF%BB/"},{"name":"JVM","slug":"JVM","permalink":"https://www.chaohang.top/tags/JVM/"},{"name":"深入理解Java虚拟机","slug":"深入理解Java虚拟机","permalink":"https://www.chaohang.top/tags/%E6%B7%B1%E5%85%A5%E7%90%86%E8%A7%A3Java%E8%99%9A%E6%8B%9F%E6%9C%BA/"}]},{"title":"博客优化小记","slug":"博客优化小记","date":"2020-02-23T09:51:38.000Z","updated":"2020-02-23T09:51:38.000Z","comments":true,"path":"2020/02/23/博客优化小记/","link":"","permalink":"https://www.chaohang.top/2020/02/23/%E5%8D%9A%E5%AE%A2%E4%BC%98%E5%8C%96%E5%B0%8F%E8%AE%B0/","excerpt":"趁着周末把博客优化了一下,主要优化以下几点: 阅读统计和访客统计 添加评论 升级hexo版本和NexT版本 配置SEO 静态文件cdn加速 使用gulp压缩文件 添加lazyload 添加站内搜索","text":"趁着周末把博客优化了一下,主要优化以下几点: 阅读统计和访客统计 添加评论 升级hexo版本和NexT版本 配置SEO 静态文件cdn加速 使用gulp压缩文件 添加lazyload 添加站内搜索 阅读统计和访客统计阅读统计和访客统计使用的都是不蒜子,因为使用的是NexT主题,只需要添加一下配置就可以了。 添加评论原本评论插件使用的是 disqus,但配置后了之后才发现已经被qi*ang了。改为使用 valine。valine 需要配合 leancloud 用于存储数据。 注意:cleancloud 在创建应用之前,要手动选择华东节点,因为华北节点会报一个 Code : undefined [410 POST https://avoscloud.com/1.1/classes/Comment] 的错误,无法正确加载出评论。 升级hexo版本和NexT版本之前使用的NexT版本是5.1.2,没有自带 valine ,尝试了各种方法想把 valine 相关的代码复制到这个版本里,但结果都不理想,迫不得已只能升级。 升级hexo比较简单,删掉 lock 文件,删掉 node_modules,在 package.json 里直接把版本号改了,重新 yarn install。升级后没有发现兼容性问题。 升级NexT就比较操蛋了,theme 文件是以源代码的形式从 GitHub 上 clone 下来的,没有在原本版本升级的办法。只能重新clone下来一份,再把配置文件手动迁移过去(复制粘贴),如果之前魔改过 theme 文件,升级简直不要太难。 幸好我之前没有做什么改动和个性化,只需要把原先的配置再重新配置一份就够了。 配置SEO基本上是参考下面这篇文章进行配置的,没有遇到什么棘手的问题。对 SEO 的配置对我这种流量为0的博客,怎么说呢,自己开心就好,哈哈哈。 Hexo博客Next主题SEO优化方法 静态文件cdn加速我对博客的一个重要要求,就是访问速度,所有容易拖累访问速度的功能都要思考一下是否必要。所以我的博客一直比较简洁,没有其他花花绿绿的东西,保证速度优先。下面几项都是对访问速度的优化 博客内所使用的图片都是存储在七牛云中的,图片资源无需再优化。这次添加功能引入了不蒜子和 valine,使用 chrome 可以看出这两个 js 文件的加载速度不快。 所以我把这两个文件单独复制出来,上传到七牛云里,并在博客里引用七牛云的链接。 但是发布上去后出现了 http 请求被浏览器 block 的问题。因为我的主站使用的 https,七牛使用的 http(因为七牛的https不能使用免费流量,穷哭o(╥﹏╥)o)。浏览器不允许https请求http。 机智的我灵机一动,使用 nginx 转发七牛云的请求不就行了。所以我在 nginx.conf 添加了以下配置: 123location /cdn { proxy_pass http://cdn.chaohang.top;} 把 /cdn 目录下的请求全部转发到 cdn.chaohang.top 这个子域名下,比如: https://chaohang.top/cdn/min.js 会被转发至 http://cdn.chaohang.top/cdn/min.js 这样子就可以成功绕过浏览器的限制了,因为实际的请求是通过服务器代理的。 Next 官方也提供了一些cdn加速,我们可以在配置文件里配置: 123456789101112# vendors底下全部都是可使用的cdnvendors: # Internal path prefix. _internal: lib # FancyBox # jquery: //cdn.jsdelivr.net/npm/jquery@3/dist/jquery.min.js # fancybox: //cdn.jsdelivr.net/gh/fancyapps/fancybox@3/dist/jquery.fancybox.min.js # fancybox_css: //cdn.jsdelivr.net/gh/fancyapps/fancybox@3/dist/jquery.fancybox.min.css jquery: //cdn.jsdelivr.net/npm/jquery@3/dist/jquery.min.js fancybox: //cdn.jsdelivr.net/gh/fancyapps/fancybox@3/dist/jquery.fancybox.min.js fancybox_css: //cdn.jsdelivr.net/gh/fancyapps/fancybox@3/dist/jquery.fancybox.min.css 使用gulp压缩html、css、js因为 Next 的版本7.0,使用 next-neat 会有问题,所以我选择使用 gulp 做代码压缩,也方便以后扩展。 安装依赖 npm install gulp -g npm install gulp-clean-css gulp-uglify gulp-htmlmin gulp-htmlclean gulp gulp-babel gulp-imagemin babel-core babel-preset-es2015 npm install -D @babel/core 安装后的包版本如下 12345678910111213"babel-core": "^6.26.3","babel-preset-es2015": "^6.24.1","canvas-nest.js": "^2.0.3","core-util-is": "^1.0.2","gulp": "^4.0.2","gulp-babel": "^7.0.0","gulp-clean-css": "^4.2.0","gulp-htmlclean": "^2.7.22","gulp-htmlmin": "^5.0.1","gulp-imagemin": "^7.1.0","gulp-uglify": "^3.0.2","@babel/core": "^7.8.4" 在执行过程中可能会遇到一些包版本问题,如有问题请参照我给出的包版本进行调整。 在项目根目录下新建 gulpfile.js 文件 1234567891011121314151617181920212223242526272829303132333435363738var gulp = require('gulp');var babel = require('gulp-babel');var cleancss = require('gulp-clean-css');var uglify = require('gulp-uglify');var htmlmin = require('gulp-htmlmin');var htmlclean = require('gulp-htmlclean');// 压缩 public 目录 cssfunction css() { return gulp.src('./public/**/*.css') .pipe(cleancss()) .pipe(gulp.dest('./public'));}// 压缩 public 目录 htmlfunction html() { return gulp.src('./public/**/*.html') .pipe(htmlclean()) .pipe(htmlmin({ removeComments: true, minifyJS: true, minifyCSS: true, minifyURLs: true, })) .pipe(gulp.dest('./public'))}// 压缩 public/js 目录 jsfunction js() { return gulp.src('./public/**/*.js') .pipe(babel({ presets: ['es2015'] })) .pipe(uglify()) .pipe(gulp.dest('./public'));}// 执行 gulp 命令时执行的任务gulp.task('css', css)gulp.task('html', html)gulp.task('js', js)gulp.task('default', gulp.series(gulp.parallel('html', 'css', 'js'))); 在生成代码文件后,使用 gulp 命令压缩文件: 123hexo ggulp 添加lazyloadlazylod可以在用户不查看的时候,不加载相关部分,从而提升网站加载速度。 先是在next目录里clone仓库: git clone https://github.com/theme-next/theme-next-jquery-lazyload source/lib/jquery_lazyload 再把配置里的开关打开: lazyload: true 添加站内搜索Next集成了站内搜索功能,可以先安装依赖 npm install hexo-generator-searchdb --save 然后设置主题配置文件: 1234567891011local_search: enable: true # If auto, trigger search by changing input. # If manual, trigger search by pressing enter key or search button. trigger: auto # Show top n results per article, show all results by setting to -1 top_n_per_article: 3 # Unescape html strings to the readable one. unescape: false # Preload the search data when the page loads. preload: false 以上就是全部优化过程了,即使没人访问的站,也要好好的优化,这就是一个工(搬)程(砖)师(工)的坚持。 本文首发于我的个人博客 https://chaohang.top 作者张小超 转载请注明出处 欢迎关注我的微信公众号 【超超不会飞】,获取第一时间的更新。","categories":[],"tags":[{"name":"随笔","slug":"随笔","permalink":"https://www.chaohang.top/tags/%E9%9A%8F%E7%AC%94/"},{"name":"博客","slug":"博客","permalink":"https://www.chaohang.top/tags/%E5%8D%9A%E5%AE%A2/"}]},{"title":"深入理解Java虚拟机1——内存区域","slug":"深入理解Java虚拟机1——内存区域","date":"2020-02-20T23:18:24.000Z","updated":"2020-02-20T23:18:24.000Z","comments":true,"path":"2020/02/21/深入理解Java虚拟机1——内存区域/","link":"","permalink":"https://www.chaohang.top/2020/02/21/%E6%B7%B1%E5%85%A5%E7%90%86%E8%A7%A3Java%E8%99%9A%E6%8B%9F%E6%9C%BA1%E2%80%94%E2%80%94%E5%86%85%E5%AD%98%E5%8C%BA%E5%9F%9F/","excerpt":"《深入理解Java虚拟机》第2章读书笔记","text":"《深入理解Java虚拟机》第2章读书笔记 本文介绍了JVM内存的各个区域和他们的用途。本文并非原创,是《深入理解Java虚拟机》第2章的整理、总结和补充。 运行时内存区域Java虚拟机在执行Java程序的过程中会把它所管理的内存划分为若干个不同的区域。这些区域都有各自的用途,以及创建和销毁的时间,有的区域随着虚拟机进程的启动而存在,有些区域则依赖用户线程的启动和结束而建立和销毁。 下图为《Java虚拟机规范》规定的几个区域,不同虚拟机实现起来略有区别: 程序计数器 保存线程当前工作状态。 程序计数器(Program Counter Register)是一块较小的内存空间,它可以看做是当前线程所执行的字节码的行号指示器。在虚拟机的概念模型里(仅是概念模型,各种虚拟机可能会通过一些更高效的方式去实现),字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令,分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成。 由于Java虚拟机的多线程是通过线程轮流切换并分配处理器执行时间的方式来实现的,在任何一个确定的时刻,一个处理器(对于多核处理器来说是一个内核)都只会执行一条线程中的指令。因此,为了线程切换后能恢复到正确的执行位置,每条线程都需要有一个独立的程序计数器,各线程之间计数器互不影响,独立存储,我们称这类内存区域为“线程私有”的内存。 如果线程正在执行的是一个Java方法,这个计数器记录的是正在执行的虚拟机字节码指令的地址。 如果正在执行的是Native方法,这个计数器值则为空(Undefined)。 此内存区域是唯一一个在Java虚拟机规范中没有规定任何OutOfMemoryError情况的区域。 Java虚拟机栈 每个方法执行时都会创建一个栈帧,栈帧里有个局部变量表存储着基本数据类型和对象引用。 Java虚拟机栈也是线程私有的,它的生命周期与线程相同。 虚拟机栈描述的是Java方法执行的内存模型:每个方法在执行的同时都会创建一个栈帧用于存储局部变量表、操作数栈、动态链接、方法出口等信息。每一个方法从调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中入栈到出栈的过程。 Java内存经常被粗糙的分为堆内存和栈内存,其中栈内存就是指的虚拟机栈。 局部变量表存放了编译期可知的各种基本数据类型、对象引用和returnAdress类型。 其中64位的 long 和 double 类型的数据会占用2个局部变量空间,其余的数据类型只占用1个。 局部变量表所需的内存空间在编译期间内完成分配,当进入一个方法时,这个方法需要在帧中分配多大的局部变量空间是完全确定的,在方法运行期间不会改变局部变量表的大小。 如果请求的栈深度大于虚拟机所允许的深度(比如无限深度的递归),将抛出 StackOverflowError 异常;如果虚拟机栈可以动态扩展(大部分虚拟机都可以),如果无法申请到足够的内存,就会抛出 OutOfMemoryError 异常。 本地方法栈 与虚拟机栈相似,只是为Native方法服务。 虚拟机栈为虚拟机执行Java方法(也就是字节码)服务。本地方法栈则为虚拟机使用到的Native方法服务。 在虚拟机规范中对本地方法栈中方法使用的语言、使用方式与数据结构没有强制规定,虚拟机可以自由实现它,甚至可以把本地方法栈和虚拟机栈合二为一。 和Java虚拟机栈一样,也会抛出StackOverflowError 和 OutOfMemoryError 异常。 Java堆 存放对象实例,GC主要管理的区域。 Java堆是Java虚拟机所管理的内存中最大的一块。Java堆是被所有线程共享的一块内存区域,在虚拟机启动时创建。此内存区域的唯一目的就是存放对象实例。 Java堆是垃圾收集器管理的主要区域,也被称为“GC堆”。 Java堆可以处于物理上不连续的内存空间中,只要逻辑上是连续的即可,就像我们的磁盘空间一样。堆的内存大小是可扩展的(通过-Xmx 和 -Xms)。 如果在堆中没有内存完成实例分配,并且堆也无法再扩展时,将抛出 OutOfMemoryError 异常 方法区 存储类信息,常量,静态变量,JIT代码。JDK1.8中HotSpot用元空间(MetaSpace)取代永久代。 方法区是各个线程共享的内存区域,它用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据(不容易发生变化的数据)。 方法区也被称为永久代(诨名),因为这个区域可以选择不实现垃圾收集。但大部分虚拟机还是会实现垃圾收集,只是回收的条件极为苛刻。 当方法区无法满足内存分配需求时,将抛出 OutOfMemoryError 异常。 直接内存缓冲区Buffer使用的堆外内存,它可以使用native函数库直接分配堆外内存,然后通过一个存储在Java堆中的DirectByteBuffer对象作为这块内存的引用进行操作。 总结本文介绍了Java虚拟机内存中的几个区域,分别介绍了不同区域各自的作用和特点。 线程隔离的区域: 程序计数器:保存线程当前工作状态。 Java虚拟机栈:每个方法执行时都会创建一个栈帧,栈帧里有个局部变量表存储着基本数据类型和对象引用。 本地方法栈:与虚拟机栈相似,只是为Native方法服务。 线程共享的区域: Java堆:存放对象实例,GC主要管理的区域。 方法区:存储类信息,常量,静态变量,JIT代码。 《深入理解Java虚拟机》阅读笔记系列深入理解Java虚拟机1——内存区域 深入理解Java虚拟机2——对象探秘 深入理解Java虚拟机3——垃圾回收 本文首发于我的个人博客 https://chaohang.top 作者 张小超 公众号【超超不会飞】 转载请注明出处 欢迎关注我的微信公众号 【超超不会飞】,获取第一时间的更新。","categories":[],"tags":[{"name":"Java","slug":"Java","permalink":"https://www.chaohang.top/tags/Java/"},{"name":"阅读","slug":"阅读","permalink":"https://www.chaohang.top/tags/%E9%98%85%E8%AF%BB/"},{"name":"JVM","slug":"JVM","permalink":"https://www.chaohang.top/tags/JVM/"},{"name":"深入理解Java虚拟机","slug":"深入理解Java虚拟机","permalink":"https://www.chaohang.top/tags/%E6%B7%B1%E5%85%A5%E7%90%86%E8%A7%A3Java%E8%99%9A%E6%8B%9F%E6%9C%BA/"}]},{"title":"Java源码系列3——LinkedHashMap","slug":"Java源码系列3——LinkedHashMap","date":"2020-02-16T11:25:24.000Z","updated":"2020-02-16T11:25:24.000Z","comments":true,"path":"2020/02/16/Java源码系列3——LinkedHashMap/","link":"","permalink":"https://www.chaohang.top/2020/02/16/Java%E6%BA%90%E7%A0%81%E7%B3%BB%E5%88%973%E2%80%94%E2%80%94LinkedHashMap/","excerpt":"什么是LinkedHashMap?LinkedHashMap 是 HashMap 的有序实现。LinkedHashMap 用一条双向链表来维护顺序,迭代的时候也使用自己实现的迭代器。","text":"什么是LinkedHashMap?LinkedHashMap 是 HashMap 的有序实现。LinkedHashMap 用一条双向链表来维护顺序,迭代的时候也使用自己实现的迭代器。 12345678910111213141516171819public static void main(String[] args) { HashMap<String, Integer> h = new HashMap<>(33); h.put(\"one\", 1); h.put(\"two\", 2); h.put(\"three\", 3); h.put(\"four\", 4); for (String key : h.keySet()) { System.out.println(\"key:\" + key + \"value:\" + h.get(key)); } LinkedHashMap<String, Integer> lh = new LinkedHashMap<>(33); lh.put(\"one\", 1); lh.put(\"two\", 2); lh.put(\"three\", 3); lh.put(\"four\", 4); for (String key : lh.keySet()) { System.out.println(\"key:\" + key + \"value:\" + lh.get(key)); }} 输出123456789key:twovalue:2key:threevalue:3key:fourvalue:4key:onevalue:1key:onevalue:1key:twovalue:2key:threevalue:3key:fourvalue:4 底层数组结构HashMap的底层是由数组,链表,红黑树组成的。数组用来存储节点,当出现哈希碰撞时使用链表存储,当链表超过一定长度后会优化成红黑树。 LinkedHashMap 的底层除了继承自 HashMap 的数组,链表,红黑树,还多了链接所有节点的双向链表(图中红色和绿色箭头),用于存储各个节点的顺序。 Entry的继承关系LinkedHashMap.Entry 继承了 HashMap.Node,多维护了 before 和 after 两个指针,这两个属性指向该Entry的前一个Entry和后一个Entry,也就是那条用于存储顺序的双向链表。 123456static class Entry<K,V> extends HashMap.Node<K,V> { Entry<K,V> before, after; Entry(int hash, K key, V value, Node<K,V> next) { super(hash, key, value, next); }} 但是 LinkedHashMap 中确没有覆写 HashMap 中 TreeNode 的代码,那红黑树中各个节点的顺序是如何存储的。 我们可以从 HashMap.TreeNode 的继承关系中找出端倪: 呦吼,这一小家子也真够乱的,子类继承了父类的内部类,父类的内部类又继承了子类的内部类,上演一出鸡生蛋,蛋生鸡的戏码。 1234567891011// 继承了 LinkedHashMap.Entrystatic final class TreeNode<K,V> extends LinkedHashMap.Entry<K,V> { TreeNode<K,V> parent; // red-black tree links TreeNode<K,V> left; TreeNode<K,V> right; TreeNode<K,V> prev; // needed to unlink next upon deletion boolean red; TreeNode(int hash, K key, V val, Node<K,V> next) { super(hash, key, val, next); }} 为什么 HashMap.TreeNode 要继承 LinkedHashMap.Entry,继承过来的 before 和 after 指针在 HashMap 中也没有被用到,何不直接继承 HashMap.Node? 这样的继承关系其实并不是为 HashMap 设计的,在 HashMap 中确实没什么用。但在 LinkedHashMap 中,就可以直接使用继承过来的 HashMap.TreeNode,因为 TreeNode 这个类通过继承已经拥有了 before 和 after 指针。 这就是为什么,LinkedHashMap 中有一个继承了 HashMap.Node 的内部类,却没有继承 HashMap.TreeNode 的内部类。 链表的创建过程链表的创建过程是在第一个元素插入的时候才开始的,一开始链表的头部(head)和尾巴(tail)都为null。 LinkedHashMap 没有覆写父类的put方法,元素的插入流程基本相同,只是 HashMap 插入的是 Node 类型的节点,LinkedHashMap 插入的是 Entry 类型的节点,并且更新链表。 那么 LinkedHashMap 是怎么插入节点,并且更新链表的呢? 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152// HashMap 中实现public V put(K key, V value) { return putVal(hash(key), key, value, false, true);}// HashMap 中实现final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) { Node<K,V>[] tab; Node<K,V> p; int n, i; // 桶为空时初始化桶 if ((tab = table) == null || (n = tab.length) == 0) n = (tab = resize()).length; // 取模得到节点在桶中的索引位置,并且该位置没有元素,直接插入 if ((p = tab[i = (n - 1) & hash]) == null) tab[i] = newNode(hash, key, value, null); // 哈希碰撞了,本节不介绍,可以看上一篇讲 HashMap 的文章 else { // ... 省略部分代码 } ++modCount; if (++size > threshold) resize(); afterNodeInsertion(evict); return null;}// HashMap 中实现的 newNodeNode<K,V> newNode(int hash, K key, V value, Node<K,V> next) { return new Node<>(hash, key, value, next);}// LinkedHashMap 中覆写的 newNodeNode<K,V> newNode(int hash, K key, V value, Node<K,V> e) { LinkedHashMap.Entry<K,V> p = new LinkedHashMap.Entry<K,V>(hash, key, value, e); linkNodeLast(p); return p;}// LinkedHashMap 中实现private void linkNodeLast(LinkedHashMap.Entry<K,V> p) { LinkedHashMap.Entry<K,V> last = tail; tail = p; // 如果链表为空,头部和尾部都赋值为p if (last == null) head = p; // 把新插入的节点放在链表尾部 else { p.before = last; last.after = p; }} 从代码里可以很明显的看出,LinkedHashMap 中索引的计算,桶的赋值,哈希碰撞时链表或者红黑树的创建,都使用的 HashMap 的实现。LinkedHashMap 只需要覆写节点的创建,并且在创建节点的时候,更新储存顺序的链表。真的是把复用利用到了极致。 节点的删除与插入操作一样,LinkedHashMap 也是使用的父类的删除操作,然后覆写了回调方法 afterNodeRemoval,用于维护双向链表。 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647// HashMap 中实现final Node<K,V> removeNode(int hash, Object key, Object value, boolean matchValue, boolean movable) { Node<K,V>[] tab; Node<K,V> p; int n, index; if ((tab = table) != null && (n = tab.length) > 0 && (p = tab[index = (n - 1) & hash]) != null) { Node<K,V> node = null, e; K k; V v; if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k)))) node = p; else if ((e = p.next) != null) { // ... 省略部分代码 } if (node != null && (!matchValue || (v = node.value) == value || (value != null && value.equals(v)))) { if (node instanceof TreeNode) ((TreeNode<K,V>)node).removeTreeNode(this, tab, movable); else if (node == p) tab[index] = node.next; else p.next = node.next; ++modCount; --size; // 删除后回调 afterNodeRemoval(node); return node; } } return null;}// LinkedHashMap 中覆写void afterNodeRemoval(Node<K,V> e) { // unlink LinkedHashMap.Entry<K,V> p = (LinkedHashMap.Entry<K,V>)e, b = p.before, a = p.after; p.before = p.after = null; // 如果b为空,则为头节点 if (b == null) head = a; else b.after = a; // 如果a为空,则为尾节点 if (a == null) tail = b; else a.before = b;} 访问顺序的维护如果我们在初始化 LinkedHashMap 时,把 accessOrder 参数设为 true,那么我们不仅在插入的时候会维护链表,在访问节点的时候也会维护链表。 当我们调用 get, getOrDefault, replace 等方法时,会更新链表,把访问的节点移动到链表尾部。 1234567891011121314151617181920212223242526272829303132333435363738394041// LinkedHashMap 中覆写public V get(Object key) { Node<K,V> e; // 调用了 HashMap 中的 getNode 方法 if ((e = getNode(hash(key), key)) == null) return null; // 如果accessOrder为true,调用afterNodeAccess if (accessOrder) afterNodeAccess(e); return e.value;}// LinkedHashMap 中覆写void afterNodeAccess(Node<K,V> e) { // move node to last LinkedHashMap.Entry<K,V> last; // 如果本来就在尾部,就不需要更新 if (accessOrder && (last = tail) != e) { LinkedHashMap.Entry<K,V> p = (LinkedHashMap.Entry<K,V>)e, b = p.before, a = p.after; p.after = null; // 如果b为空,则为头部 if (b == null) head = a; else b.after = a; if (a != null) a.before = b; // 尾部不会为空,不知为何要多一个判断 else last = b; if (last == null) head = p; else { // 把尾部赋值为p p.before = last; last.after = p; } tail = p; ++modCount; }} 使用测试代码体验一下效果 1234567891011public static void main(String[] args) { LinkedHashMap<String, Integer> lh = new LinkedHashMap<>(33, 0.75f, true); lh.put(\"one\", 1); lh.put(\"two\", 2); lh.put(\"three\", 3); lh.put(\"four\", 4); lh.get(\"two\"); for (String key : lh.keySet()) { System.out.println(\"key:\" + key + \"value:\" + lh.get(key)); }} 竟然报错了 看一下 LinkedHashMap 覆写的迭代器代码 12345678910final LinkedHashMap.Entry<K,V> nextNode() { LinkedHashMap.Entry<K,V> e = next; if (modCount != expectedModCount) throw new ConcurrentModificationException(); if (e == null) throw new NoSuchElementException(); current = e; next = e.after; return e;} ConcurrentModificationException 这个报错是为了防止并发条件下,遍历的同时链表发生变化。因为我们在遍历的时候又调用了 get 方法,导致链表发生变化,才会抛这个错。 accessOrder 为 true 时的正确遍历姿势如下,使用 LinkedHashMap 覆写forEach 方法,就不会在读取值的时候修改顺序链表了。 123lh.forEach((String k, Integer v) -> { System.out.println(\"key:\" + k + \", value:\" + v);}); 使用 LinkedHashMap 实现简单的 LRU LRU 全称 Least Recently Used,也就是最近最少使用的意思,是一种内存管理算法,该算法最早应用于 Linux 操作系统。 这个算法基于一种假设:长期不被使用的数据,在未来被用到的几率也不大。因此,当数据所占内存达到一定阈值时,我们要移除最近最少被使用的数据。 下面我们介绍一下前置知识。 afterNodeInsertion 是一个回调方法,在插入元素的时候回调。LinkedHashMap 覆写了这个方法,主要用来判断是否需要将链表的 head 移除。 1234567891011121314// LinkedHashMap 中覆写void afterNodeInsertion(boolean evict) { // possibly remove eldest LinkedHashMap.Entry<K,V> first; // 根据条件判断是否移除链表的head节点 if (evict && (first = head) != null && removeEldestEntry(first)) { K key = first.key; removeNode(hash(key), key, null, false, true); }}// LinkedHashMap 中实现,默认返回falseprotected boolean removeEldestEntry(Map.Entry<K,V> eldest) { return false;} 下面我们将继承 LinkedHashMap,通过覆写 removeEldestEntry,达到当 Map 的节点个数超过指定阈值时,删除最少访问的节点。从而实现 LRU 缓存策略。 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647public class SimpleCache<K, V> extends LinkedHashMap<K, V> { private static final int MAX_NODE_NUM = 100; private int limit; public SimpleCache(){ this(MAX_NODE_NUM); } public SimpleCache(int limit) { super(limit, 0.75f, true); this.limit = limit; } /** * 判断节点数是否超出限制 * @param eldest * @return boolean */ @Override protected boolean removeEldestEntry(Map.Entry eldest) { return size() > limit; } /** * 测试 */ public static void main(String[] args) { SimpleCache<Integer, Integer> cache = new SimpleCache<>(3); for (int i = 0; i < 10; i++) { cache.put(i, i * i); } System.out.println(\"插入10个键值对后,缓存内容:\"); System.out.println(cache); System.out.println(\"访问键值为7的节点后,缓存的内容:\"); cache.get(7); System.out.println(cache); System.out.println(\"插入键值为1的键值对后,缓存的内容:\"); cache.put(1, 1); System.out.println(cache); }} 测试结果如下: 总结本文围绕 LinkedHashMap 如何维护存储顺序的双向链表展开,介绍了 LinkedHashMap 和 HashMap 节点类的继承关系,介绍了新增,删除,访问时,LinkedHashMap 如何在复用 HashMap 的同时,维护双向链表。最后通过继承 LinkedHashMap 很简单的实现了 LRU 缓存策略。 全文的代码量较多,但都较为好理解。理解JDK的设计思路,探寻背后的实现原理,也是一件很有趣的事。 本文讨论的源代码都基于JDK1.8版本。 参考资料LinkedHashMap 源码详细分析(JDK1.8) 【Java入门提高篇】Day28 Java容器类详解(十)LinkedHashMap详解 源码系列文章Java源码系列1——ArrayList Java源码系列2——HashMap Java源码系列3——LinkedHashMap 本文首发于我的个人博客 https://chaohang.top 作者张小超 转载请注明出处 欢迎关注我的微信公众号 【超超不会飞】,获取第一时间的更新。","categories":[],"tags":[{"name":"Java","slug":"Java","permalink":"https://www.chaohang.top/tags/Java/"},{"name":"Java源码系列","slug":"Java源码系列","permalink":"https://www.chaohang.top/tags/Java%E6%BA%90%E7%A0%81%E7%B3%BB%E5%88%97/"}]},{"title":"Java源码系列2——HashMap","slug":"Java源码系列2——HashMap","date":"2020-02-12T07:58:48.000Z","updated":"2020-02-12T07:58:48.000Z","comments":true,"path":"2020/02/12/Java源码系列2——HashMap/","link":"","permalink":"https://www.chaohang.top/2020/02/12/Java%E6%BA%90%E7%A0%81%E7%B3%BB%E5%88%972%E2%80%94%E2%80%94HashMap/","excerpt":"HashMap 的源码很多也很复杂,本文只是摘取简单常用的部分代码进行分析。能力有限,欢迎指正。","text":"HashMap 的源码很多也很复杂,本文只是摘取简单常用的部分代码进行分析。能力有限,欢迎指正。 HASH 值的计算前置知识——位运算按位异或操作符^:1^1=0, 0^0=0, 1^0=1, 值相同为0,值不同为1。按位异或就是对二进制中的每一位进行异或运算。 1234 1111 0000 1111 1110^ 1111 1111 0000 1111______________________ 0000 1111 1111 0001 按位右移补零操作符>>>:左操作数按右操作数指定的位数右移,移动得到的空位以零填充。 1234 1110 1101 1001 1111>>> 4 ___________________________ 0000 1110 1101 1001 扰动函数为什么要做扰动?理论上哈希值是一个int类型,如果直接拿哈希值做下标的话,考虑到2进制32位带符号的int表值范围从-2147483648到2147483648。前后加起来大概40亿的映射空间。这么大的数组,内存是存不下的,所以这个散列值是不能直接拿来用的。用之前还要先做对数组的长度取模运算,得到的余数才能用来访问数组下标。 因为只取最后几位,所以哈希碰撞的可能性大大增加,这时候扰动函数的价值就来了。 扰动计算 先调用hashCode()方法得出hash值,再进行扰动操作。 右位移16位,正好是32bit的一半(int 是32位的),自己的高半区和低半区做异或,就是为了混合原始哈希码的高位和低位,以此来加大低位的随机性。而且混合后的低位掺杂了高位的部分特征,这样高位的信息也变相保留下来。 1234static final int hash(Object key) { int h; return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);} 取模,计算出下标在计算下标的时候,让列表长度对哈希值做取模操作,让计算出来的哈希值在列表范围内,n 为list长度 1i = (n - 1) & hash 为什么HashMap的数组长度要取2的整次幂因为这样(数组长度 - 1)正好相当于一个“低位掩码”。&操作的结果就是散列值的高位全部归零,只保留低位值,用来做数组的下标访问。以初始长度16为例,16-1=15,2进制表示是0000 1111。和某散列值做&操作如下: 1234 1010 0011 0110 1111 0101& 0000 0000 0000 0000 1111____________________________ 0000 0000 0000 0000 0101 是什么存入了 tableHashMap存入table的值并不只有value,而是构造成一个 Node 对象实例存入 table。 Node对象里有:hash, key, value, next(哈希冲突时的链表) 理论最大容量int MAXIMUM_CAPACITY = 1 << 30; 2的30次方 负载因子负载因子是用来计算负载容量(所能容纳的最大Node个数)的,当前list长度 length,负载因子 loadFactor 负载容量计算公式为: threshold = length * loadFactor 默认负载因子为 0.75。也就是说,当Node个数达到当前list长度的75%时,就要进行扩容,否则会增加哈希碰撞的可能性。负载因子的作用是在空间和时间效率上取得一个平衡。 float DEFAULT_LOAD_FACTOR = 0.75f 扩容做了哪些操作 创建一个新的Entry空数组,长度是原数组的2倍。当Node个数超过负载容量时,进行扩容。 old << 1 左移一位相当于 old * 2。 重新Hash 遍历原Entry数组,把所有的Entry重新Hash到新数组中。 为什么要重新hash?因为长度扩大以后,hash值也随之改变(数组下标的计算是数组长度对hashcode进行取模)。 这样就可以把原先哈希冲突的链表拉平,使数组变得稀疏。 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798final Node<K,V>[] resize() { // 保存现有的数组 Node<K,V>[] oldTab = table; // 保存现有的数组长度 int oldCap = (oldTab == null) ? 0 : oldTab.length; // 保存现有的负载容量 int oldThr = threshold; int newCap, newThr = 0; if (oldCap > 0) { // 如果现有容量已经超过最大值了,就没办扩容了,只好随你碰撞了 if (oldCap >= MAXIMUM_CAPACITY) { threshold = Integer.MAX_VALUE; return oldTab; } // 原有容量左移一位,相当于 oldCap * 2 else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY && oldCap >= DEFAULT_INITIAL_CAPACITY) // 负载容量也扩大一倍 newThr = oldThr << 1; // double threshold } else if (oldThr > 0) // initial capacity was placed in threshold newCap = oldThr; else { // zero initial threshold signifies using defaults newCap = DEFAULT_INITIAL_CAPACITY; newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY); } // 负载容量为0,根据数组大小和负载因子计算出来 if (newThr == 0) { float ft = (float)newCap * loadFactor; newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ? (int)ft : Integer.MAX_VALUE); } threshold = newThr; @SuppressWarnings({\"rawtypes\",\"unchecked\"}) Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap]; table = newTab; // 遍历数组中所有元素,重新进行hash if (oldTab != null) { for (int j = 0; j < oldCap; ++j) { Node<K,V> e; if ((e = oldTab[j]) != null) { // 删除旧索引位置的值 oldTab[j] = null; if (e.next == null) // 给新的索引位置赋值 newTab[e.hash & (newCap - 1)] = e; else if (e instanceof TreeNode) /** * 这个地方可以参考源码系列的第四篇 * 《Java源码系列4——HashMap扩容时究竟对链表和红黑树做了什么?》 */ ((TreeNode<K,V>)e).split(this, newTab, j, oldCap); else { // 优化链表 // 把原有链表拆成两个链表 // 链表1存放在低位(原索引位置) Node<K,V> loHead = null, loTail = null; // 链表2存放在高位(原索引 + 旧数组长度) Node<K,V> hiHead = null, hiTail = null; Node<K,V> next; do { next = e.next; /** * 链表1 * 这个位运算的原理可以参考源码系列的第四篇 * 《Java源码系列4——HashMap扩容时究竟对链表和红黑树做了什么?》 */ if ((e.hash & oldCap) == 0) { if (loTail == null) loHead = e; else loTail.next = e; loTail = e; } // 链表2 else { if (hiTail == null) hiHead = e; else hiTail.next = e; hiTail = e; } } while ((e = next) != null); // 链表1存放于原索引位置 if (loTail != null) { loTail.next = null; newTab[j] = loHead; } // 链表2存放原索引加上旧数组长度的偏移量 if (hiTail != null) { hiTail.next = null; newTab[j + oldCap] = hiHead; } } } } } return newTab;} 树化改造链表长度太长,会被改造成红黑树。 当链表的长度超过MIN_TREEIFY_CAPACITY 最大树化临界值,就会进行树化改造。 12345678final void treeifyBin(Node<K,V>[] tab, int hash) { int n, index; Node<K,V> e; if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY) resize(); else if ((e = tab[index = (n - 1) & hash]) != null) { ... }} 为什么要树化? 本质上是个安全问题。因为链表查询影响性能,如果有人恶意造成哈希碰撞,就会构成哈希碰撞拒绝服务攻击,服务端CPU被大量占用用于链表查询,造成服务变慢或不可用。 源码系列文章Java源码系列1——ArrayList Java源码系列2——HashMap Java源码系列3——LinkedHashMap 参考Java 8系列之重新认识HashMap JDK 源码中 HashMap 的 hash 方法原理是什么?胖君的回答 HashMap 源码详细分析(JDK1.8) 本文首发于我的个人博客 https://chaohang.top 作者张小超 转载请注明出处 欢迎关注我的微信公众号 【超超不会飞】,获取第一时间的更新。","categories":[],"tags":[{"name":"Java","slug":"Java","permalink":"https://www.chaohang.top/tags/Java/"},{"name":"Java源码系列","slug":"Java源码系列","permalink":"https://www.chaohang.top/tags/Java%E6%BA%90%E7%A0%81%E7%B3%BB%E5%88%97/"}]},{"title":"Java源码系列1——ArrayList","slug":"Java源码系列1——ArrayList","date":"2020-02-11T05:58:48.000Z","updated":"2020-02-11T05:58:48.000Z","comments":true,"path":"2020/02/11/Java源码系列1——ArrayList/","link":"","permalink":"https://www.chaohang.top/2020/02/11/Java%E6%BA%90%E7%A0%81%E7%B3%BB%E5%88%971%E2%80%94%E2%80%94ArrayList/","excerpt":"本文简单介绍了 ArrayList,并对扩容,添加,删除操作的源代码做分析。能力有限,欢迎指正。","text":"本文简单介绍了 ArrayList,并对扩容,添加,删除操作的源代码做分析。能力有限,欢迎指正。 ArrayList是什么?ArrayList 就是数组列表,主要用来装载数据。底层实现是数组 Object[] elementData,当我们装载的是基本数据类型 int, long, boolean, shot…的时候我们只能存储他们对应的包装类型。 与它类似的是 LinkedList,和 LinkedList 相比,它的查找和访问元素的速度较快,但新增,删除的速度较慢。 线程安全吗?线程不安全。 正常使用场景中,ArrayList 都是用来查询,不会涉及太频繁的增删,如果涉及频繁的增删,可以使用 LinkedList。如果需要线程安全就使用 Vector。 Vector 是 ArrayList 的线程安全版本,实现方式就是在所有方法加上synchronized,性能较差。 如何扩容?因为数组的大小是固定,当容量超出了现有数组的大小,就需要进行扩容。 1234567891011121314private void grow(int minCapacity) { // overflow-conscious code int oldCapacity = elementData.length; // 每次扩大原有容量的一半 int newCapacity = oldCapacity + (oldCapacity >> 1); // 如果扩大一半后还是无法满足,则使用minCapacity if (newCapacity - minCapacity < 0) newCapacity = minCapacity; // 如果超过最大size,则获取最大容量的数组 if (newCapacity - MAX_ARRAY_SIZE > 0) newCapacity = hugeCapacity(minCapacity); // minCapacity is usually close to size, so this is a win: elementData = Arrays.copyOf(elementData, newCapacity);} 为什么说ArrayList插入效率低?原因有两点: 新增就要检测容量够不够,如果不够就需要扩容 尾部新增比较快,如果是在数组头部或者中部新增就会慢很多,因为要把后面的元素全部往后移一位 把元素往后移一位使用的是复制 System.arraycopy(),它是native方法(java定义了接口,其他语言进行实现),所以比较快 1234567891011121314151617181920212223242526/** * 添加在尾部 */public boolean add(E e) { // 检查容量 ensureCapacityInternal(size + 1); // 添加在尾部 elementData[size++] = e; return true;}/** * 按指定位置添加 */public void add(int index, E element) { // 检查index rangeCheckForAdd(index); // 检查容量 ensureCapacityInternal(size + 1); // index后面的元素全部往后移一位 System.arraycopy(elementData, index, elementData, index + 1, size - index); elementData[index] = element; size++;} 删除元素效率如何?效率和新增差不多,都是要移动元素,但是不需要检查容量和扩容。 12345678910111213141516public E remove(int index) { // 检查index rangeCheck(index); modCount++; E oldValue = elementData(index); int numMoved = size - index - 1; if (numMoved > 0) // index后面的元素全部往前移一位 System.arraycopy(elementData, index+1, elementData, index, numMoved); elementData[--size] = null; // clear to let GC do its work return oldValue;} 适合做队列吗?非常不适合。 队列是FIFO,在尾巴进,头部出,出的时候需要移动后面所有数据,效率很低。链表比较适合做队列。 new ArrayList<>(18) 会不会初始化数组大小?不会初始化数组大小!!! 这是Java Bug。 而且将构造函数与initialCapcity结合使用,然后使用set()方法会抛出异常。 12345public static void main(String[] args) { ArrayList<Integer> a = new ArrayList<>(12); System.out.println(a.size()); a.set(3, 3);} 总结 底层实现是数组 Object[] elementData 查找和访问元素的速度较快,但新增,删除的速度较慢 线程不安全 每次扩容原有数组大小的一半 源码系列文章Java源码系列1——ArrayList Java源码系列2——HashMap Java源码系列3——LinkedHashMap 本文首发于我的个人博客 https://chaohang.top 作者张小超 转载请注明出处 欢迎关注我的微信公众号 【超超不会飞】,获取第一时间的更新。","categories":[],"tags":[{"name":"Java","slug":"Java","permalink":"https://www.chaohang.top/tags/Java/"},{"name":"Java源码系列","slug":"Java源码系列","permalink":"https://www.chaohang.top/tags/Java%E6%BA%90%E7%A0%81%E7%B3%BB%E5%88%97/"}]},{"title":"理解Spring中的IoC和DI","slug":"理解Spring中的IoC和DI","date":"2019-12-15T04:44:36.000Z","updated":"2019-12-15T04:44:36.000Z","comments":true,"path":"2019/12/15/理解Spring中的IoC和DI/","link":"","permalink":"https://www.chaohang.top/2019/12/15/%E7%90%86%E8%A7%A3Spring%E4%B8%AD%E7%9A%84IoC%E5%92%8CDI/","excerpt":"什么是IoC和DI IoC(Inversion of Control 控制反转):是一种面向对象编程中的一种设计原则,用来降低计算机代码之间的耦合度。其基本思想是:借助于“第三方”实现具有依赖关系的对象之间的解耦。 DI(Dependence Injection 依赖注入):将实例变量传入到一个对象中去(Dependency injection means giving an object its instance variables)。","text":"什么是IoC和DI IoC(Inversion of Control 控制反转):是一种面向对象编程中的一种设计原则,用来降低计算机代码之间的耦合度。其基本思想是:借助于“第三方”实现具有依赖关系的对象之间的解耦。 DI(Dependence Injection 依赖注入):将实例变量传入到一个对象中去(Dependency injection means giving an object its instance variables)。 控制反转是一种思想 依赖注入是一种设计模式 IoC框架使用依赖注入作为实现控制反转的方式 为什么需要在没有IoC之前,我们要在A类中使用B类,就要在A类中new出B类的实例,这样A类和B类之间就出现了耦合。 123public class A { private B b = new B();} 使用了IoC之后,我们就把实例化这样操作交给框架去帮我们做了。 Spring 中的IoC容器是Spring的核心,Spring容器负责创建应用程序中的bean并通过DI来协调这些对象之间的关系。 Spring容器并不是只有一个,Spring自带多个容器的实现,可以归纳为两种不同的类型: bean工厂(BeanFactory),最简单的容器,提供基本的DI支持。 应用上下文(ApplicationContext),继承了BeanFactory,并提供应用框架级别的服务。 作为开发人员,我们需要告诉Spring哪些对象要作为bean装配到容器中,bean和bean之间的依赖关系。Spring提供了三种主要的装配机制: 隐式的bean发现机制和自动装配 在Java中进行显示配置 在XML中进行显示配置 下面我们逐一介绍这三种机制。 自动装配bean组件扫描:Spring会自动发现应用上下文中所创建的bean@Component 注解表明该类会作为组件类,并告知Spring要为这个类创建bean。 1234@Componentpublic class Dog {} @ComponentScan 注解启用了组件扫描。 12345@Configuration@ComponentScanpublic class DemoApplication {} 自动装配:Spring自动满足bean之间的依赖@Autowired 注解可以作用在构造器、方法、属性上。 12345678910111213141516171819@Componentpublic class Dog { // 属性 @Autowired private Cat cat; // 构造器 // 从Spring 4.3开始,具有单个构造函数的类可以省略@Autowired注释 @Autowired public Dog(Cat cat) { this.cat = cat; } // 方法 @Autowired public void setCat(Cat cat) { this.cat = cat; }} 在Java中装配bean组价配置:声明一个配置类,并在配置类中配置bean@Configuration 注解表明这个类是配置类,我们可以在配置类下创建bean。 @bean 注解会告诉Spring这个方法将会返回一个对象,该对象要注册为Spring上下文中的bean。 12345678910111213141516171819202122232425/** * 普通类 */public class BaseBean { public void p() { System.out.println(\"Hello bean\"); }}/** * 配置类 */@Configurationpublic class BeanConfig { // 这个方法返回一个对象,Spring会把这个对象注册为bean @Bean public BaseBean getBaseBean() { return new BaseBean(); }} 组件注入:在配置类中把被依赖的组件注入另一个组件中两种方式注入bean: 我们可以直接调用get方法,获取到对应的组件 在get方法中把被依赖的组件作为参数传入,Spring在调用这个方法时,会自动为你注入。 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051/** * 普通类 */public class BaseBean { public void p() { System.out.println(\"Hello bean\"); }}/** * 普通类 */public class UserBean { private BaseBean baseBean; public UserBean(BaseBean baseBean) { this.baseBean = baseBean; }}/** * 配置类 */@Configurationpublic class BeanConfig { // 这个方法返回一个对象,Spring会把这个对象注册为bean @Bean public BaseBean getBaseBean() { return new BaseBean(); } /** * 以下为两种注入bean的方法 */ // 方法一:直接调用get方法 @Bean public UseBean getUseBean() { return new UseBean(getBaseBean()); } // 方法二:当做参数传入,Spring将自动为你注入 @Bean public UseBean getUseBean(BaseBean baseBean) { return new UseBean(baseBean); }} 通常情况下我们都会使用方法二。 通过XML装配bean尽管现在我们已经不怎么再使用XML装配bean,但在Spring刚刚出现的时候,XML是描述配置的主要方式,我们还是有必要了解一下的。 在使用JavaConfig的时候,我们创建了一个配置类来装配bean,而在XML配置中,我们需要创建一个XML文件,并且要以<beans>元素为根。 最为简单的Spring XML配置如下所示: 123456789<?xml version=\"1.0\" encoding=\"UTF-8\"?><beans xmlns=\"http://www.springframework.org/schema/beans\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xsi:schemaLocation=\"http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd\"> <!-- 在这里配置你的bean --></beans> 组件配置以上文的BaseBean为例,我们在XML文件中把它声明为bean。 1<bean id=\"baseBean\" class=\"com.example.demo.BaseBean\" /> 组件注入12<bean id=\"useBean\" class=\"com.example.demo.UseBean\" c:_=\"baseBean\" /> XML的语法我就不再这里详述了,有兴趣的同学可以自行学习。 总结本文我们简单介绍了Spring中的IoC,介绍了Spring中装配bean的三种方式:自动化配置,基于Java的显式配置以及基于XML的显式配置。这些技术都是为了描述Spring应用中的组件以及组件之间的关系。 一般来说我们都会使用自动化配置,尽量避免显式配置带来的维护成本。如果不得不使用显式配置的话,我们优先选择基于Java的配置,它比基于XML的配置更加强大、类型安全并且易于重构。 参考资料控制反转(IoC)与依赖注入(DI) Spring 实战 Spring 揭秘 本文首发于我的个人博客 https://chaohang.top 作者张小超 转载请注明出处 欢迎关注我的微信公众号 【超超不会飞】,获取第一时间的更新。","categories":[],"tags":[{"name":"Java","slug":"Java","permalink":"https://www.chaohang.top/tags/Java/"},{"name":"Spring","slug":"Spring","permalink":"https://www.chaohang.top/tags/Spring/"}]},{"title":"Java注解基本原理","slug":"Java注解基本原理","date":"2019-12-04T07:22:16.000Z","updated":"2019-12-04T07:22:16.000Z","comments":true,"path":"2019/12/04/Java注解基本原理/","link":"","permalink":"https://www.chaohang.top/2019/12/04/Java%E6%B3%A8%E8%A7%A3%E5%9F%BA%E6%9C%AC%E5%8E%9F%E7%90%86/","excerpt":"注解的本质「java.lang.annotation.Annotation」接口中有这么一句话,用来描述『注解』。 The common interface extended by all annotation types所有的注解类型都继承自这个普通的接口(Annotation)","text":"注解的本质「java.lang.annotation.Annotation」接口中有这么一句话,用来描述『注解』。 The common interface extended by all annotation types所有的注解类型都继承自这个普通的接口(Annotation) 这句话有点抽象,但却说出了注解的本质。我们看一个 JDK 内置注解的定义: 1234@Target(ElementType.METHOD)@Retention(RetentionPolicy.SOURCE)public @interface Override {} 这是注解 @Override 的定义,其实它本质上就是:12public interface Override extends Annotation{}没错,注解的本质就是一个继承了 Annotation 接口的接口。有关这一点,你可以去反编译任意一个注解类,你会得到结果的。 一个注解准确意义上来说,只不过是一种特殊的注释而已,如果没有解析它的代码,它可能连注释都不如。 解析一个类或者方法的注解往往有两种形式,一种是编译期直接扫描,一种是运行期反射。 元注解『元注解』是用于修饰注解的注解,通常用在注解的定义上。 JAVA 中有以下几个『元注解』: @Target:注解的作用目标 @Retention:注解的生命周期 @Documented:注解是否应当被包含在 JavaDoc 文档中 @Inherited:是否允许子类继承该注解 @Target@Target 注解指明该注解可以作用哪些对象上。 123456789101112@Documented@Retention(RetentionPolicy.RUNTIME)@Target(ElementType.ANNOTATION_TYPE)public @interface Target { /** * Returns an array of the kinds of elements an annotation type * can be applied to. * @return an array of the kinds of elements an annotation type * can be applied to */ ElementType[] value();} 注解接收一个ElementType数组,ElementType是一个枚举,成员如下: ElementType.TYPE:允许被修饰的注解作用在类、接口和枚举上 ElementType.FIELD:允许作用在属性字段上 ElementType.METHOD:允许作用在方法上 ElementType.PARAMETER:允许作用在方法参数上 ElementType.CONSTRUCTOR:允许作用在构造器上 ElementType.LOCAL_VARIABLE:允许作用在本地局部变量上 ElementType.ANNOTATION_TYPE:允许作用在注解上 ElementType.PACKAGE:允许作用在包上 ElementType.TYPE_PARAMETER:允许作用在类型参数上 ElementType.TYPE_USE:允许作用在类型上 @Retention@Retention 用于指明当前注解的生命周期12345678910@Documented@Retention(RetentionPolicy.RUNTIME)@Target(ElementType.ANNOTATION_TYPE)public @interface Retention { /** * Returns the retention policy. * @return the retention policy */ RetentionPolicy value();}注解接收一个RetentionPolicy数据,RetentionPolicy是个枚举,成员如下: RetentionPolicy.SOURCE:当前注解编译期可见,不会写入 class 文件 RetentionPolicy.CLASS:类加载阶段丢弃,会写入 class 文件 RetentionPolicy.RUNTIME:永久保存,可以反射获取 JAVA 的内置三大注解 @Override @Deprecated 标识类或方法不再推荐使用 @SuppressWarnings 主要用来压制 java 的警告 实现一个自己的注解定义一个注解:12345@Target(ElementType.METHOD)@Retention(RetentionPolicy.RUNTIME)public @interface HelloAnnotation { String value() default \"Hello annotation!\";}使用这个注解:123456789101112public class UseAnnotation { @HelloAnnotation public void hello() { System.out.println(\"hello\"); } @HelloAnnotation(\"Hello world!\") public void helloWorld() { System.out.println(\"Hello world!\"); }}注解最重要的部分在于对注解的处理。注解处理器就是通过反射机制获取被检查方法上的注解信息,然后根据注解元素的值进行特定的处理。如果没有注解处理器,注解就是个注释,或者连注释都不如。 处理这个注解:123456789101112131415public class Test { public static void main(String[] args) { testAnnotation(UseAnnotation.class); } private static void testAnnotation(Class<?> cl) { for (Method m : cl.getDeclaredMethods()) { HelloAnnotation ha = m.getAnnotation(HelloAnnotation.class); if (ha != null) { System.out.println(\"Found My Annotation: \" + ha.value()); } } }}输出结果:12Found My Annotation: Hello annotation!Found My Annotation: Hello world! 参考资料JAVA 注解的基本原理 Java注解基本原理 本文首发于我的个人博客 https://chaohang.top 作者张小超 转载请注明出处 欢迎关注我的微信公众号 【超超不会飞】,获取第一时间的更新。","categories":[],"tags":[{"name":"Java","slug":"Java","permalink":"https://www.chaohang.top/tags/Java/"}]},{"title":"堆(优先队列)","slug":"堆","date":"2019-08-25T08:58:11.000Z","updated":"2019-08-25T08:58:11.000Z","comments":true,"path":"2019/08/25/堆/","link":"","permalink":"https://www.chaohang.top/2019/08/25/%E5%A0%86/","excerpt":"定义堆(heap)也被称为优先队列(priority queue)。是一种特殊的树状数据结构。","text":"定义堆(heap)也被称为优先队列(priority queue)。是一种特殊的树状数据结构。 普通队列是先进先出(first in first out),而优先队列出栈的顺序是按照元素的优先权大小。 堆可以分为”大顶堆“也称”最大堆“(最大值优先出列),”小顶堆“也称”最小堆“(最小值优先出列)。 堆的常用方法: 构建优先队列 快速找出最大值(最小值) 实现堆是用数组实现的完全二叉树。目前有多种算法可以实现堆,速度最快的是斐波那契堆。 具体的实现可以参考一下这两篇文章 Learning to Love HeapsHeap 使用LeetCode第703题Kth Largest Element in a Stream)就是一个使用堆的场景,如果我们使用简单的数组排序的方法很有可能超时,使用堆是该题的最优解之一。 12345678910111213141516171819202122class KthLargest { private final PriorityQueue<Integer> q; private final int k; public KthLargest(int k, int[] nums) { this.k = k; q = new PriorityQueue<>(k); for (int n : nums) { add(n); } } public int add(int val) { if (q.size() < k) { q.offer(val); } else if (q.peek() < val) { q.poll(); q.offer(val); } return q.peek(); }} 参考资料Heap wiki) Learning to Love Heaps Heap Heap Data Structure","categories":[],"tags":[{"name":"数据结构","slug":"数据结构","permalink":"https://www.chaohang.top/tags/%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84/"}]},{"title":"LeetCode20——有效的括号","slug":"LeetCode20——有效的括号","date":"2019-08-20T14:36:50.000Z","updated":"2019-08-20T14:36:50.000Z","comments":true,"path":"2019/08/20/LeetCode20——有效的括号/","link":"","permalink":"https://www.chaohang.top/2019/08/20/LeetCode20%E2%80%94%E2%80%94%E6%9C%89%E6%95%88%E7%9A%84%E6%8B%AC%E5%8F%B7/","excerpt":"题目描述给定一个只包括 ‘(‘,’)’,’{‘,’}’,’[‘,’]’ 的字符串,判断字符串是否有效。","text":"题目描述给定一个只包括 ‘(‘,’)’,’{‘,’}’,’[‘,’]’ 的字符串,判断字符串是否有效。 有效字符串需满足: 左括号必须用相同类型的右括号闭合。 左括号必须以正确的顺序闭合。 注意空字符串可被认为是有效字符串。 示例 1:12输入: "()"输出: true 示例 2:12输入: "()[]{}"输出: true 示例 3:12输入: "(]"输出: false 示例 4:12输入: "([)]"输出: false 示例 5:12输入: "{[]}"输出: true 分析本题需要利用 栈 数据结构先进后出的特点。如果遇到左括号则push入栈中,如果遇到右括号,则需要从栈中pop出一个与之匹配的左括号,否则该字符串不合法。 全部判断完后,该栈需要为空,不为空则表示该字符串不合法。 123456789101112131415/** * @param {string} s * @return {boolean} */var isValid = function(s) { const stack = [] const paren_map = {')': '(', ']': '[', '}': '{'} for (let i of s) { if (!(i in paren_map)) stack.push(i) else if (stack.length === 0 || stack.pop() !== paren_map[i]) return false } return stack.length === 0} 时间复杂度: O(n)空间复杂度: O(n) 开心消消乐解法在LeetCode上还看到一种有趣的解法,把两两匹配的括号全部消掉,直到没有两两匹配的括号为止,如果字符串为空则为true。 123456789101112/** * @param {string} s * @return {boolean} */var isValid = function(s) { let length do { length = s.length s = s.replace(\"()\", \"\").replace(\"[]\", \"\").replace(\"{}\", \"\") } while(length !== s.length) return s.length === 0}","categories":[],"tags":[{"name":"算法","slug":"算法","permalink":"https://www.chaohang.top/tags/%E7%AE%97%E6%B3%95/"},{"name":"LeetCode","slug":"LeetCode","permalink":"https://www.chaohang.top/tags/LeetCode/"}]},{"title":"LeetCode141——环形链表","slug":"LeetCode141——环形链表","date":"2019-08-18T02:04:01.000Z","updated":"2019-08-18T02:04:01.000Z","comments":true,"path":"2019/08/18/LeetCode141——环形链表/","link":"","permalink":"https://www.chaohang.top/2019/08/18/LeetCode141%E2%80%94%E2%80%94%E7%8E%AF%E5%BD%A2%E9%93%BE%E8%A1%A8/","excerpt":"题目描述给定一个链表,如何判断链表中是否有环。","text":"题目描述给定一个链表,如何判断链表中是否有环。 方法一:硬破解循环一定次数,或者循环一定的时间,还没有出来的就是进入到环里了,至于循环几次或者循环多久,有空的朋友可以慢慢调这个参数。🙂🙂🙂 1234567891011121314/** * @param {ListNode} head * @return {boolean} */var hasCycle = function(head) { let now = head let t = 0 while(now) { if (t > 10000) return true now = now.next t += 1 } return false}; 方法二:哈希表我们可以用一个 set 存下之前访问过的节点,如果再次访问了这个节点,则有环。 1234567891011121314/** * @param {ListNode} head * @return {boolean} */var hasCycle = function(head) { const set = new Set() let now = head while(now) { if (set.has(now)) return true set.add(now) now = now.next } return false}; 时间复杂度:O(n)空间复制读:O(n) 方法三:龟兔赛跑有两个指针,一个快指针,一个慢指针,快指针一次走两步,慢指针一次走一步。如果链表有环,则两个指针最终将会相遇。如果没有环,则永远不会相遇。 12345678910111213/** * @param {ListNode} head * @return {boolean} */var hasCycle = function(head) { let fast = slow = head while(fast && slow && fast.next) { fast = fast.next.next slow = slow.next if (fast === slow) return true } return false}; 时间复杂度:O(n)空间复杂度:O(1)","categories":[],"tags":[{"name":"算法","slug":"算法","permalink":"https://www.chaohang.top/tags/%E7%AE%97%E6%B3%95/"},{"name":"LeetCode","slug":"LeetCode","permalink":"https://www.chaohang.top/tags/LeetCode/"}]},{"title":"LeetCode206——反转链表","slug":"LeetCode206——反转链表","date":"2019-08-13T06:50:56.000Z","updated":"2019-08-13T06:50:56.000Z","comments":true,"path":"2019/08/13/LeetCode206——反转链表/","link":"","permalink":"https://www.chaohang.top/2019/08/13/LeetCode206%E2%80%94%E2%80%94%E5%8F%8D%E8%BD%AC%E9%93%BE%E8%A1%A8/","excerpt":"题目描述反转一个单链表。 示例:输入: 1->2->3->4->5->NULL输出: 5->4->3->2->1->NULL 进阶:你可以迭代或递归地反转链表。你能否用两种方法解决这道题?","text":"题目描述反转一个单链表。 示例:输入: 1->2->3->4->5->NULL输出: 5->4->3->2->1->NULL 进阶:你可以迭代或递归地反转链表。你能否用两种方法解决这道题? 分析反转链表的关键在于将当前节点的 next 指向前一节点。 方法一:迭代需要两个变量,分别用来存储当前节点和上一节点,并把当前节点的 next 指向上一节点,然后前进一位,直到结束。 123456789101112131415/** * @param {ListNode} head * @return {ListNode} */var reverseList = function(head) { var cur = head var prev = null while (cur) { var next = cur.next cur.next = prev prev = cur cur = next } return prev}; 时间复杂度O(n)空间复杂度O(1) 方法二:递归递归理解起来比较绕。假设有 1->2->3->null 这样一个链表,当执行到3时开始反转,此时 list 为 3->null,head 为 2,此时2的状态为 2->3->null,要变为 3->2->null 则需要让 head.next.next = head,并且 head.next = null,画了个图: ) 12345678910111213/** * @param {ListNode} head * @return {ListNode} */var reverseList = function(head) { if (head === null || head.next === null) { return head } const list = reverseList(head.next) head.next.next = head head.next = null return list} 时间复杂度O(n)空间复杂度O(n),因为使用了递归","categories":[],"tags":[{"name":"算法","slug":"算法","permalink":"https://www.chaohang.top/tags/%E7%AE%97%E6%B3%95/"},{"name":"LeetCode","slug":"LeetCode","permalink":"https://www.chaohang.top/tags/LeetCode/"}]},{"title":"极客时间算法40讲笔记之一——如何学习","slug":"极客时间算法40讲笔记之一——如何学习","date":"2019-08-10T08:16:55.000Z","updated":"2019-08-10T08:16:55.000Z","comments":true,"path":"2019/08/10/极客时间算法40讲笔记之一——如何学习/","link":"","permalink":"https://www.chaohang.top/2019/08/10/%E6%9E%81%E5%AE%A2%E6%97%B6%E9%97%B4%E7%AE%97%E6%B3%9540%E8%AE%B2%E7%AC%94%E8%AE%B0%E4%B9%8B%E4%B8%80%E2%80%94%E2%80%94%E5%A6%82%E4%BD%95%E5%AD%A6%E4%B9%A0/","excerpt":"如何有效学习数据结构 Chunk it up (切碎知识点) 比如要学习算法与数据结构,我们可以把想要学习的数据结构和算法罗列出来,在整理出他们之间的关系,然后各个击破。","text":"如何有效学习数据结构 Chunk it up (切碎知识点) 比如要学习算法与数据结构,我们可以把想要学习的数据结构和算法罗列出来,在整理出他们之间的关系,然后各个击破。 Deliberate practicing (刻意练习) 练习缺陷、弱点的地方 练习时会不舒服、不爽、枯燥 会有一个下降后上升的曲线,重复这个曲线就能够提高 Feedback (反馈) 及时反馈 主动型反馈 看别人的代码(GitHub,LeetCode) 被动型反馈 code review 写博客,输出 如何刷题切题四件套 Clarification (审题) Possible solution (可能的解决办法) compare(time/spac) (比较多种解决方法之间的差别) optimal (找出最优解) Coding (多写) Test cases 算法复杂度","categories":[],"tags":[{"name":"方法论","slug":"方法论","permalink":"https://www.chaohang.top/tags/%E6%96%B9%E6%B3%95%E8%AE%BA/"},{"name":"极客时间","slug":"极客时间","permalink":"https://www.chaohang.top/tags/%E6%9E%81%E5%AE%A2%E6%97%B6%E9%97%B4/"}]},{"title":"设计模式之TypeScript中的单例模式","slug":"设计模式之TypeScript中的单例模式","date":"2019-08-03T09:17:09.000Z","updated":"2019-08-03T09:17:09.000Z","comments":true,"path":"2019/08/03/设计模式之TypeScript中的单例模式/","link":"","permalink":"https://www.chaohang.top/2019/08/03/%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F%E4%B9%8BTypeScript%E4%B8%AD%E7%9A%84%E5%8D%95%E4%BE%8B%E6%A8%A1%E5%BC%8F/","excerpt":"单例模式单例模式指的是一个类只能有一个实例存在。 在实际应用中有些场景我们只需要唯一实例的存在,如果出现多个实例就会出现问题,这个时候我们就需要用到单例模式。比如线程池,缓存,硬件设备等。","text":"单例模式单例模式指的是一个类只能有一个实例存在。 在实际应用中有些场景我们只需要唯一实例的存在,如果出现多个实例就会出现问题,这个时候我们就需要用到单例模式。比如线程池,缓存,硬件设备等。 实现思路每个类中都有一个构造函数 constructor,当我们 new 这个类时,实际上就是在调用这个构造函数。比如我们定义了一个 Car 类,实例化了一只叫“茶”的小猫咪和一只叫“菊”的小猫咪。此时我们声明了两个 Car 的实例。 123456789class Car { name: string constructor(name) { this.name = name }}const cha = new Car('茶')const ju = new Car('菊') 如果我们把构造函数变成私有函数,外部就无法将这个类实例化。 所以,只要我们把一个类的构造函数私有化,并在类内部实例化出唯一的一个实例,就可以实现单例模式了。 12345678910111213class Singleton { private constructor() { // 初始化 } // 声明一个私有的静态属性作为唯一的实例 private static INSTANCE: Singleton = new Singleton() // 暴露一个公开的方法用于获取实例 public getInstance() { return Singleton.INSTANCE }} 饿汉模式和懒汉模式单例模式又分为饿汉模式和懒汉模式 饿汉模式:指全局的单例实例在类装载时构建 懒汉模式:指全局的单例实例在第一次被使用时构建 饿汉模式12345678910111213class Singleton { private constructor() { // 初始化 } // 声明一个私有的静态属性作为唯一的实例 private static INSTANCE: Singleton = new Singleton() // 暴露一个公开的方法用于获取实例 public getInstance() { return Singleton.INSTANCE }} 饿汉模式12345678910111213141516class Singleton { private constructor() { // 初始化 } // 声明一个私有的静态属性作为唯一的实例 private static INSTANCE?: Singleton = null // 暴露一个公开的方法用于获取实例 public getInstance() { if (!Singleton.INSTANCE) { Singleton.INSTANCE = new Singleton() } return Singleton.INSTANCE }}","categories":[],"tags":[{"name":"设计模式","slug":"设计模式","permalink":"https://www.chaohang.top/tags/%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F/"}]},{"title":"深入JavaScript之模拟apply,call,bind","slug":"深入JavaScript之模拟apply-call-bind","date":"2019-07-23T07:33:48.000Z","updated":"2019-07-23T07:33:48.000Z","comments":true,"path":"2019/07/23/深入JavaScript之模拟apply-call-bind/","link":"","permalink":"https://www.chaohang.top/2019/07/23/%E6%B7%B1%E5%85%A5JavaScript%E4%B9%8B%E6%A8%A1%E6%8B%9Fapply-call-bind/","excerpt":"apply call bind 这三个函数可以改变 this 的绑定,语法上有略微差别,可以看我很早之前的一篇文章做过简单的介绍 浅谈JavaScript中的apply、call、bind。","text":"apply call bind 这三个函数可以改变 this 的绑定,语法上有略微差别,可以看我很早之前的一篇文章做过简单的介绍 浅谈JavaScript中的apply、call、bind。 this 优先级 new 关键字。函数被 new 调用,this 指向由 new 新构建出来的这个对象 函数通过 apply call bind 调用,this 指向绑定的对象 函数被调用时,this 指向调用的对象 默认。非严格模式情况下,this指向window, 严格模式下,this指向undefined。 总结一下优先级new 关键字 > apply call bind > 函数调用 > 默认 实现我们既然要重写 apply,那前两个优先级是用不了的。我们使用第三个优先级——函数调用——改变 this 的指向。 先看下代码 123456789Function.prototype.myApply = function (context, args) { context = context || window args = args || [] const key = Symbol() // 保证key的唯一性,避免覆盖原有的属性方法 context[key] = this // 此时的this指向myApply的调用者 const result = context[key](...args) delete context[key] // 删掉新建的key return result} 上面的代码忽略一些边界情况,实现了简易版的 apply。看下使用的效果 12345678910const person = { name: 'A', getName(prefix, suffix) { return `${prefix} ${this.name} ${suffix}` }}const result = person.getName.myApply({name: 'apply'}, ['one', 'two'])console.log(result) // one apply two 首先我们创建了一个 Symbol 对象作为 context 的 key,并把 this 赋值给他。此时的 this 值为 getName,因为是 getName 调用了 myApply,所以 this 指向调用者。当我们以 context[key](...args) 这种方式调用 getName 时,this 指向了 context,因为是 context 调用了 getName 方法。借此完成了 this 值得转换。 call 和 bind 同理 call 12345678Function.prototype.myCall = function (context, ...args) { context = context || window const key = Symbol() context[key] = this const result = context[key](...args) delete context[key] return result} bind 的实现可以使用我们写的 call 函数。需要注意的是 bind 可以传参,新生成的函数也可以传参。 123456Function.prototype.myBind = function (context, ...args) { const self = this return function newFn(...newFnArgs) { return self.myCall(context, ...args, ...newFnArgs) }}","categories":[],"tags":[{"name":"JavaScript","slug":"JavaScript","permalink":"https://www.chaohang.top/tags/JavaScript/"}]},{"title":"查询各个科目成绩前三的记录","slug":"查询各个科目成绩前三的记录","date":"2019-06-19T11:55:41.000Z","updated":"2019-06-19T11:55:41.000Z","comments":true,"path":"2019/06/19/查询各个科目成绩前三的记录/","link":"","permalink":"https://www.chaohang.top/2019/06/19/%E6%9F%A5%E8%AF%A2%E5%90%84%E4%B8%AA%E7%A7%91%E7%9B%AE%E6%88%90%E7%BB%A9%E5%89%8D%E4%B8%89%E7%9A%84%E8%AE%B0%E5%BD%95/","excerpt":"id student_id name course_id course_name score 1 2005001 张三 1 数学 69 2 2005002 李四 1 数学 89 3 2005005 王五 1 数学 23 3 2005001 张三 2 语文 69 3 2005002 李四 2 语文 73 3 2005005 王五 2 语文 88 1 2005006 赵六 2 语文 69 2 2005006 赵六 1 数学 89 3 2005007 郑七 2 语文 100","text":"id student_id name course_id course_name score 1 2005001 张三 1 数学 69 2 2005002 李四 1 数学 89 3 2005005 王五 1 数学 23 3 2005001 张三 2 语文 69 3 2005002 李四 2 语文 73 3 2005005 王五 2 语文 88 1 2005006 赵六 2 语文 69 2 2005006 赵六 1 数学 89 3 2005007 郑七 2 语文 100 1234567891011121314151617181920CREATE TABLE `test` ( `id` INT UNSIGNED NOT NULL AUTO_INCREMENT, `student_id` INT UNSIGNED NOT NULL, `name` VARCHAR(20) NOT NULL, `course_id` INT UNSIGNED NOT NULL, `course_name` VARCHAR(20) NOT NULL, `score` INT UNSIGNED NOT NULL, PRIMARY KEY (`id`)) ENGINE = InnoDB;INSERT INTO `test` (`student_id`, `name`, `course_id`, `course_name`, `score`)VALUES (2005001, '张三', 1, '数学', 69), (2005002, '李四', 1, '数学', 80), (2005005, '王五', 1, '数学', 23), (2005001, '张三', 2, '语文', 69), (2005002, '李四', 2, '语文', 73), (2005005, '王五', 2, '语文', 88), (2005006, '赵六', 2, '语文', 69), (2005006, '赵六', 1, '数学', 89), (2005007, '郑七', 2, '语文', 100); 1. 用一条 SQL 语句查询出每门课都大于 80 分的学生姓名理解:分数最小的那门课大于80,自然每门课都大于80了。 1SELECT `name` FROM `test` GROUP BY `name` HAVING min(`score`) >= 80; 2. 查询各个科目成绩前三的记录思路:查出该科成绩超过他的人数小于3的学生,再进行排列 12345678SELECT `student_id`, `name`, `course_id`, `course_name`, `score`FROM `test` `t1`WHERE ( SELECT count(1) FROM `test` `t2` WHERE `t1`.`course_id` = `t2`.`course_id` AND `t1`.`score` < `t2`.`score`) < 3ORDER BY `course_id`, `score` DESC;","categories":[],"tags":[{"name":"MySQL","slug":"MySQL","permalink":"https://www.chaohang.top/tags/MySQL/"}]},{"title":"MySQL重复数据的处理","slug":"mysql重复数据的处理","date":"2019-06-19T07:54:13.000Z","updated":"2019-06-19T07:54:13.000Z","comments":true,"path":"2019/06/19/mysql重复数据的处理/","link":"","permalink":"https://www.chaohang.top/2019/06/19/mysql%E9%87%8D%E5%A4%8D%E6%95%B0%E6%8D%AE%E7%9A%84%E5%A4%84%E7%90%86/","excerpt":"假设有一张表如下,含有部分重复数据,这些重复数据除了 id 不同,其他数据都相同。","text":"假设有一张表如下,含有部分重复数据,这些重复数据除了 id 不同,其他数据都相同。 id student_id name course_id course_name score 1 2005001 张三 1 数学 69 2 2005002 李四 1 数学 89 3 2005001 张三 1 数学 69 1234567891011121314CREATE TABLE `test` ( `id` INT UNSIGNED NOT NULL AUTO_INCREMENT, `student_id` INT UNSIGNED NOT NULL, `name` VARCHAR(20) NOT NULL, `course_id` INT UNSIGNED NOT NULL, `course_name` VARCHAR(20) NOT NULL, `score` INT UNSIGNED NOT NULL, PRIMARY KEY (`id`)) ENGINE = InnoDB;INSERT INTO `test` (`student_id`, `name`, `course_id`, `course_name`, `score`)VALUES (2005001, '张三', 1, '数学', 69), (2005002, '李四', 1, '数学', 89), (2005001, '张三', 1, '数学', 69); 1. 过滤重复数据使用 group by 进行查找就可以过滤重复数据 12SELECT `student_id`, `name`, `course_id`, `course_name`, `score` FROM `test` GROUP BY `student_id`, `name`, `course_id`, `course_name`, `score`; 2. 统计重复数据123SELECT count(1) AS `repetitions`, `name` FROM `test` GROUP BY `student_id`, `name`, `course_id`, `course_name`, `score` HAVING `repetitions` > 1; 3. 删除重复数据思路:分组后,选出 id 最小的一条,不被选中的 id 全部删除 1234567DELETE FROM `test`WHERE `id` NOT IN ( SELECT `min_id` FROM (SELECT min(`id`) AS `min_id` FROM `test` GROUP BY `student_id`, `name`, `course_id`, `course_name`, `score`) AS `a`);","categories":[],"tags":[{"name":"MySQL","slug":"MySQL","permalink":"https://www.chaohang.top/tags/MySQL/"}]},{"title":"LeetCode176——第二高的薪水","slug":"LeetCode176——第二高的薪水","date":"2019-06-19T03:51:52.000Z","updated":"2019-06-19T03:51:52.000Z","comments":true,"path":"2019/06/19/LeetCode176——第二高的薪水/","link":"","permalink":"https://www.chaohang.top/2019/06/19/LeetCode176%E2%80%94%E2%80%94%E7%AC%AC%E4%BA%8C%E9%AB%98%E7%9A%84%E8%96%AA%E6%B0%B4/","excerpt":"题目描述编写一个 SQL 查询,获取 Employee 表中第二高的薪水(Salary) 。","text":"题目描述编写一个 SQL 查询,获取 Employee 表中第二高的薪水(Salary) 。 1234567+----+--------+| Id | Salary |+----+--------+| 1 | 100 || 2 | 200 || 3 | 300 |+----+--------+ 例如上述 Employee 表,SQL查询应该返回 200 作为第二高的薪水。如果不存在第二高的薪水,那么查询应返回 null。 12345+---------------------+| SecondHighestSalary |+---------------------+| 200 |+---------------------+ 思路 排序,取出排名第二的值 123select Salary from Employee order by Salary desc limit 1, 1; group by 过滤掉相同薪水 1234select Salary from Employee group by Salary order by Salary desc limit 1, 1; 当不存在第二高的薪水时,会返回空而不是 null,做个是否为 null 的判断 12345select ifnull( (select Salary from Employee group by Salary order by Salary desc limit 1, 1), null ) as SecondHighestSalary; 可以简写为 123select (select Salary from Employee group by Salary order by Salary desc limit 1, 1) as SecondHighestSalary;","categories":[],"tags":[{"name":"算法","slug":"算法","permalink":"https://www.chaohang.top/tags/%E7%AE%97%E6%B3%95/"},{"name":"LeetCode","slug":"LeetCode","permalink":"https://www.chaohang.top/tags/LeetCode/"}]},{"title":"理解 Node.js 的 GC 机制","slug":"理解 Node.js 的 GC 机制","date":"2019-06-02T09:18:36.000Z","updated":"2019-06-02T09:18:36.000Z","comments":true,"path":"2019/06/02/理解 Node.js 的 GC 机制/","link":"","permalink":"https://www.chaohang.top/2019/06/02/%E7%90%86%E8%A7%A3%20Node.js%20%E7%9A%84%20GC%20%E6%9C%BA%E5%88%B6/","excerpt":"《深入浅出Node.js》第五章《内存控制》阅读笔记 随着 Node 的发展,JavaScript 的应用场景早已不再局限在浏览器中。本文不讨论网页应用、命令行工具等短时间执行,且只影响终端用户的场景。由于运行时间短,随着进程的退出,内存会释放,几乎没有内存管理的必要。但随着 Node 在服务端的广泛应用,JavaScript 的内存管理需要引起我们的重视。 V8 的内存限制在一般的后端开发语言中,在基本的内存使用上没有什么限制,然而在 Node 中通过 JavaScript 使用内存时就会发现只能使用部分内存(64位系统下约为1.4GB,32位系统下约为0.7GB)。在这样的限制下,将会导致 Node 无法直接操作大内存对象。 造成这个问题的主要原因在于 Node 的 JavaScript 执行引擎 V8。 在 V8 中,所有的 JavaScript 对象都是通过堆来进行分配的。Node 提供了 V8 中内存的使用量查看方法 process.memoryUsage()。","text":"《深入浅出Node.js》第五章《内存控制》阅读笔记 随着 Node 的发展,JavaScript 的应用场景早已不再局限在浏览器中。本文不讨论网页应用、命令行工具等短时间执行,且只影响终端用户的场景。由于运行时间短,随着进程的退出,内存会释放,几乎没有内存管理的必要。但随着 Node 在服务端的广泛应用,JavaScript 的内存管理需要引起我们的重视。 V8 的内存限制在一般的后端开发语言中,在基本的内存使用上没有什么限制,然而在 Node 中通过 JavaScript 使用内存时就会发现只能使用部分内存(64位系统下约为1.4GB,32位系统下约为0.7GB)。在这样的限制下,将会导致 Node 无法直接操作大内存对象。 造成这个问题的主要原因在于 Node 的 JavaScript 执行引擎 V8。 在 V8 中,所有的 JavaScript 对象都是通过堆来进行分配的。Node 提供了 V8 中内存的使用量查看方法 process.memoryUsage()。 heapTotal 已申请到的堆内存 heapUsed 当前使用的堆内存 为什么 V8 要限制堆的大小: V8 为浏览器而设计,不太可能遇到用大量内存的场景 V8 的垃圾回收机制的限制。(按官方的说法,以1.5GB的垃圾回收堆内存为例,V8做一次小的垃圾回收需要50ms以上,做一次非增量式的垃圾回收需要1s以上) V8提供了选项让我们可以控制使用内存的大小 node --max-old-space-size=1700 test.js 设置老生代内存空间最大值,单位为MB node --max-new-space-size=1024 test.js 设置新生代内存空间最大值,单位为KB 比较遗憾的是,这两个最大值需要在启动时执行。这意味着 V8 使用的内存没办法根据使用的情况自动扩充,当内存分配过程中超过极限值时,就会引起进程出错。 V8 的垃圾回收机制V8 的垃圾回收策略主要基于分代式垃圾回收机制。在 V8 中,主要将内存分为新生代和老生代两代。新生代中的对象为存活时间较短的对象,老生代中的对象为存活时间较长或常驻内存的对象。 V8 堆的整体大小就是新生代的内存空间加上老生代的内存空间 Scavenge 算法在分代的基础上,新生代中的对象主要通过 Scavenge 算法进行垃圾回收。在 Scavenge 的具体实现中,主要采用了 Cheney 算法。 Cheney 算法是一种采用复制的方式实现的垃圾回收算法。它将堆内存一分为二,每一部分空间成为 semispace。在这两个 semispace 空间中,只有一个处于使用中,另一个处于闲置中。处于使用中的 semispace 空间成为 From 空间,处于闲置状态的空间成为 To 空间。当我们分配对象时,先是在 From 空间中进行分配。当开始进行垃圾回收时,会检查 From 空间中的存活对象,这些存活对象将被复制到 To 空间中,而非存活对象占用的空间将被释放。完成复制后, From 空间和 To 空间的角色发生对换。 Scavenge 的缺点是只能使用堆内存的一半,但 Scavenge 由于只复制存活的对象,并且对于生命周期短的场景存活对象只占少部分,所以它在时间效率上表现优异。Scavenge 是典型的牺牲空间换取时间的算法,无法大规模地应用到所有的垃圾回收中,但非常适合应用在新生代中。 晋升对象从新生代中移动到老生代中的过程称为晋升。 From 空间中的存活对象在复制到 To 空间之前需要进行检查,在一定条件下,需要将存活周期长的对象移动到老生代中,也就是完成对象的晋升。 晋升条件主要有两个: 对象是否经历过一次 Scavenge 回收 To 空间已经使用超过 25% 设置 25% 这个限制值得原因是当这次 Scavenge 回收完成后,这个 To 空间将变成 From 空间,接下来的内存分配将在这个空间中进行,如果占比过高,会影响后续的内存分配。 Mark-Sweep & Mark-CompactV8 在老生代中主要采用了 Mark-Sweep 和 Mark-Compact 相结合的方式进行垃圾回收。 Mark-Sweep 是标记清除的意思,它分为两个阶段,标记和清除。Mark-Sweep 在标记阶段遍历堆中的所有对象,并标记活着的对象,在随后的清除阶段中,只清除未被标记的对象。 Mark-Sweep 最大的问题是在进行一次标记清除回收后,内存空间会出现不连续的状态。这种内存碎片会对后续的内存分配造成问题,因为很可能出现需要分配一个大对象的情况,这时所有的碎片空间都无法完成此次分配,就会提前触发垃圾回收,而这次回收是不必要的。 为了解决 Mark-Sweep 的内存碎片问题,Mark-Compact 被提出来。Mark-Compact是标记整理的意思,是在 Mark-Sweep 的基础上演进而来的。它们的差别在于对象在标记为死亡后,在整理过程中,将活着的对象往一端移动,移动完成后,直接清理掉边界外的内存。 下表为3种主要垃圾回收算法的简单比较 从表中可以看出,在 Mark-Sweep 和 Mark-Compact 之间,由于 Mark-Compact 需要移动对象,所以它的执行速度不可能很快,所以在取舍上,V8 主要使用 Mark-Sweep,在空间不足以从新生代中晋升过来的对象进行分配时才使用 Mark-Compact 。 Incremental Marking为了避免出现 JavaScript 应用逻辑与垃圾回收器看到的不一致的情况,垃圾回收的3种算法都需要将应用逻辑暂停下来,这种行为称为“全停顿” (stop-the-world)。 由于新生代配置的空间较小,存活对象较少,全停顿对新生代影响不大。但老生代通常配置的空间较大,且存活对象较多,全堆垃圾回收(full 垃圾回收)的标记、清除、整理等动作造成的停顿就会比较可怕。 为了降低全堆垃圾回收带来的停顿时间,V8 先从标记阶段入手,将原本要一口气停顿完成的动作改成增量标记(Incremental Marking),也就是拆分为许多小“步进”,每做完一“步进”就让JavaScript应用逻辑执行一小会儿,垃圾回收和应用逻辑交替执行直到标记阶段完成。 V8 在经过增量标记的改进后,垃圾回收的最大停顿时间可以减少到原本的 1/6 左右。 查看GC日志查看垃圾回收日志的方式主要是在启动时添加 --trace_gc 参数。 小结 Node 的 JavaScript 执行引擎为 V8,内存使用和控制也受限于 V8。 V8 把内存分为新生代和老生代,分别存放存活时间较短和存活时间较长或常驻内存的对象。 在新生代中使用 Scavenge 算法进行垃圾回收,优点是速度快无内存碎片,缺点是占用双倍内存空间。 在老生代中将 Mark-Sweep 和 Mark-Compact 两种算法结合使用,主要使用 Mark-Sweep,优点的是无需移动对象,缺点是产生内存碎片。Mark-Compact 是对 Mark-Sweep 的补充,在空间不足以对新晋升的对象进行分配时整理内存,清除内存碎片,由于要移动对象,速度较慢。 V8 使用 Incremental Marking 来减少全停顿带来的影响。 本文首发于我的个人博客 https://chaohang.top 作者张小超 转载请注明出处","categories":[],"tags":[{"name":"JavaScript","slug":"JavaScript","permalink":"https://www.chaohang.top/tags/JavaScript/"},{"name":"Node.js","slug":"Node-js","permalink":"https://www.chaohang.top/tags/Node-js/"}]},{"title":"LeetCode70——爬楼梯","slug":"LeetCode70——爬楼梯","date":"2019-05-26T08:17:45.000Z","updated":"2019-05-26T08:17:45.000Z","comments":true,"path":"2019/05/26/LeetCode70——爬楼梯/","link":"","permalink":"https://www.chaohang.top/2019/05/26/LeetCode70%E2%80%94%E2%80%94%E7%88%AC%E6%A5%BC%E6%A2%AF/","excerpt":"题目描述假设你正在爬楼梯。需要 n 阶你才能到达楼顶。 每次你可以爬 1 或 2 个台阶。你有多少种不同的方法可以爬到楼顶呢? 注意:给定 n 是一个正整数。","text":"题目描述假设你正在爬楼梯。需要 n 阶你才能到达楼顶。 每次你可以爬 1 或 2 个台阶。你有多少种不同的方法可以爬到楼顶呢? 注意:给定 n 是一个正整数。 示例 1:12345输入: 2输出: 2解释: 有两种方法可以爬到楼顶。1. 1 阶 + 1 阶2. 2 阶 示例 2:123456输入: 3输出: 3解释: 有三种方法可以爬到楼顶。1. 1 阶 + 1 阶 + 1 阶2. 1 阶 + 2 阶3. 2 阶 + 1 阶 思路每次可以爬 1 或 2 个台阶。当我们爬 4 个台阶时,就是爬 3 个台阶的方法数,加上爬 2 个台阶的方法数,等于 F(3) + F(2) = 3 + 2 = 5。所以当我们爬 N 个台阶,就有 F(N - 1) + F(N - 2) 种方法。 解决方案方案一:暴力破解我们可以用递归的方法得到所有小于N的方法数,并把它们相加得出结果。递归结束的标志为 N=1 或 N =2。 12345var climbStairs = function(n) { if (n == 1) return 1 if (n == 2) return 2 return climbStairs(n - 1) + climbStairs(n - 2)}; 时间复杂度 O($2^n$)。这种暴力解题的方法会超出时间限制,显然不是我们想要的。 方案二:优化暴力破解从上一种方法我们可以发现,每一步的结果都做了上一步的重复计算。比如F(6) + F(5) 后会计算 F(5) + F(4),F(5) 我们已经计算过了,就不要重复计算了。所以我们可以用一个数组来储存计算结果,方便重复利用。 123456789101112var climbStairs = function(n) { let arr = [] function climb(n) { if (n == 1) return 1 if (n == 2) return 2 if (arr[n] > 0) return arr[n] arr[n] = climb(n - 1) + climb(n - 2) return arr[n] } return climb(n)}; 时间复杂度 O(n),优化之后提高了速度,已经不会超出时间限制了。 方案三:问题分解和递归的思路一样,把一个大问题分解成多个小问题,只是这次我们使用循环的方式,减少内存的开销。 123456789101112var climbStairs = function(n) { if (n == 1) return 1 if (n == 2) return 2 let arr = [] arr[1] = 1 arr[2] = 2 for (let i = 3; i<= n; i++) { arr[i] = arr[i - 1] + arr[i - 2] } return arr[n]}; 时间复杂度 O(n),优化了内存的消耗,速度没有提升。 方案四:斐波那契数从上一个方案我们可以看出这是一个斐波那契数列。 12345678910111213var climbStairs = function(n) { if (n == 1) return 1 if (n == 2) return 2 let first = 1 let second = 2 for (let i = 3; i<= n; i++) { let third = first + second first = second second = third } return second}; 时间复杂度 O(n)","categories":[],"tags":[{"name":"算法","slug":"算法","permalink":"https://www.chaohang.top/tags/%E7%AE%97%E6%B3%95/"},{"name":"LeetCode","slug":"LeetCode","permalink":"https://www.chaohang.top/tags/LeetCode/"}]},{"title":"使用 trash 避免 rm -rf 悲剧","slug":"使用-trash-避免-rm-rf-悲剧","date":"2019-04-24T10:04:26.000Z","updated":"2019-04-24T10:04:26.000Z","comments":true,"path":"2019/04/24/使用-trash-避免-rm-rf-悲剧/","link":"","permalink":"https://www.chaohang.top/2019/04/24/%E4%BD%BF%E7%94%A8-trash-%E9%81%BF%E5%85%8D-rm-rf-%E6%82%B2%E5%89%A7/","excerpt":"昨晚做了一个令人痛心疾首的操作,rm -rf something,把我个人电脑里的重要文件夹给删掉了,懵逼了半天才缓过来。还好是个人文件,不对公司造成影响。这件事也让我意识到 rm -rf 确实是个高风险操作,文件备份也是重中之重。","text":"昨晚做了一个令人痛心疾首的操作,rm -rf something,把我个人电脑里的重要文件夹给删掉了,懵逼了半天才缓过来。还好是个人文件,不对公司造成影响。这件事也让我意识到 rm -rf 确实是个高风险操作,文件备份也是重中之重。 为了规避这个风险操作,我决定用 trash 替代 rm,这样文件就不会直接被删除,而是进入废纸篓。 安装 trashtrash–CLI tool that moves files or folder to the trash 使用 homebrew 安装 trashbrew install trash 配置命令行安装完之后在 .zshrc 或者 .bashrc 添加以下配置,.Trash 是Mac下的废纸篓目录。 12345678alias rm=trashalias r=trashalias rl='ls ~/.Trash'alias ur=undelfileundelfile(){ mv -i ~/.Trash/$@ ./} rm 或 r 命令可以把文件或者文件夹移入废纸篓。 rl 罗列出废纸篓内的文件。 ur 把废纸篓内的某个文件移动到当前位置,相当于恢复。","categories":[],"tags":[{"name":"tool","slug":"tool","permalink":"https://www.chaohang.top/tags/tool/"}]},{"title":"实现一个简单的WebSocket聊天室","slug":"实现一个简单的WebSocket聊天室","date":"2019-04-14T09:36:12.000Z","updated":"2019-04-14T09:36:12.000Z","comments":true,"path":"2019/04/14/实现一个简单的WebSocket聊天室/","link":"","permalink":"https://www.chaohang.top/2019/04/14/%E5%AE%9E%E7%8E%B0%E4%B8%80%E4%B8%AA%E7%AE%80%E5%8D%95%E7%9A%84WebSocket%E8%81%8A%E5%A4%A9%E5%AE%A4/","excerpt":"WebSocket 简介 WebSocket 是 HTML5 开始提供的一种在单个 TCP 连接上进行全双工通讯的协议。WebSocket 使得客户端和服务器之间的数据交换变得更加简单,允许服务端主动向客户端推送数据。在 WebSocket API 中,浏览器和服务器只需要完成一次握手,两者之间就直接可以创建持久性的连接,并进行双向数据传输。在 WebSocket API 中,浏览器和服务器只需要做一个握手的动作,然后,浏览器和服务器之间就形成了一条快速通道。两者之间就直接可以数据互相传送。","text":"WebSocket 简介 WebSocket 是 HTML5 开始提供的一种在单个 TCP 连接上进行全双工通讯的协议。WebSocket 使得客户端和服务器之间的数据交换变得更加简单,允许服务端主动向客户端推送数据。在 WebSocket API 中,浏览器和服务器只需要完成一次握手,两者之间就直接可以创建持久性的连接,并进行双向数据传输。在 WebSocket API 中,浏览器和服务器只需要做一个握手的动作,然后,浏览器和服务器之间就形成了一条快速通道。两者之间就直接可以数据互相传送。 为什么传统的HTTP协议不能做到WebSocket实现的功能?这是因为HTTP协议是一个请求-响应协议,请求必须先由浏览器发给服务器,服务器才能响应这个请求,再把数据发送给浏览器。换句话说,浏览器不主动请求,服务器是没法主动发数据给浏览器的。 Web我们先用 express 搭一个基础的服务端。 创建 index.js 文件 12345678910var app = require('express')();var http = require('http').createServer(app);app.get('/', function(req, res){ res.send('<h1>Hello world</h1>');});http.listen(3000, function(){ console.log('listening on *:3000');}); run node index.js,并在浏览器打开 http://localhost:3000,访问成功即可看到 HTML设计我们的主页,实现一个简单的聊天窗口。 修改 index.js 123app.get('/', function(req, res){ res.sendFile(__dirname + '/index.html');}); 创建 index.html 12345678910111213141516171819202122<!doctype html><html> <head> <title>Socket.IO chat</title> <style> * { margin: 0; padding: 0; box-sizing: border-box; } body { font: 13px Helvetica, Arial; } form { background: #000; padding: 3px; position: fixed; bottom: 0; width: 100%; } form input { border: 0; padding: 10px; width: 90%; margin-right: .5%; } form button { width: 9%; background: rgb(130, 224, 255); border: none; padding: 10px; } #messages { list-style-type: none; margin: 0; padding: 0; } #messages li { padding: 5px 10px; } #messages li:nth-child(odd) { background: #eee; } </style> </head> <body> <ul id=\"messages\"></ul> <form action=\"\"> <input id=\"m\" autocomplete=\"off\" /><button>Send</button> </form> </body></html> 重启应用并刷新页面你就可以看到一个如下图所示 Socket.io引入socket.io npm install --save socket.io 修改 index.js 123456789101112131415var app = require('express')();var http = require('http').Server(app);var io = require('socket.io')(http);app.get('/', function(req, res){ res.sendFile(__dirname + '/index.html');});io.on('connection', function(socket){ console.log('an user connected');});http.listen(3000, function(){ console.log('listening on *:3000');}); 修改 index.html,在</body>下添加如下代码 1234<script src=\"/socket.io/socket.io.js\"></script><script> var socket = io();</script> 重启应用并刷新页面,就可以看到命令行打印如下 Emitting event当用户在聊天窗的输入框内输入,并提交时,触发 emit 事件,服务端监听到该事件并做出相应的反应。 修改 index.html 12345678910111213<script src=\"/socket.io/socket.io.js\"></script><script src=\"https://code.jquery.com/jquery-1.11.1.js\"></script><script> $(function () { var socket = io(); $('form').submit(function(e){ e.preventDefault(); // 防止页面重新加载 socket.emit('chat message', $('#m').val()); $('#m').val(''); return false; }); });</script> 服务端监听该事件,在 index.js 文件添加该代码 12345io.on('connection', function(socket){ socket.on('chat message', function(msg){ console.log('message: ' + msg); });}); 展示消息当服务端收到用户A发出的消息,服务端重新发出该消息,让客户端接收,客户端监听到该事件后展示该条消息,就可以实现用户 A, B, C 都接收到该消息。 修改 index.js 12345io.on('connection', function(socket){ socket.on('chat message', function(msg){ io.emit('chat message', msg); });}); 修改 index.html,实现消息的展示 1234567891011121314<script> $(function () { var socket = io(); $('form').submit(function(e){ e.preventDefault(); // prevents page reloading socket.emit('chat message', $('#m').val()); $('#m').val(''); return false; }); socket.on('chat message', function(msg){ $('#messages').append($('<li>').text(msg)); }); });</script> 重启应用并刷新页面,可以打开多个浏览器页面同时访问 localhost:3000,就可以体验简单聊天室的效果。 设置昵称 在每个用户进入的时候,随机生成一个数字作为用户的昵称,并且向所有用户广播该用户进入聊天室。当用户发送消息时,拼接上用户的昵称。 修改 index.js 12345678io.on('connection', (socket) => { const nickname = 'user' + Math.ceil((Math.random() * 1000)) socket.broadcast.emit('connection', nickname + ' connected') socket.on('chat message', (msg) => { io.emit('chat message', nickname + ': ' + msg) })}) 修改 index.html,监听 connection 事件 1234567891011121314151617<script> $(() => { const socket = io() $('form').submit((e) => { e.preventDefault() socket.emit('chat message', $('#m').val()) $('#m').val('') return false }); socket.on('chat message', (msg) => { $('#messages').append($('<li>').text(msg)) }) socket.on('connection', (msg) => { $('#messages').append($('<li>').text(msg)) }) }); </script> 重启应用并打开多个客户端,可以看到如下效果 参考资料本文大部分案例出自 socket.io 的入门文档 https://socket.io/get-started/chat/ https://en.wikipedia.org/wiki/WebSocket 廖雪峰官网 WebSocket 菜鸟教程 WebSocket","categories":[],"tags":[{"name":"WebSocket","slug":"WebSocket","permalink":"https://www.chaohang.top/tags/WebSocket/"}]},{"title":"微信支付开发实记","slug":"微信支付开发实记","date":"2018-11-19T08:50:48.000Z","updated":"2018-11-19T08:50:48.000Z","comments":true,"path":"2018/11/19/微信支付开发实记/","link":"","permalink":"https://www.chaohang.top/2018/11/19/%E5%BE%AE%E4%BF%A1%E6%94%AF%E4%BB%98%E5%BC%80%E5%8F%91%E5%AE%9E%E8%AE%B0/","excerpt":"微信支付分为JSAPI支付,扫码支付,APP支付,小程序支付等不同的支付方式。但大体的支付过程是一致的,本文以JSAPI支付,也就是微信内的H5支付为例,描述一下支付的整个开发流程。 配置商户需要提前开通商户平台,并去公众平台或开放平台提交微信支付申请,获得商户号和秘钥。 详细文档可以看这里","text":"微信支付分为JSAPI支付,扫码支付,APP支付,小程序支付等不同的支付方式。但大体的支付过程是一致的,本文以JSAPI支付,也就是微信内的H5支付为例,描述一下支付的整个开发流程。 配置商户需要提前开通商户平台,并去公众平台或开放平台提交微信支付申请,获得商户号和秘钥。 详细文档可以看这里 支付流程 微信支付的流程图画的很完整,开发前要把整个流程研究清楚。 整个流程,服务端需要做的有三件事。 前端支付按钮被触发后,服务端要去调用 统一下单 接口,把预付单信息、支付参数和参数签名返回给前端。前端根据这些参数唤起支付。 当用户支付成功后,微信会给我们一个回调通知,告知我们支付结果。这一步要实现“完成订单”操作,标记用户已经成功支付,进入“发货”流程。 提供一个查询接口,让前端再次确认是否支付成功。 统一下单在支付前,商户系统先调用该接口在微信支付后台生成预支付交易单,同样的,商户系统也需要在自己的表里记录一笔“未完成订单”。生成之后返回正确的预付单信息、支付参数和参数签名返回给前端。前端根据这些参数唤起支付。 接口 https://api.mch.weixin.qq.com/pay/unifiedorder参数巨多,具体还是看文档 这里需要说明的一点是,我们在调用这个接口时,需要签一次名用来给微信做校验,微信也返回了一个新的签名用来给我们做校验,然后我们还要返回给前端一个签名,用来唤起支付。这三个签名都不是同一个。 我们不能直接把调用统一下单接口返回的签名返回给前端,而是根据前端唤起支付的参数去重新签名。 注意,是根据前端唤起支付的参数去重新签名,因为前端的参数名和后端的参数名会略微有差别,这里需要小心。 说明:签名的意图是用来校验身份,当前端把这些参数传给微信,微信会把调用参数除去签名后重新签名,用来校验签名的正确性,所以用来签名的参数名要和前端参数一致。 支付结果通知在统一下单时我们填了一个参数叫 notify_url,这是一个服务端的接口地址,微信在用户支付成功后,会回调这个地址,告知我们支付结果。 详情看文档 在这一步还是需要做多点校验的,免得被人有机可乘。 校验支付是否成功,不成功直接返回”FAIL” 校验签名和appid 校验订单是否完成(幂等校验,防止微信多次回调导致多次订单写入) 订单金额校验 一通校验完事之后就可以做业务相关的事了。记得所有操作结束后返回”SUCCESS”,不然微信会不断发起回调。 总结 流程图描述的很清楚,要仔细阅读流程图。 调用完下单接口后要进行二次签名,签名的参数要看前端验签用哪些参数,即使是同一个参数,字段名也会跟第一次加签不一样。 前端支付完成之后微信会有一个回调,我们需要做以下几点校验: 做幂等处理(因为同样的通知微信可能发送多次)。 校验签名,校验APPID。 校验订单金额。 订单状态分为 0-未支付 1-支付完成 2-支付失败: 用户触发支付组件然后关闭或者杀掉进程微信不会给到后台任何回复,始终处于 0-未支付,所以这个状态也是一个支付失败状态。 支付失败比较少见(到现在没有遇到过),比如签名错误(发生在调试阶段)。 微信回执表尽量详细的记录微信传回的所有必有参数,以备出问题时排查,可以直接丢到mongodb里。 本文首发于我的个人博客 https://chaohang.top 作者张小超 转载请注明出处","categories":[],"tags":[{"name":"wechat","slug":"wechat","permalink":"https://www.chaohang.top/tags/wechat/"}]},{"title":"微信网页授权","slug":"微信网页授权","date":"2018-11-19T06:13:36.000Z","updated":"2018-11-19T06:13:36.000Z","comments":true,"path":"2018/11/19/微信网页授权/","link":"","permalink":"https://www.chaohang.top/2018/11/19/%E5%BE%AE%E4%BF%A1%E7%BD%91%E9%A1%B5%E6%8E%88%E6%9D%83/","excerpt":"背景用户在微信客户端中访问第三方网页,公众号可以通过微信网页授权机制,来获取用户基本信息,进而实现业务逻辑。 配置 在微信公众号请求用户网页授权之前,开发者需要先到公众平台官网中的“开发 - 接口权限 - 网页服务 - 网页帐号 - 网页授权获取用户基本信息”的配置选项中,修改授权回调域名。请注意,这里填写的是域名(是一个字符串),而不是URL,因此请勿加 http:// 等协议头; 授权回调域名配置规范为全域名,比如需要网页授权的域名为:www.qq.com,配置以后此域名下面的页面http://www.qq.com/music.html 、 http://www.qq.com/login.html 都可以进行OAuth2.0鉴权。但http://pay.qq.com 、 http://music.qq.com 、 http://qq.com无法进行OAuth2.0鉴权","text":"背景用户在微信客户端中访问第三方网页,公众号可以通过微信网页授权机制,来获取用户基本信息,进而实现业务逻辑。 配置 在微信公众号请求用户网页授权之前,开发者需要先到公众平台官网中的“开发 - 接口权限 - 网页服务 - 网页帐号 - 网页授权获取用户基本信息”的配置选项中,修改授权回调域名。请注意,这里填写的是域名(是一个字符串),而不是URL,因此请勿加 http:// 等协议头; 授权回调域名配置规范为全域名,比如需要网页授权的域名为:www.qq.com,配置以后此域名下面的页面http://www.qq.com/music.html 、 http://www.qq.com/login.html 都可以进行OAuth2.0鉴权。但http://pay.qq.com 、 http://music.qq.com 、 http://qq.com无法进行OAuth2.0鉴权 两种授权方式微信提供了两种授权方式来应对不同的使用场景。通过网页授权作用域 scope 参数控制。 静默授权 静默授权:用户进入页面后自动授权并跳转回页面,这种授权对用户无感知。通过这种授权我们只能获取到用户的 openid,无法获得用户的其他信息。 使用场景:只需要标识该用户,而不需要收集其他信息的场景都可以使用。比如投票、点赞等场景。 scope: snsapi_base 主动授权 主动授权:用户进入页面后会有授权弹窗,需要手动同意。该方试用来获取用户的基本信息 注意:对于已关注公众号的用户,用户从公众号的会话或者自定义菜单进入本公众号的网页授权页,即使是scope: snsapi_userinfo,也是静默授权,用户无感知。 scope: snsapi_userinfo 具体流程 用户进入页面,获取 code 通过 code 换取网页授权 access_token 和 openid,静默授权到这步结束 通过 access_token 获取用户信息 获取CODE引导用户跳传至该链接 https://open.weixin.qq.com/connect/oauth2/authorize?appid=APPID&redirect_uri=REDIRECT_URI&response_type=code&scope=SCOPE&state=STATE 用户同意授权或静默授权后将跳转至 redirect_url?code=CODE&state=STATE 这个地址并带上生成的 code。 code 作为换取access_token的票据,只能使用一次,5分钟未使用自动过期。 参数说明 通过 code 换取网页授权 access_token注意,这里的 access_token 和 调用公众平台API所需的基础 access_token不是一个东东,只是名字一样而已。 这一步是在服务端完成的,需要公众号的 secret,access_token 也不可以传给客户端。 请求接口 https://api.weixin.qq.com/sns/oauth2/access_token?appid=APPID&secret=SECRET&code=CODE&grant_type=authorization_code 返回 1234567{ "access_token":"ACCESS_TOKEN", "expires_in":7200, "refresh_token":"REFRESH_TOKEN", "openid":"OPENID", "scope":"SCOPE" } 拉取用户信息(需scope为snsapi_userinfo)当网页授权作用域为 snsapi_userinfo时,开发者通过 access_token 和 openid 获取用户信息。 请求接口 https://api.weixin.qq.com/sns/userinfo?access_token=ACCESS_TOKEN&openid=OPENID&lang=zh_CN 返回 1234567891011{ \"openid\": \"OPENID\", \"nickname\": \"NICKNAME\", \"sex\": \"1\", \"province\": \"PROVINCE\" \"city\": \"CITY\", \"country\": \"COUNTRY\", \"headimgurl\": \"http://thirdwx.qlogo.cn/mmopen/g3MonUZtNHkdmzicIlibx6iaFqAc56vxLSUfpb6n5WKSYVY0ChQKkiaJSgQ1dZuTOgvLLrhJbERQQ4eMsv84eavHiaiceqxibJxCfHe/46\", \"privilege\": [ \"PRIVILEGE1\" \"PRIVILEGE2\" ], \"unionid\": \"o6_bmasdasdsad6_2sgVt7hMZOPfL\"} 参考资料微信网页授权官方文档 本文首发于我的个人博客 https://chaohang.top 作者张小超 转载请注明出处","categories":[],"tags":[{"name":"wechat","slug":"wechat","permalink":"https://www.chaohang.top/tags/wechat/"}]},{"title":"小程序登录及用户信息和手机号的获取","slug":"小程序登录及用户信息和手机号的获取","date":"2018-11-16T06:28:58.000Z","updated":"2018-11-16T06:28:58.000Z","comments":true,"path":"2018/11/16/小程序登录及用户信息和手机号的获取/","link":"","permalink":"https://www.chaohang.top/2018/11/16/%E5%B0%8F%E7%A8%8B%E5%BA%8F%E7%99%BB%E5%BD%95%E5%8F%8A%E7%94%A8%E6%88%B7%E4%BF%A1%E6%81%AF%E5%92%8C%E6%89%8B%E6%9C%BA%E5%8F%B7%E7%9A%84%E8%8E%B7%E5%8F%96/","excerpt":"登录流程 小程序通过 wx.login 获取 code(用户登录凭证,有效期五分钟,使用一次后实效) 服务端拿到 code,调用 code2Session,获取用户的 openid (用户在当前应用的唯一标识)、unionid (用户在开发平台的唯一标识) session_key (会话秘钥是对用户数据进行 加密签名 的秘钥,在获取用户信息时会用到) 等信息。为了安全,服务器不应该把会话秘钥下发到小程序,也不应该对外提供这个秘钥 通过 unionid openid 自定义服务端登录态","text":"登录流程 小程序通过 wx.login 获取 code(用户登录凭证,有效期五分钟,使用一次后实效) 服务端拿到 code,调用 code2Session,获取用户的 openid (用户在当前应用的唯一标识)、unionid (用户在开发平台的唯一标识) session_key (会话秘钥是对用户数据进行 加密签名 的秘钥,在获取用户信息时会用到) 等信息。为了安全,服务器不应该把会话秘钥下发到小程序,也不应该对外提供这个秘钥 通过 unionid openid 自定义服务端登录态 获取用户信息 用户授权后,小程序通过 wx.getUserInfo 获取用户信息,敏感信息被加密 把加密信息传给服务端,使用在登录时获取的 session_key 解密数据,解密方法可以看微信的 开放数据校验与解密文档 获取手机号需要用户主动触发才能发起获取手机号接口,所以该功能不由 API 来调用,需用 <button> 组件的点击来触发。获取手机号文档。 小程序通过 <button> 组件触发 getPhoneNumber 事件,获得加密后的信息 把加密信息传给服务端,使用在登录时获取的 session_key 解密数据,获得用户绑定的手机号 下面是我实际开发中画的一张前后端交互的流程图,欢迎指出不足指出。 参考文档小程序开发文档 本文首发于我的个人博客 https://chaohang.top 作者张小超 转载请注明出处","categories":[],"tags":[{"name":"wechat","slug":"wechat","permalink":"https://www.chaohang.top/tags/wechat/"}]},{"title":"Node.js 获取微信JS-SDK CONFIG","slug":"Node-js-获取微信JS-SDK-CONFIG","date":"2018-11-07T10:44:57.000Z","updated":"2018-11-07T10:44:57.000Z","comments":true,"path":"2018/11/07/Node-js-获取微信JS-SDK-CONFIG/","link":"","permalink":"https://www.chaohang.top/2018/11/07/Node-js-%E8%8E%B7%E5%8F%96%E5%BE%AE%E4%BF%A1JS-SDK-CONFIG/","excerpt":"背景前端在调用微信提供的分享、拍照、扫一扫等功能时需要到后台获取配置,主要是签名(signature)。Node 开发可以用朴灵大佬的SDK——co-wechat-api。 配置到微信公众平台进入“公众号设置”的“功能设置”里填写“JS接口安全域名”。前提是该域名的项目目录下有微信提供的校验文件。如果需要本地调试或者测试环境调试,可以把IP白名单也顺便配置一下。","text":"背景前端在调用微信提供的分享、拍照、扫一扫等功能时需要到后台获取配置,主要是签名(signature)。Node 开发可以用朴灵大佬的SDK——co-wechat-api。 配置到微信公众平台进入“公众号设置”的“功能设置”里填写“JS接口安全域名”。前提是该域名的项目目录下有微信提供的校验文件。如果需要本地调试或者测试环境调试,可以把IP白名单也顺便配置一下。 开发实例化 WechatAPI 对象 1234567891011const WECHAT_API = new WechatAPI( appid, secret, get_access_token, // function save_access_token, // function 缓存 access_token);WECHAT_API.registerTicketHandle( get_js_api_ticket, // function save_js_api_ticket, // function 缓存 jsapi_ticket); 注意:jsapi_ticket 是公众号用于调用微信JS接口的临时票据。正常情况下,jsapi_ticket 的有效期为7200秒,通过 access_token 来获取。由于获取 jsapi_ticket 的api调用次数非常有限,频繁刷新 jsapi_ticket 会导致api调用受限,影响自身业务,开发者必须在自己的服务全局缓存 jsapi_ticket。同样的,access_token 有效期也是7200秒,同样需要全局缓存。 缓存的函数就是简单的 JSON.stringify 后存入 redis 或其他数据库。 调用123456const params = { debug: false, jsApiList: [], // 需要获取的能力,由前端传入 url: '', // 申请能力并且在后台配置过的域名};ctx.body = await WECHAT_API.getJsConfig(params); 接入多个微信公众号待我重构归来。。。。。。 本文首发于我的个人博客 https://chaohang.top 作者张小超 转载请注明出处","categories":[],"tags":[{"name":"JavaScript","slug":"JavaScript","permalink":"https://www.chaohang.top/tags/JavaScript/"},{"name":"node.js","slug":"node-js","permalink":"https://www.chaohang.top/tags/node-js/"},{"name":"wechat","slug":"wechat","permalink":"https://www.chaohang.top/tags/wechat/"}]},{"title":"代码顺序的重要性","slug":"代码顺序的重要性","date":"2018-11-07T10:43:57.000Z","updated":"2018-11-07T10:43:57.000Z","comments":true,"path":"2018/11/07/代码顺序的重要性/","link":"","permalink":"https://www.chaohang.top/2018/11/07/%E4%BB%A3%E7%A0%81%E9%A1%BA%E5%BA%8F%E7%9A%84%E9%87%8D%E8%A6%81%E6%80%A7/","excerpt":"背景在修改前辈留下来的代码时踩到一个坑。简单讲一下代码做了什么事:在 try/catch 里实例化多个类,并调用类的方法为其赋值,catch 到错误也不会中断,而是通过打日志的形式收集报错信息。 我需要在里面新实例化一个类,有样学样,上来就是一顿copy改,可是新增的类的某个属性总是不对,排查了半天才发现一个坑。","text":"背景在修改前辈留下来的代码时踩到一个坑。简单讲一下代码做了什么事:在 try/catch 里实例化多个类,并调用类的方法为其赋值,catch 到错误也不会中断,而是通过打日志的形式收集报错信息。 我需要在里面新实例化一个类,有样学样,上来就是一顿copy改,可是新增的类的某个属性总是不对,排查了半天才发现一个坑。 错误模拟12345678910111213141516171819202122232425262728class Point { constructor(x, y) { this.x = x this.y = y } getX() { return this.x } setX(x) { this.x = x }}let p, cattry { p = new Point(1, 2) cat = new Cat('胖虎') // 这里实例化了一个不存在的类,用来模拟报错,实际情况比这个复杂 p.setX(66) // 这一行应该写在 `cat` 的前面,不然 `cat` 报错后直接中断下面语句的执行,导致这行语句没有执行} catch (e) { console.log(e)}console.log(p.getX()) 执行结果,x 没有被赋值成66,抛出错误导致程序中断,赋值语句没有执行 正确的写法12345678910111213141516171819202122232425262728class Point { constructor(x, y) { this.x = x this.y = y } getX() { return this.x } setX(x) { this.x = x }}let p, cattry { p = new Point(1, 2) p.setX(66) // 方法调用紧随实例后面,不受下面代码的影响 cat = new Cat('胖虎')} catch (e) { console.log(e)}console.log(p.getX()) 执行结果,x 正确被赋值,错误也正常抛出 总结 如果 catch 了错误又不抛出,可以在调试的时候把错误抛出,方便发现问题。 代码结构顺序和整洁性很重要。 如果出现这种傻瓜式错误,是该考虑重构代码了。","categories":[],"tags":[{"name":"JavaScript","slug":"JavaScript","permalink":"https://www.chaohang.top/tags/JavaScript/"}]},{"title":"七牛视频防盗链处理","slug":"七牛视频防盗链处理","date":"2018-11-02T06:41:40.000Z","updated":"2018-11-02T06:41:40.000Z","comments":true,"path":"2018/11/02/七牛视频防盗链处理/","link":"","permalink":"https://www.chaohang.top/2018/11/02/%E4%B8%83%E7%89%9B%E8%A7%86%E9%A2%91%E9%98%B2%E7%9B%97%E9%93%BE%E5%A4%84%E7%90%86/","excerpt":"背景公司在做付费课程,视频教学为主,需要对视频链接作防盗链处理。 REFERERHTTP referer 是 header 上的一个属性。当浏览器向服务器发起请求时,一般会带上Referer,告诉服务器我是从哪个页面链接过来的。 七牛在融合 CDN -> 域名管理 -> 高级配置里有一个域名防盗链选项。 配置好之后就可以限制链接只能从配置好的白名单域名进行访问,简单的防御了盗链。但是HTTP header 是可以被伪造的,经测试,直接用 postman 加个 Referer 就可以绕过了。","text":"背景公司在做付费课程,视频教学为主,需要对视频链接作防盗链处理。 REFERERHTTP referer 是 header 上的一个属性。当浏览器向服务器发起请求时,一般会带上Referer,告诉服务器我是从哪个页面链接过来的。 七牛在融合 CDN -> 域名管理 -> 高级配置里有一个域名防盗链选项。 配置好之后就可以限制链接只能从配置好的白名单域名进行访问,简单的防御了盗链。但是HTTP header 是可以被伪造的,经测试,直接用 postman 加个 Referer 就可以绕过了。 私有空间七牛云提供了私有空间,空间内的文件对象都要获得拥有者的授权才能进行访问,并且可以设置链接的有效时间,超过时长后自动失效(视频会有缓存,清空缓存之前视频仍然有效)。 如何创建私有空间的访问链接,下面是 Node.js SDK 的一段示例代码,其他语言可以去官网查看。 123456var mac = new qiniu.auth.digest.Mac(accessKey, secretKey);var config = new qiniu.conf.Config();var bucketManager = new qiniu.rs.BucketManager(mac, config);var privateBucketDomain = 'http://if-pri.qiniudn.com';var deadline = parseInt(Date.now() / 1000) + 3600; // 1小时过期var privateDownloadUrl = bucketManager.privateDownloadUrl(privateBucketDomain, key, deadline); 注意 测试域名不能用于私有空间。 自定义域名必须开启回源鉴权。 播放 hls 文件需要使用 pm3u8 服务。 详情可见这里:私有空间的注意事项。 PM3U8视频切片(七牛视频切片)后放在私有仓库时,获取单段视频的链接也需要带上token。七牛提供了 pm3u8服务 ,对 m3u8文件中的 ts 资源进行批量下载授权。通过将 ts 资源的 url 改写成私有 url,以临时获取访问权限。 有个需要注意的地方是,做签名处理时,要对整个链接做签名,比如 https://test.com/12354.m3u8?pm3u8/0。 123456var privateBucketDomain = 'http://if-pri.qiniudn.com';var deadline = parseInt(Date.now() / 1000) + 3600; // 1小时过期var key = 12354.m3u8?pm3u8/0; // pm3u8 参数和 url 一起做签名var privateDownloadUrl = bucketManager.privateDownloadUrl(privateBucketDomain, key, deadline); 还有个需要注意的地方是,虽然官方文档有一句这种提示,但和七牛沟通后的结果是,pm3u8 服务也是支持 https 的。 总结使用 REFERER + 私有空间 已经可以解决盗链问题了,但是如果视频被下载仍然是个问题。需要对视频做加密处理,但是即使做了加密处理,也阻止不了录屏。完全杜绝视频泄露是很难的,或者我们可以采取其他方式加大盗用的成本,比如在视频里加上水印或者观看者ID,甚至让水印或者观看者ID在视频里四处游走(影响观看体验)。 本文首发于我的个人博客 https://chaohang.top 作者张小超 转载请注明出处","categories":[],"tags":[{"name":"JavaScript","slug":"JavaScript","permalink":"https://www.chaohang.top/tags/JavaScript/"},{"name":"node.js","slug":"node-js","permalink":"https://www.chaohang.top/tags/node-js/"},{"name":"七牛","slug":"七牛","permalink":"https://www.chaohang.top/tags/%E4%B8%83%E7%89%9B/"}]},{"title":"白夜行","slug":"白夜行","date":"2018-10-31T01:51:58.000Z","updated":"2018-10-31T01:51:58.000Z","comments":true,"path":"2018/10/31/白夜行/","link":"","permalink":"https://www.chaohang.top/2018/10/31/%E7%99%BD%E5%A4%9C%E8%A1%8C/","excerpt":"“我呢,”雪穗继续说,“从来就没有生活在太阳底下。” “我的天空里没有太阳,总是黑夜,但并不暗,因为有东西代替了太阳。虽然没有太阳那么明亮,但对我来说已经足够。凭借着这份光,我便能把黑夜当成白天。你明白吗?我从来没有太阳,所以不害怕失去。” ——《白夜行》东野圭吾。 开头是一宗离奇的命案,调查以无任何结果告终。然后开始分别叙述两位主人公的生活。从十一岁到初中到高中到成人,时间跨度长达十九年。每一件事单独看都是一个精彩的故事,谁知道最后所有的事情都会串起来成为解答开头命案的线索。本来以为是两个互不相关的两个人,作者在分别叙述他们的故事,看起来毫无相关性。看到最后才知道两个人始终在一起,策划着每一个意外。","text":"“我呢,”雪穗继续说,“从来就没有生活在太阳底下。” “我的天空里没有太阳,总是黑夜,但并不暗,因为有东西代替了太阳。虽然没有太阳那么明亮,但对我来说已经足够。凭借着这份光,我便能把黑夜当成白天。你明白吗?我从来没有太阳,所以不害怕失去。” ——《白夜行》东野圭吾。 开头是一宗离奇的命案,调查以无任何结果告终。然后开始分别叙述两位主人公的生活。从十一岁到初中到高中到成人,时间跨度长达十九年。每一件事单独看都是一个精彩的故事,谁知道最后所有的事情都会串起来成为解答开头命案的线索。本来以为是两个互不相关的两个人,作者在分别叙述他们的故事,看起来毫无相关性。看到最后才知道两个人始终在一起,策划着每一个意外。 大师就是大师,把这么多繁复的事情一一叙述,中间用一条几乎不可见的线串联起来,看到最后只能张大嘴巴惊讶,原来竟是这样。 本来以为唐泽雪穗是一步一步黑化,其实在她十一岁被母亲卖给一个个男人,她的灵魂就已经被夺走了。她也用同样的方法夺走了三个女孩的灵魂,同时又达到自己的目的。而桐原亮司是一直爱着她的,他总是在人们看不见的地方守护雪穗。书的结尾描写他装扮成圣诞老人在雪穗新开的店里,应该是书中第一次描写两人同时出现在同一个地点,也是最后一次。被警察发现后抱着必死的决心跳楼,怕跳楼可能死不了还用自己心爱的剪刀插在心脏。桐原亮司要杀一个人确实从未失手过,包括他自己。 看一遍没办法看清细节,更何况我是有空才看一二十分钟,前后文间隔时间有点长,有的情节容易忘掉。下次再看一定要捧着实体书,在一个有阳光的午后,一次性看完。不过再次看时已经知道了每个人物身份每个情节,应该没了第一次看时的惊讶和刺激了吧。那就等我把情节忘记得差不多的时候,再重新读一遍吧。","categories":[],"tags":[{"name":"随笔","slug":"随笔","permalink":"https://www.chaohang.top/tags/%E9%9A%8F%E7%AC%94/"},{"name":"阅读","slug":"阅读","permalink":"https://www.chaohang.top/tags/%E9%98%85%E8%AF%BB/"}]},{"title":"《码农翻身》读书笔记","slug":"《码农翻身》读书笔记","date":"2018-10-28T12:14:14.000Z","updated":"2018-10-28T12:14:14.000Z","comments":true,"path":"2018/10/28/《码农翻身》读书笔记/","link":"","permalink":"https://www.chaohang.top/2018/10/28/%E3%80%8A%E7%A0%81%E5%86%9C%E7%BF%BB%E8%BA%AB%E3%80%8B%E8%AF%BB%E4%B9%A6%E7%AC%94%E8%AE%B0/","excerpt":"花了一天时间翻完了《码农翻身》(跳过了Java部分)。生动有趣的书看起来就是舒服,竟有引人入胜、手不释卷的感觉,一不小心就翻完了。是一本很好的入门科普读物,开阔技术视野。 下面是书中的摘抄和笔记 线程我是一个线程 不知道什么时候会被挑中执行 在执行的过程中随时可能被打断,让出CPU 一旦出现硬盘、数据库这样耗时的操作,也得让出CPU 数据来了,也不一定马上执行,还得等着被CPU挑选 三次握手TCP/IP之大明内阁TCP/IP之蓟辽督师TCP/IP 之 大明王朝邮差 发送的信息被拆分成小包裹,编上序号,打乱顺序后发送,由服务端发送,客户端接收,接收后返回确认接收信息。如果一定时间内没有收到确认信息,则代表信息丢失(可能是发出的包裹丢失,也可能是返回的确认信息丢失),重新发送该包裹。全部接收完成后按照序号重新组装成完整信息。 HTTPS一个故事讲完https","text":"花了一天时间翻完了《码农翻身》(跳过了Java部分)。生动有趣的书看起来就是舒服,竟有引人入胜、手不释卷的感觉,一不小心就翻完了。是一本很好的入门科普读物,开阔技术视野。 下面是书中的摘抄和笔记 线程我是一个线程 不知道什么时候会被挑中执行 在执行的过程中随时可能被打断,让出CPU 一旦出现硬盘、数据库这样耗时的操作,也得让出CPU 数据来了,也不一定马上执行,还得等着被CPU挑选 三次握手TCP/IP之大明内阁TCP/IP之蓟辽督师TCP/IP 之 大明王朝邮差 发送的信息被拆分成小包裹,编上序号,打乱顺序后发送,由服务端发送,客户端接收,接收后返回确认接收信息。如果一定时间内没有收到确认信息,则代表信息丢失(可能是发出的包裹丢失,也可能是返回的确认信息丢失),重新发送该包裹。全部接收完成后按照序号重新组装成完整信息。 HTTPS一个故事讲完https 这里面讲HTTPS讲的非常好,通俗易懂有趣。 bill的公钥及身份信息通过HTTPS传输给CA服务器,并生成数字证书。 验证完证书后,除了对称秘钥是用非对称加密传输,其他请求都使用对称加密通信。 如何确保CA公钥的安全CA本身也有证书来证明自己的身份,并且CA的信用是像树一样分级的,高层的CA给底层的CA做信用背书,而操作系统/浏览器中会内置一些顶层的CA的证书,相当于你自动信任了他们。 这些顶层的CA证书一定得安全地放入操作系统/浏览器当中,否则世界大乱。 OTHER机房夜话 单点登录也讲得妙趣横生 从密码到token, 一个授权的故事 第三方授权,接入微信服务会经常用到。 张大胖的socket 端口号为了区分同一台服务器的不同进程。进程号是动态的,如果服务器端的进程重启了,进程号就变了。端口号就是从来标识服务器端的进程。 老司机的忠告 凡事必先骑虎上背 局部性原理(1)时间局部性:如果程序中的某条指令一旦执行,则不久之后该条指令可能再次被执行;如果某数据被访问,则不久之后该数据可能再次被访问。(2)空间局部性:是指一旦程序访问了某个存储单元,则不久之后,其附近的存储单元也将被访问。 抽象是计算机科学中极为重要的武器之一 要透彻地理解一门技术的本质 写漂亮的代码 书籍里的知识,体系更加完整,有深度——看书好过碎片阅读 不要游走在各大语言的边缘 坚持写作(输出)","categories":[],"tags":[{"name":"阅读","slug":"阅读","permalink":"https://www.chaohang.top/tags/%E9%98%85%E8%AF%BB/"}]},{"title":"看见","slug":"看见","date":"2018-10-19T16:38:46.000Z","updated":"2018-10-19T16:38:46.000Z","comments":true,"path":"2018/10/20/看见/","link":"","permalink":"https://www.chaohang.top/2018/10/20/%E7%9C%8B%E8%A7%81/","excerpt":"花了半个月断断续续看完了柴静的《看见》。这本书值得重新再看一次。 这次看这本书,用手机看,一目十行,有时候看着看着走神了,有时候动动手指就多翻了几页,或者在电梯里地铁里吃饭排队时匆匆看两眼。碎片时间让文章没了上下文,没了沉浸感。用手机看书是方便,随时随地,但不适合看一些有深度的书。 这本书是一个时代的记忆——非典,汶川地震,奥运。也记录了各式各样的人和群体——双城集体服毒的小学生,戒毒所里被卖去卖淫的女人,采访同性恋,不知道犯了什么罪就被关了二十八年的人,因家庭暴力杀死丈夫的女人,山西的环境污染,采访用高跟鞋踩死小猫拍成视频传到网上的女人,华南虎照片真假事件,福建涂岭拆迁户不同意搬迁,户主的儿子——一个刚复员回来的年轻人,二十三岁——被开发商砍死,农村征地问题,采访卢安克,药家鑫事件。","text":"花了半个月断断续续看完了柴静的《看见》。这本书值得重新再看一次。 这次看这本书,用手机看,一目十行,有时候看着看着走神了,有时候动动手指就多翻了几页,或者在电梯里地铁里吃饭排队时匆匆看两眼。碎片时间让文章没了上下文,没了沉浸感。用手机看书是方便,随时随地,但不适合看一些有深度的书。 这本书是一个时代的记忆——非典,汶川地震,奥运。也记录了各式各样的人和群体——双城集体服毒的小学生,戒毒所里被卖去卖淫的女人,采访同性恋,不知道犯了什么罪就被关了二十八年的人,因家庭暴力杀死丈夫的女人,山西的环境污染,采访用高跟鞋踩死小猫拍成视频传到网上的女人,华南虎照片真假事件,福建涂岭拆迁户不同意搬迁,户主的儿子——一个刚复员回来的年轻人,二十三岁——被开发商砍死,农村征地问题,采访卢安克,药家鑫事件。 还有她的恩师,陈虻。严苛,多批评很少夸奖,又恰好激发了潜力和斗志。能够遇到这样的引路人,我们只有羡慕的份。 这么多事情里,贯穿这本书的是:如何做一个合格的记者。书到结尾,没有答案,而是作者每个阶段对记者不同的理解。 是不是应该客观不带丝毫感情,要引导采访对象而不是咄咄逼问。要如何做一个记者,让形形色色的人愿意在你面前,在镜头面前吐露心声。如何知道得到的回答是受访者的本意,而非受记者有意引导。 往大了讲,如何辨别真假新闻——这是连特朗普都头疼的事。对于这个问题我就不敢乱说了,带着怀疑的态度看问题,有调查的新闻才是好新闻。 之前没有看过柴进的报道,特地去找了一集看一下。《双城的创伤》,小孩的内心世界往往比想象中的复杂,在心智还没完全成熟的时候,一些事对孩子来说可能是很大很难的事。柴静在采访结尾说:”双城事件调查到最后,我们发现,最大的迷,其实是孩子的内心世界,能不能打开它,可能是每个人都需要面对的问题。” 书摘 人们声称的最美好的岁月其实都是最痛苦的 无卷宗,无判决,无罪名,无期限。他被关了二十八年。 在中国,一个男人仍然可以打一个女人,用刀砍她的手,用酒瓶子扎她的眼睛,用枪抵住她的后背,强暴她的姐妹,殴打她的孩子。他甚至在众人面前这样子做,不会受到惩罚——只因为他是她的丈夫。 开始是不宣布对军队与政府不利的消息,后来发展为对军队和政府有利的消息要大力宣传。那些敢于坚持独立性、发出不同声音的报业成为受害者。 不再坐在教室里被动地接收别人灌输给我的东西了,而是主动地去了解。你们这些真正了解战争的人,请多告诉我们一些。 钱老师送这本书给我,我明白他当年让我读历史的原因:“新旧之间没有怨讼,唯有真与伪是大敌。” 宽容的基础是理解,你理解吗? 你有自己认识事物的坐标体系吗? 不要用道德的眼光看经济问题。 专制就是坚信自己是不会错的想法。 “你挺容忍的。”她说,“但你心里还是有委屈。”这话说得我一怔。委屈,这个词,好像心里有一只捏紧的小拳头。","categories":[],"tags":[{"name":"随笔","slug":"随笔","permalink":"https://www.chaohang.top/tags/%E9%9A%8F%E7%AC%94/"},{"name":"阅读","slug":"阅读","permalink":"https://www.chaohang.top/tags/%E9%98%85%E8%AF%BB/"}]},{"title":"Node.js 使用 RSA 做加密","slug":"Node-js-使用-RSA-做加密","date":"2018-09-25T10:41:42.000Z","updated":"2018-09-25T10:41:42.000Z","comments":true,"path":"2018/09/25/Node-js-使用-RSA-做加密/","link":"","permalink":"https://www.chaohang.top/2018/09/25/Node-js-%E4%BD%BF%E7%94%A8-RSA-%E5%81%9A%E5%8A%A0%E5%AF%86/","excerpt":"RSARSA加密算法是一种非对称加密算法。 假设 A 与 B 通信。A 和 B 都提供一个公开的公钥。A 把需要传递的信息,先用自己的私钥签名,再用 B 的公钥加密。B 接收到这串密文后,用自己的私钥解密,用 A 提供的公钥验签。 为什么要先签名后加密?如果你先加密后签名,非法用户通过获取的公钥就可以破解签名,破解之后就可以替换签名。 详细的原理可以参考以下文档:RSA算法原理(一)RSA算法原理(二) node-rsa在 node.js 中使用 rsa 算法,我们使用的是 node-rsa 这个包。 12345678910111213141516171819202122232425262728293031const NodeRSA = require('node-rsa');const a_public_key_data = '-----BEGIN PUBLIC KEY----- ... -----END PUBLIC KEY-----';const a_private_key_data = '-----BEGIN PRIVATE KEY----- ... -----END PRIVATE KEY-----';const b_public_key_data = '-----BEGIN PUBLIC KEY----- ... -----END PUBLIC KEY-----';const b_private_key_data = '-----BEGIN PRIVATE KEY----- ... -----END PRIVATE KEY-----';// 生成 A 的公私钥对象const a_public_key = new NodeRSA(a_public_key_data);const a_private_key = new NodeRSA(a_private_key_data);// 生成 B 的公私钥对象const a_public_key = new NodeRSA(a_public_key_data);const a_private_key = new NodeRSA(a_private_key_data);const text = 'Hello RSA!';// 加签并加密const sign = a_private_key.sign(text, 'base64', 'utf8');console.log('A 私钥加签:', sign);const encrypted = a_public_key.encrypt(sign, 'base64');console.log('B 公钥加密:', encrypted);// 解密并验签const decrypted = a_public_key.decrypt(encrypted, 'utf8');console.log('B 私钥解密:', decrypted);const verify = a_public_key.verify(text, decrypted, 'utf8', 'base64');console.log('A 公钥验签:', verify); serialize","text":"RSARSA加密算法是一种非对称加密算法。 假设 A 与 B 通信。A 和 B 都提供一个公开的公钥。A 把需要传递的信息,先用自己的私钥签名,再用 B 的公钥加密。B 接收到这串密文后,用自己的私钥解密,用 A 提供的公钥验签。 为什么要先签名后加密?如果你先加密后签名,非法用户通过获取的公钥就可以破解签名,破解之后就可以替换签名。 详细的原理可以参考以下文档:RSA算法原理(一)RSA算法原理(二) node-rsa在 node.js 中使用 rsa 算法,我们使用的是 node-rsa 这个包。 12345678910111213141516171819202122232425262728293031const NodeRSA = require('node-rsa');const a_public_key_data = '-----BEGIN PUBLIC KEY----- ... -----END PUBLIC KEY-----';const a_private_key_data = '-----BEGIN PRIVATE KEY----- ... -----END PRIVATE KEY-----';const b_public_key_data = '-----BEGIN PUBLIC KEY----- ... -----END PUBLIC KEY-----';const b_private_key_data = '-----BEGIN PRIVATE KEY----- ... -----END PRIVATE KEY-----';// 生成 A 的公私钥对象const a_public_key = new NodeRSA(a_public_key_data);const a_private_key = new NodeRSA(a_private_key_data);// 生成 B 的公私钥对象const a_public_key = new NodeRSA(a_public_key_data);const a_private_key = new NodeRSA(a_private_key_data);const text = 'Hello RSA!';// 加签并加密const sign = a_private_key.sign(text, 'base64', 'utf8');console.log('A 私钥加签:', sign);const encrypted = a_public_key.encrypt(sign, 'base64');console.log('B 公钥加密:', encrypted);// 解密并验签const decrypted = a_public_key.decrypt(encrypted, 'utf8');console.log('B 私钥解密:', decrypted);const verify = a_public_key.verify(text, decrypted, 'utf8', 'base64');console.log('A 公钥验签:', verify); serialize 接口传递的一般是复杂的对象,所以我们需要把对象按一定的顺序排列并序列化成字符串再进行签名加密的操作 123456789const serialize = (obj) => { const str = []; Object.keys(obj).sort().forEach((key) => { if (obj.hasOwnProperty(key)) { str.push(encodeURIComponent(key) + '=' + encodeURIComponent(obj[key])); } }); return str.join('&');}; 注意RSA 算法有一定的计算量,加上 Node 不适合做计算密集型的操作。当接口被频繁调用可能会占用主线程,阻塞其他接口,使用了 RSA 的接口并发量会下降十倍左右。如非必要,谨慎在 Node 里使用 RSA。 本文首发于我的个人博客 https://chaohang.top 作者张小超 转载请注明出处","categories":[],"tags":[{"name":"JavaScript","slug":"JavaScript","permalink":"https://www.chaohang.top/tags/JavaScript/"},{"name":"node.js","slug":"node-js","permalink":"https://www.chaohang.top/tags/node-js/"},{"name":"rsa","slug":"rsa","permalink":"https://www.chaohang.top/tags/rsa/"}]},{"title":"用node.js写一个jenkins发版脚本","slug":"用node-js写一个jenkins发版脚本","date":"2018-09-25T10:41:05.000Z","updated":"2018-09-25T10:41:05.000Z","comments":true,"path":"2018/09/25/用node-js写一个jenkins发版脚本/","link":"","permalink":"https://www.chaohang.top/2018/09/25/%E7%94%A8node-js%E5%86%99%E4%B8%80%E4%B8%AAjenkins%E5%8F%91%E7%89%88%E8%84%9A%E6%9C%AC/","excerpt":"背景每次到网页里手动发版有点烦,写个脚本来提高开发效率。","text":"背景每次到网页里手动发版有点烦,写个脚本来提高开发效率。 CFG在 jenkins 设置里获取 API TOKEN。把 host 和账号密码拼接起来就可以通过鉴权。 1234567const token = { user: '', password: '',}const host = 'jenkins.xxx.cn';const basicUrl = `http://${token.user}:${token.password}@${host}`; BUILD 调用 jenkins 提供的 jenkins.job.build() 方法发版。 1234567891011121314151617// crumbIssuer 默认false, true 启用CSRF保护支持const jenkins = require('jenkins')({ baseUrl, crumbIssuer: true });const job_name = ''; // 想要发版的job的名字jenkins.job.build( { name: job_name, parameters: { name: 'value', // 填写一些参数信息 } }, function(err, data) { if (err) throw err; console.log('queue item number', data);}); 查看发版状态执行 build 之后我们想要查看当前 build 的状态,只成功、失败还是进行中。 jenkins.job.get() 获取当前 job 的信息。data.lastBuild.number 可以拿到最后一个 build 的 number,最后一个 build 也就是我们刚刚触发的 build。jenkins.build.get() 获取当前 build 的信息。data.result 获取当前 build 的结果 1234567891011jenkins.job.get(job_name, (err, data) => { if (err) throw err; const lastBuildNumber = data.lastBuild.number; console.log('last build number', lastBuildNumber); jenkins.build.get(job_name, lastBuildNumber, (err, data) => { if (err) throw err; console.log('last build result', data.result); })}); 资料jenkins","categories":[],"tags":[{"name":"JavaScript","slug":"JavaScript","permalink":"https://www.chaohang.top/tags/JavaScript/"},{"name":"node.js","slug":"node-js","permalink":"https://www.chaohang.top/tags/node-js/"},{"name":"jenkins","slug":"jenkins","permalink":"https://www.chaohang.top/tags/jenkins/"}]},{"title":"七牛视频切片方案","slug":"七牛视频切片方案","date":"2018-08-07T14:23:28.000Z","updated":"2018-08-07T14:23:28.000Z","comments":true,"path":"2018/08/07/七牛视频切片方案/","link":"","permalink":"https://www.chaohang.top/2018/08/07/%E4%B8%83%E7%89%9B%E8%A7%86%E9%A2%91%E5%88%87%E7%89%87%E6%96%B9%E6%A1%88/","excerpt":"使用七牛 sdk 上传视频并做分片操作。 step1 生成 token123456789101112131415161718192021222324const qiniu = require(\"qiniu\");var accessKey = proc.env.QINIU_ACCESS_KEY;var secretKey = proc.env.QINIU_SECRET_KEY;var mac = new qiniu.auth.digest.Mac(accessKey, secretKey);var bucket = proc.env.QINIU_TEST_BUCKET; // 对象存储空间//带数据处理的凭证var origin_filename = \"avthumb_test_target.mp4\"; // 源文件名var save_as_filename = `${origin_filename}.m3n8`; // 分片后的文件名var saveMp4Entry = qiniu.util.urlsafeBase64Encode(`${bucket}:${save_as_filename}`);var avthumbMp4Fop = \"avthumb/m3u8/noDomain/1/segtime/15/vb/440k|saveas/\" + saveMp4Entry;var options = { scope: bucket, expires: 3600, // 凭证有效时间 persistentOps: avthumbMp4Fop, // 视频上传后触发预转持久化,切片处理 persistentPipeline: \"video-pipe\", // 多媒体处理队列 persistentNotifyUrl: \"http://api.example.com/qiniu/pfop/notify\", // 回调通知地址}var putPolicy = new qiniu.rs.PutPolicy(options);console.log(putPolicy.uploadToken(mac)); 生成不同作用的凭证可以查看官方提供的示例nodejs-sdk example。 以上代码为生成上传视频并进行切片处理的凭证。上传相关的文档查看七牛上传策略。 切片动作是由persistentOps参数触发并启动一个异步任务, persistentOps 详解","text":"使用七牛 sdk 上传视频并做分片操作。 step1 生成 token123456789101112131415161718192021222324const qiniu = require(\"qiniu\");var accessKey = proc.env.QINIU_ACCESS_KEY;var secretKey = proc.env.QINIU_SECRET_KEY;var mac = new qiniu.auth.digest.Mac(accessKey, secretKey);var bucket = proc.env.QINIU_TEST_BUCKET; // 对象存储空间//带数据处理的凭证var origin_filename = \"avthumb_test_target.mp4\"; // 源文件名var save_as_filename = `${origin_filename}.m3n8`; // 分片后的文件名var saveMp4Entry = qiniu.util.urlsafeBase64Encode(`${bucket}:${save_as_filename}`);var avthumbMp4Fop = \"avthumb/m3u8/noDomain/1/segtime/15/vb/440k|saveas/\" + saveMp4Entry;var options = { scope: bucket, expires: 3600, // 凭证有效时间 persistentOps: avthumbMp4Fop, // 视频上传后触发预转持久化,切片处理 persistentPipeline: \"video-pipe\", // 多媒体处理队列 persistentNotifyUrl: \"http://api.example.com/qiniu/pfop/notify\", // 回调通知地址}var putPolicy = new qiniu.rs.PutPolicy(options);console.log(putPolicy.uploadToken(mac)); 生成不同作用的凭证可以查看官方提供的示例nodejs-sdk example。 以上代码为生成上传视频并进行切片处理的凭证。上传相关的文档查看七牛上传策略。 切片动作是由persistentOps参数触发并启动一个异步任务, persistentOps 详解 persistentOps 字段用于指定预转数据处理命令和保存处理结果的存储空间与资源名。为此字段指定非空值,则在成功上传一个文件后,会启动一个异步数据处理任务。persistentId 字段,唯一标识此任务。当 returnBody 中指定了 persistentId 魔法变量时,客户端收到的响应内容 returnBody 中会有 persistentId;当没有指定 returnBody 时,默认也会返回 persistentId。 使用默认的存储空间和资源名* 当只指定了数据处理命令时,服务端会选择上传文件的 Bucket 作为数据处理结果的存储空间,Key 由七牛服务器自动生成。 使用指定的存储空间和资源名 在数据处理命令后用管道符|拼接saveas/<encodedEntryURI>指令,指示七牛服务器使用EncodedEntryURI格式中指定的 Bucket 与 Key 来保存处理结果。如 avthumb/flv|saveas/cWJ1Y2tldDpxa2V5,是将上传的视频文件转码flv格式后存储为qbucket:qkey,其中cWJ1Y2tldDpxa2V5是qbucket:qkey的URL安全的Base64编码结果。以上方式可以同时作用于多个数据处理命令,用;分隔,如 avthumb/mp4|saveas/cWJ1Y2tldDpxa2V5;avthumb/flv|saveas/cWJ1Y2tldDpxa2V5Mg== 摘自七牛上传凭证 数据处理命令可以查看官方文档,比如视频切片接口。 step2 前端直传12345678910111213141516171819POST http://up.qiniu.com格式: form-data参数:key: 文件名,上文代码中的 `origin_filename`token: 上传tokenfile: 具体文件// 上传成功的响应体// 状态码 200{ \"hash\": \"luVJbz9RtE3Ha7c9XaDerE6rPyvP\", \"key\": \"prTEDwvJY18YBD_720_p1.mp4\", \"persistentId\": \"z0.5b56c9b938b9f324a573edb4\" // 可以使用persistentId,检测当前处理状态}作者:youthcity链接:https://www.jianshu.com/p/f73b39ae02dc來源:简书简书著作权归作者所有,任何形式的转载都请联系作者获得授权并注明出处。 step3 轮询获取结果1234567891011121314151617181920212223242526GET api.qiniu.com/status/get/prefop?id=${persistentId}{ \"code\": 0, // 状态码0成功,1等待处理,2正在处理,3处理失败,4通知提交失败。 \"desc\": \"The fop was completed successfully\", \"id\": \"z0.5b56c9b938b9f324a573edb4\", \"inputBucket\": \"ocean\", \"inputKey\": \"prTEDwvJY18YBD_720_p1.mp4\", \"items\": [ { \"cmd\": \"avthumb/m3u8/noDomain/1/segtime/15/vb/440k|saveas/b2NlYW46aGxzX3ByVEVEd3ZKWTE4WUJEXzcyMF9wMS5tM3U4\", \"code\": 0, \"desc\": \"The fop was completed successfully\", \"hash\": \"FraV6PX6Xp7mYYgAnY0sniTK-pPt\", \"key\": \"hls_prTEDwvJY18YBD_720_p1.m3u8\", \"returnOld\": 0 } ], \"pipeline\": \"1381326751.k12_video\", \"reqid\": \"FQIAAKAxpZfFOkQV\"}作者:youthcity链接:https://www.jianshu.com/p/f73b39ae02dc來源:简书简书著作权归作者所有,任何形式的转载都请联系作者获得授权并注明出处。 参考资料七牛上传凭证 七牛上传策略 视频切片接口 七牛上传开发(三)使用NodeJS生成带数据处理的凭证 nodejs-sdk example 本文首发于我的个人博客 https://chaohang.top 作者张小超 转载请注明出处","categories":[],"tags":[{"name":"JavaScript","slug":"JavaScript","permalink":"https://www.chaohang.top/tags/JavaScript/"},{"name":"node.js","slug":"node-js","permalink":"https://www.chaohang.top/tags/node-js/"},{"name":"七牛","slug":"七牛","permalink":"https://www.chaohang.top/tags/%E4%B8%83%E7%89%9B/"}]},{"title":"Sequelize 基本操作","slug":"Sequelize-基本操作","date":"2018-01-10T11:11:31.000Z","updated":"2018-01-10T11:11:31.000Z","comments":true,"path":"2018/01/10/Sequelize-基本操作/","link":"","permalink":"https://www.chaohang.top/2018/01/10/Sequelize-%E5%9F%BA%E6%9C%AC%E6%93%8D%E4%BD%9C/","excerpt":"sequelize 基本操作Sequelize 是 Node 的一个 ORM(Object-Relational Mapping) 框架,用来方便数据库操作。 配置 sequelize 以 mysql 为例 首先我们要引入npm包,sequelize 依赖 mysql2 作为底层驱动,暴露出自己的 API 让我们调用,在转成 mysql 语句进行执行。 12\"mysql2\": \"^1.5.1\",\"sequelize\": \"^4.28.6\" 12345678910111213141516171819202122232425262728293031323334const Sequelize = require('sequelize')// 连接数据库const sequelize = new Sequelize('database', 'username', 'password', { host: sqlconf.host, dialect: 'mysql', // 这里可以改成任意一种关系型数据库 pool: { max: 5, min: 0, acquire: 30000, idle: 10000, },})// 测试连接是否成功sequelize .authenticate() .then(() => { console.log('Connection has been established successfully.') }) .catch(err => { console.log('Unable to connect to the database', err) })// 根据 model自动创建表sequelize .sync() .then(() => { console.log('init db ok') }) .catch(err => { console.log('init db error', err) }) 我们可以调用sync()根据 model自动在数据库中创建表,也可以不调用,自己手动创。如果使用了 Sequelize 的 Associations,这必须通过 sync() 生成表结构。 创建 model创建模型,告诉 Sequelize 如何映射数据库表","text":"sequelize 基本操作Sequelize 是 Node 的一个 ORM(Object-Relational Mapping) 框架,用来方便数据库操作。 配置 sequelize 以 mysql 为例 首先我们要引入npm包,sequelize 依赖 mysql2 作为底层驱动,暴露出自己的 API 让我们调用,在转成 mysql 语句进行执行。 12\"mysql2\": \"^1.5.1\",\"sequelize\": \"^4.28.6\" 12345678910111213141516171819202122232425262728293031323334const Sequelize = require('sequelize')// 连接数据库const sequelize = new Sequelize('database', 'username', 'password', { host: sqlconf.host, dialect: 'mysql', // 这里可以改成任意一种关系型数据库 pool: { max: 5, min: 0, acquire: 30000, idle: 10000, },})// 测试连接是否成功sequelize .authenticate() .then(() => { console.log('Connection has been established successfully.') }) .catch(err => { console.log('Unable to connect to the database', err) })// 根据 model自动创建表sequelize .sync() .then(() => { console.log('init db ok') }) .catch(err => { console.log('init db error', err) }) 我们可以调用sync()根据 model自动在数据库中创建表,也可以不调用,自己手动创。如果使用了 Sequelize 的 Associations,这必须通过 sync() 生成表结构。 创建 model创建模型,告诉 Sequelize 如何映射数据库表 12345678910111213const UserModel = sequelize.define('user', { id: { type: Sequelize.INTEGER(11), primaryKey: true, // 主键 autoIncrement: true, // 自动递增 }, username: Sequelize.STRING(100), password: Sequelize.STRING(100), createdAt: Sequelize.BIGINT, updatedAt: Sequelize.BIGINT,}, { timestamps: false}) define() 方法的第一个参数为表名,对应的是 users 表。如果不设置 timestamps,Sequlize 会自动为我们添加创建时间和更新时间,我一般选择关闭,手动添加灵活性高些。 增删改查增12345678910(async () => { const now = Date.now() const user = await UserModel.create({ username: '小张', password: 'root', createAt: now, updateAt: now, }) console.log('创建:' + JSON.stringify(user))})(); 查123456789101112131415161718192021222324252627282930313233(async () => { // 查找所有 const allUser = await UserModel.findAll() // 按id查找 const oneUser = await UserModel.findById(id) // 按条件查询 const someUser = await UserModel.findAll({ where: { // 模糊查询 name: { $like: '%小%', }, // 精确查询 password: 'root', } }) // 分页查询 const size = 10 // 每页10条数据 const page = 1 // 页数 const pageUser = await UserModel.findAndCountAll({ where: { name: { $like: '%小%', }, }, limit: size, offset: size * (page - 1), })})(); 改12345678(async () => {// 方法一await UserModel.upert(data) // data 里面如果带有 id 则更新,不带则新建// 方法二const user = await UserModel.findById(id)user.update(data)})() 删12345678910111213(async () => {// 方法一// 删除所有名字带’小‘的用户await UserModel.destroy({ where: { username: '小', },})// 方法二const user = await UserModel.findById(id)user.destroy()})() 关联表Sequelize 提供了一对一,一对多,多对多等关联表操作,我用的不多,这里只介绍 hasMany() 这一种,其他的可以看文档。 设置首先要在 model 中设置 12345678910111213141516171819const School = sequelize.define('school', { id: { type: Sequelize.INTEGER(11), primaryKey: true, autoIncrement: true, }, username: Sequelize.STRING(100),})const Student = sequelize.define('student', { id: { type: Sequelize.INTEGER(11), primaryKey: true, autoIncrement: true, }, username: Sequelize.STRING(100),})School.hasMany(Student, {as: 'student', foreignKey: 'schoolId'}) as 参数重新定义了目标model的名字。foreignKey 参数定义了在 t_student 表中关联 key 的名字。 关联查如果我们想查找一个学校和这个学校中所有的学生信息,可以这样找: 12345678(async () => { const group = await School.findById(id, { include: [{ model: Student, as: 'student', }], })})() 如果我们设置了 as 就需要在 include 选项中设置同样的 as。 按条件查找 1234567891011(async () => { const group = await School.findAll({ where: { name: 'someting', }, include: [{ model: Student, as: 'student', }], })})() 文档更多详细操作请参考官方文档","categories":[],"tags":[{"name":"mysql","slug":"mysql","permalink":"https://www.chaohang.top/tags/mysql/"}]},{"title":"解决 iPhone 微信 H5 无法自动播放音乐问题","slug":"解决-iPhone-微信-H5-无法自动播放音乐问题","date":"2017-11-29T09:29:26.000Z","updated":"2017-11-29T09:29:26.000Z","comments":true,"path":"2017/11/29/解决-iPhone-微信-H5-无法自动播放音乐问题/","link":"","permalink":"https://www.chaohang.top/2017/11/29/%E8%A7%A3%E5%86%B3-iPhone-%E5%BE%AE%E4%BF%A1-H5-%E6%97%A0%E6%B3%95%E8%87%AA%E5%8A%A8%E6%92%AD%E6%94%BE%E9%9F%B3%E4%B9%90%E9%97%AE%E9%A2%98/","excerpt":"","text":"使用 wx sdk 在html导入sdk js文件。(如果网站是https导入的sdk也要使用https!)然后在 wx.ready 中设置 play 方法并复制给全局变量 fm,当需要使用的时候再调用 fm() 12345678910111213141516171819202122// 全局变量 fm2var fm2;wx.config({ debug : false, // 这里为false appId : '', // 以下随意填写即可 timestamp : (new Date()).getTime(), nonceStr : '', signature : '', jsApiList : ['checkJsApi']});wx.ready(function() { $('#music1').play(); // 第一段音乐页面加载后自动播放 $('#music2').load(); // 第二段音乐等待,触发 fm2 时播放 fm2 = function () { music2.play(); };});fm2(); //调用之后即可播放 本文首发于我的个人博客 https://chaohang.top 作者张小超 转载请注明出处","categories":[],"tags":[{"name":"前端","slug":"前端","permalink":"https://www.chaohang.top/tags/%E5%89%8D%E7%AB%AF/"}]},{"title":"promise 和 async 的用法","slug":"promise-和-async-的用法","date":"2017-10-27T01:56:34.000Z","updated":"2017-10-27T01:56:34.000Z","comments":true,"path":"2017/10/27/promise-和-async-的用法/","link":"","permalink":"https://www.chaohang.top/2017/10/27/promise-%E5%92%8C-async-%E7%9A%84%E7%94%A8%E6%B3%95/","excerpt":"promise12345678910111213141516171819202122232425262728293031323334353637383940// 先构造一个 promise 函数// resolve 和 reject 都是一个函数// resolve 在成功时调用// reject 在失败时调用function promise() { return new Promise((resolve, reject) => { let tar = Math.random(0, 1) if (tar > 0.5) { console.log('suceess') setTimeout(function() { resolve('resolve') }, 1000) } else { console.log('fail') setTimeout(function() { reject('reject') }, 1000) } })}// then 和 catch 第一个参数为resolve状态时的回调,第二个参数为reject状态时的回调// catch 相当于 then(null, reject)promise().then( (resolve) => console.log('then', resolve)).catch( (reject) => console.log('catch', reject))// then 后面还可以接 then 一直链式调用// 第一个 then 接受的参数是 resolve 函数传过来的参数// 第二个 then 接受一个参数是第一个 then 返回的值promise().then( (resolve) => (resolve + 1) // 接受 resolve 返回 resolve + 1).then( (plusOne) => console.log(plusOne) // 接受 resolve +1 并打印).catch( (reject) => console.log('catch', reject))","text":"promise12345678910111213141516171819202122232425262728293031323334353637383940// 先构造一个 promise 函数// resolve 和 reject 都是一个函数// resolve 在成功时调用// reject 在失败时调用function promise() { return new Promise((resolve, reject) => { let tar = Math.random(0, 1) if (tar > 0.5) { console.log('suceess') setTimeout(function() { resolve('resolve') }, 1000) } else { console.log('fail') setTimeout(function() { reject('reject') }, 1000) } })}// then 和 catch 第一个参数为resolve状态时的回调,第二个参数为reject状态时的回调// catch 相当于 then(null, reject)promise().then( (resolve) => console.log('then', resolve)).catch( (reject) => console.log('catch', reject))// then 后面还可以接 then 一直链式调用// 第一个 then 接受的参数是 resolve 函数传过来的参数// 第二个 then 接受一个参数是第一个 then 返回的值promise().then( (resolve) => (resolve + 1) // 接受 resolve 返回 resolve + 1).then( (plusOne) => console.log(plusOne) // 接受 resolve +1 并打印).catch( (reject) => console.log('catch', reject)) async1234567891011121314151617181920212223242526272829303132333435// 同样需要一个 promise 对象function promise() { return new Promise((resolve, reject) => { let tar = Math.random(0, 1) if (tar > 0.5) { console.log('success') setTimeout(function() { resolve('resolve') }, 1000) } else { console.log('fail') setTimeout(function() { reject('reject') }, 1000) } })}// 只是调用的方法跟 then/catch 不一样// async 函数执行时,一旦遇到 await 就会先返回,等异步操作完成,再接着执行后面的操作async function async() { const res = await promise() console.log(res) console.log('wait') return res}// async 函数返回 promise 对象// 只有函数内的 await 语句全部执行完,或者遇到 return 或 抛出错误,才会发生状态改变// 如果发生错误或状态为 reject 则执行 catchasync().then( (res) => console.log(res)).catch( (e) => console.log(e))","categories":[],"tags":[{"name":"JavaScript","slug":"JavaScript","permalink":"https://www.chaohang.top/tags/JavaScript/"}]},{"title":"阿里云云服务器的端口配置问题","slug":"阿里云云服务器的端口配置问题","date":"2017-05-23T06:26:28.000Z","updated":"2017-05-23T06:26:28.000Z","comments":true,"path":"2017/05/23/阿里云云服务器的端口配置问题/","link":"","permalink":"https://www.chaohang.top/2017/05/23/%E9%98%BF%E9%87%8C%E4%BA%91%E4%BA%91%E6%9C%8D%E5%8A%A1%E5%99%A8%E7%9A%84%E7%AB%AF%E5%8F%A3%E9%85%8D%E7%BD%AE%E9%97%AE%E9%A2%98/","excerpt":"心血来潮花了9块9(学生价)买了个阿里云。因为后端只会一点 Node,所以上来第一件事就是按照官方教程把部署 Node 项目这个教程撸了一遍。 问题描述官方教程代码1234567891011const http = require('http');const hostname = 'ECS公网IP地址'; // 这里把文字换成自己的\b公网IP地址const port = 3000;const server = http.createServer((req, res) => { res.statusCode = 200; res.setHeader('Content-Type', 'text/plain'); res.end('Hello World\\n');});server.listen(port, hostname, () => { console.log(`Server running at http://${hostname}:${port}/`);}); 一段简单最简单的 Node http服务器,成功运行后可以在页面输出 Hello World。","text":"心血来潮花了9块9(学生价)买了个阿里云。因为后端只会一点 Node,所以上来第一件事就是按照官方教程把部署 Node 项目这个教程撸了一遍。 问题描述官方教程代码1234567891011const http = require('http');const hostname = 'ECS公网IP地址'; // 这里把文字换成自己的\b公网IP地址const port = 3000;const server = http.createServer((req, res) => { res.statusCode = 200; res.setHeader('Content-Type', 'text/plain'); res.end('Hello World\\n');});server.listen(port, hostname, () => { console.log(`Server running at http://${hostname}:${port}/`);}); 一段简单最简单的 Node http服务器,成功运行后可以在页面输出 Hello World。 报错 报错了,因为之前写node只需要监听端口,没必要把IP地址也给写进去,所以我删了\bhostname,改成下面这样。 1234567891011const http = require('http');const hostname = 'ECS公网IP地址'; // 这里把文字换成自己的\b公网IP地址const port = 3000;const server = http.createServer((req, res) => { res.statusCode = 200; res.setHeader('Content-Type', 'text/plain'); res.end('Hello World\\n');});server.listen(port, () => { console.log(`Server running at http://${hostname}:${port}/`);}); 运行成功,但是在外网用\b浏览器输入IP地址\b之后无法访问。 问题分析curl ip.cn 查询当前外网IP,没有问题,与阿里云提供的一致。 ping www.baidu.com 服务器可以访问外网 curl http://127.0.0.1:3000 正确输出 “Hello World”,说明代码的编写没有问题。 代码没问题,服务器网络没问题,ip地址没问题。我估计应该是防火墙把端口禁掉了。 关闭防火\b墙身为小菜鸟,第一时间想到的就是把服务器的防火墙关掉。我的服务器是 CentOS7.0 ,与6.0不一样,CentOS7.0默认使用 firewall 作为防火墙。 systemctl stop firewalld.service #停止firewall systemctl disable firewalld.service #禁止开机启动firewall firewall-cmd --state #查看防火墙状态,已经处于关闭状态 开Node服务器,浏览器访问还是失败。 好像没招了,问小伙伴,他们说可能是没有备案的原因。但是没备案的小伙伴说他用得好好的,而且我就买了一个月的服务器,\b再等半个月备案我也不需要用了。正打算放弃问客服,小伙伴说你装个Nginx试试,只要正确运行\bNginx而且服务器没问题直接访问IP地址就能出现Nginx首页了。 安装 NginxCentOS有两种方法安装Nginx,具体的自行谷歌吧。 安装完成,运行成功。 浏览器访问公网\bIP,无法访问。 从吃完中午饭13:00一直折腾到下午6:30左右,一动不动坐凳子上\b5个多小时了,不行了,休息吃完饭去。 解决既然Nginx正确运行,那只剩两个问题了,一个是没有备案,不过这个问题的可能性很小很小。还有一点就是虽然我CentOS上把防火墙关了,但云服务器上阿里云应该\b会有一些安全措施,导致一部分端口被禁,或者压根就没有把\b端口打开。 吃完饭,散完步,我就在云服务器\b控制台\b里一个一个的找,发现安全组里点击配置规则后有一个安全组规则,里面通过添加安全组规则可以\b开放需要用到的端口。默认只打开了两个端口。 总结这是一个简单的问题,不过需要一步步排除掉可能的原因。 如果身边有个后端或者运维的小伙伴在的话,应该可以很快意识到这个问题。独狼是走不远的,至少是走不快的。 其实在购买时就可以选取要开放的端口,第一次嘛,没有经验,都是直接默认选项。怪不得小伙伴说他服务器的配置都没有动过,应该是购买的时候就已经设置好的。 趁年轻,多踩坑。 本文首发于我的个人博客 https://chaohang.top 作者张小超 转载请注明出处","categories":[],"tags":[{"name":"踩坑","slug":"踩坑","permalink":"https://www.chaohang.top/tags/%E8%B8%A9%E5%9D%91/"}]},{"title":"《图解HTTP》读书笔记","slug":"《图解HTTP》读书笔记","date":"2017-04-23T12:49:25.000Z","updated":"2017-04-23T12:49:25.000Z","comments":true,"path":"2017/04/23/《图解HTTP》读书笔记/","link":"","permalink":"https://www.chaohang.top/2017/04/23/%E3%80%8A%E5%9B%BE%E8%A7%A3HTTP%E3%80%8B%E8%AF%BB%E4%B9%A6%E7%AC%94%E8%AE%B0/","excerpt":"TCP/IP 协议族TCP/IP 协议族按层次分为以下4层:应用层、传输层、网络层和数据链路层。 应用层决定了向用户提供应用服务时通信的活动。DNS服务、HTTP协议处于该层。 传输层对上层应用层,提供处于网络连接中的两台计算机之间的数据传输。TCP协议处于该层。 网络层用来处理在网络上流动的数据包。数据包是网络传输的最小数据单位。该层规定了通过怎样的路径到达对方的计算机,并把数据包传给对方。IP网际协议位于该层。 链路层用来处理链接网络的硬件部分。包括操作系统、硬件驱动等。","text":"TCP/IP 协议族TCP/IP 协议族按层次分为以下4层:应用层、传输层、网络层和数据链路层。 应用层决定了向用户提供应用服务时通信的活动。DNS服务、HTTP协议处于该层。 传输层对上层应用层,提供处于网络连接中的两台计算机之间的数据传输。TCP协议处于该层。 网络层用来处理在网络上流动的数据包。数据包是网络传输的最小数据单位。该层规定了通过怎样的路径到达对方的计算机,并把数据包传给对方。IP网际协议位于该层。 链路层用来处理链接网络的硬件部分。包括操作系统、硬件驱动等。 IP协议IP网际协议位于网络层。IP协议的作用是把各种数据包传送给对方。而要保证确实传送到对方那里,则需要满足各类条件。其中两个重要的条件是IP地址和MAC地址。 IP地址指明了节点被分配到的地址,MAC地址是指网卡所属的固定地址。IP地址可以和MAC地址进行配对。IP地址可变换,但MAC地址基本上不会改变。 TCP协议TCP协议位于传输层,提供可靠的字节流服务。 所谓的字节流服务(Byte Stream Service)是指,为了方便传输,将大块数据分割成以报文段为单位的数据包进行管理。而可靠的传输服务是指,能够把数据准确可靠的传给对方。一言以蔽之,TCP协议为了更容易传送大数据才把数据分割,而且TCP协议能够确认数据最终是否送达到对方。 三次握手(three-way handshaking)为了准确无误地将数据送达目标处,TCP协议采用了三次握手策略。 发送端发送一个带SYN标志的数据包给对方 –> 接受端收到后传回一个带SYN/ACK标志的数据包以示传达确认信息 –> 发送端再传回一个带ACK标志的数据包表示握手结束 DNS服务DNS服务位于应用层。提供域名到IP地址之间的解析服务。 整个服务的流程客户端请求一个网址 –> DNS把域名解析为IP地址 –> HTTP生成针对目标Web服务器的HTTP请求报文 –> TCP将HTTP请求报文分割成报文段按序号传输 –> IP协议负责搜索对方的地址边中转边传送 –> TCP从对方那里接受报文段并重组成报文 –> HTTP对请求的内容进行处理 –> 请求的处理结果也按照这个流程返回客户端 HTTP 协议HTTP 方法 方法 作用 说明 GET 获取资源 POST 传输实体主体 一般用于提交需要存储的信息 HEAD 获取报文首部 和GET一样,只是不返回报文主体部分 DELETE 删除文件 OPTIONS 询问支持的方法 持久连接在HTTP/1.1中,所有的连接默认都是持久连接 HTTP 状态码 状态码 类别 2** 成功 3** 重定向 4** 客户端错误 5** 服务端错误","categories":[],"tags":[{"name":"阅读","slug":"阅读","permalink":"https://www.chaohang.top/tags/%E9%98%85%E8%AF%BB/"},{"name":"HTTP","slug":"HTTP","permalink":"https://www.chaohang.top/tags/HTTP/"}]},{"title":"一个 Vue + Node + MongoDB 博客系统","slug":"一个-Vue-Node-MongoDB-博客系统","date":"2017-04-22T11:45:10.000Z","updated":"2017-04-22T11:45:10.000Z","comments":true,"path":"2017/04/22/一个-Vue-Node-MongoDB-博客系统/","link":"","permalink":"https://www.chaohang.top/2017/04/22/%E4%B8%80%E4%B8%AA-Vue-Node-MongoDB-%E5%8D%9A%E5%AE%A2%E7%B3%BB%E7%BB%9F/","excerpt":"源码耗时半载(半个月)的大项目终于完成了。这是一个博客系统,使用 Vue 做前端框架,Node + express 做后端,数据库使用的是 MongoDB。实现了用户注册、用户登录、博客管理(文章的修改和删除)、文章编辑(Markdown)、标签分类等功能。","text":"源码耗时半载(半个月)的大项目终于完成了。这是一个博客系统,使用 Vue 做前端框架,Node + express 做后端,数据库使用的是 MongoDB。实现了用户注册、用户登录、博客管理(文章的修改和删除)、文章编辑(Markdown)、标签分类等功能。 很早之前就想写一个个人博客。学了 Vue 之后,把前端部分写出来,然后 Node 一直拖拖拉拉的学了很久,中间又跑去实习了一段时间,所以直到回学校之后才列了个计划把这个项目实现了。 翻出之前写的前端部分,好丑啊,干脆推掉重写吧。前端模仿的是 hexo 的经典主题 NexT ,本来是想把源码直接拿过来用的,后来发现还不如自己写来得快,就全部自己动手实现成 vue components。 实现的功能 文章的编辑,修改,删除 支持使用 Markdown 编辑与实时预览 支持代码高亮 给文章添加标签 支持用户注册登录 使用到的技术前端 Vue.js vue-cli vue-router vue-resource element-ui marked highlight.js 后端 Node.js Express Mongoose 基本思路前端使用 vue-router 操作路由,实现单页应用的效果。使用 vue-resource 从后台获取数据,数据的处理全部都在前端,所以后端要做的事情很简单——把前端打包好的数据存进数据库中和从数据库中取出数据。前后端使用统一的路由命名规则。 项目目录123456789101112131415161718192021| app.js 后端入口| index.html 入口页面| .babelrc babel配置| .gitignore git配置| package.json| webpack.config.js webpack配置||-dist vue打包生成的文件||-node_modules 模块||-server 后端 | check.js | db.js 数据库 __| router.js 路由||-src 前端 |-assets 静态资源 |-components 组件 | App.vue | main.js webpack 配置webpack 大部分是 vue-cli 自动生成的,添加了让前后端http请求都转到node的3000端口,而不是前端的8080端口的配置。 1234567891011devServer: { historyApiFallback: true, noInfo: true, //让前后端http请求都转到node的3000端口,而不是前端的8080端口 proxy: { '/': { target: 'http://localhost:3000/' } } } 这里涉及一个新手可能会不明白的问题(我之前就捣鼓了半天)。 开发的时候要先打开数据库 MongoDB ,使用命令 mongod。 然后打开后端服务器 node app,后端监听 3000 端口。 最后打开前端开发模式 npm run dev,前端启动了一个 webpack 服务器,监听 8080 端口用于热刷新。通过配置把前端的http请求转到 3000 端口。 前端部分命名视图所有页面都用到的元素可以写在 App.vue 上面,也可以写成公共组件。我在 App.vue 中使用了命名视图,因为 sidebar 这个组件有的页面需要有的不需要,不需要的时候就不用加载。 12345678910<!--App.vue--><template> <div id=\"app\"> <div class=\"black_line\"></div> <div id=\"main\"> <router-view name=\"sidebar\"></router-view> <router-view></router-view> </div> </div></template> router路由的配置写在 main.js 中,分为前台展示和后台管理。后台管理统一以 ‘/admin’ 开头。注册页和登录页写在一起了,上面有两个按钮“注册”和“登录”(我好懒-_-)。 12345678910111213// main.jsconst router = new VueRouter({ routes: [ {path: '/', components: {default: article, sidebar: sidebar}}, {path: '/article', components: {default: article, sidebar: sidebar}}, {path: '/about', components: {default: about, sidebar: sidebar}}, {path: '/articleDetail/:id', components: {default: articleDetail, sidebar: sidebar}}, {path: '/admin/articleList', components: {default: articleList, sidebar: sidebar}}, {path: '/admin/articleEdit', component: articleEdit}, {path: '/admin/articleEdit/:id', component: articleEdit}, {path: '/admin/signin', component: signin} ]}) element UI使用了 element 用于消息提醒和标签分类。并不需要整个引入,而是使用按需引入。 123456789101112131415161718// main.js// 按需引用elementimport { Button, Message, MessageBox, Notification, Popover, Tag, Input } from 'element-ui'import 'element-ui/lib/theme-default/index.css'const components = [Button, Message, MessageBox, Notification, Popover, Tag, Input]components.forEach((item) => { Vue.component(item.name, item)})const MsgBox = MessageBoxVue.prototype.$msgbox = MsgBoxVue.prototype.$alert = MsgBox.alertVue.prototype.$confirm = MsgBox.confirmVue.prototype.$prompt = MsgBox.promptVue.prototype.$message = MessageVue.prototype.$notify = Notification vue-resource用于向后端发起请求。打通前后端的关键。 123456// GET /someUrl this.$http.get('/someUrl').then(response => { // success callback }, response => { // error callback }); get 请求前端发起 get 请求,当请求成功被返回执行第一个回调函数,请求没有被成功返回则执行第二个回调函数。1234this.$http.get('/api/articleDetail/' + id).then( response => this.article = response.body, response => console.log(response)) 后端响应请求并返回结果12345678910// router.jsrouter.get('/api/articleDetail/:id', function (req, res) { db.Article.findOne({ _id: req.params.id }, function (err, docs) { if (err) { console.error(err) return } res.send(docs) })}) post 请求前端发起 post 请求,当请求成功被返回执行第一个回调函数,请求没有被成功返回则执行第二个回调函数。12345678910111213141516171819202122// 新建文章// 即将被储存的数据 objlet obj = { title: this.title, date: this.date, content: this.content, gist: this.gist, labels: this.labels}this.$http.post('/api/admin/saveArticle', { articleInformation: obj}).then( response => { self.$message({ message: '发表文章成功', type: 'success' }) // 保存成功后跳转至文章列表页 self.refreshArticleList() }, response => console.log(response)) 后端存储数据并返回结果1234567891011// router.js// 文章保存router.post('/api/admin/saveArticle', function (req, res) { new db.Article(req.body.articleInformation).save(function (err) { if (err) { res.status(500).send() return } res.send() })}) 后端部分后端使用 express 构建了一个简单的服务器,几乎只用于操作数据库。 app.js 位于项目根目录,使用 node app 运行服务器。 123456789101112131415161718192021222324252627282930313233343536373839const express = require('express')const fs = require('fs')const path = require('path')const bodyParse = require('body-parser')const session = require('express-session')const MongoStore = require('connect-mongo')(session)const router = require('./server/router')const app = express()const resolve = file => path.resolve(__dirname, file)app.use('/dist', express.static(resolve('./dist')))app.use(bodyParse.json())app.use(bodyParse.urlencoded({ extended: true }))app.use(router)// sessionapp.set('trust proxy', 1) // trust first proxyapp.use(session({ secret: 'blog', resave: false, saveUninitialized: true, cookie: { secure: true, maxAge: 2592000000 }, store: new MongoStore({ url: 'mongodb://localhost:27017/blog' })}))app.get('*', function (req, res) { let html = fs.readFileSync(resolve('./' + 'index.html'), 'utf-8') res.send(html)})app.listen(3000, function () { console.log('访问地址为 localhost:3000')}) 给自己挖了一个坑。因为登录之后需要保存用户状态,用来判断用户是否登录,如果登录则可以进入后台管理,如果没有登录则不能进入后台管理页面。之前写 node 的时候用的是 session 来保存,不过spa应用不同于前后端不分离的应用,我在前端对用户输入的账号密码进行了判断,如果成功则请求登录在后端保存 session。不过不知道出于什么原因,session 总是没办法赋值。因为我 node 学的也是半吊子,所以暂时放着,等我搞清楚了再来填坑。 收获 学一个新模块,新框架第一步就是阅读官方文档。 不要觉得读文档费时间,认真的读一遍官方文档比你瞎折腾来得有效率。 阅读与你项目相关的优秀项目的源码,学习别人如何组织代码。 自己的解决方案不一定是最优解,不过在找到最优解之前不妨自己先试试。 框架模块的使用都不难,套API的活每个人都能干,只是快与慢的差别。 尝试思考这个API是如何实现的。 了解了完整的web应用是如何运作的,包括服务器,数据库,前端是如何联系在一起的。 本文首发于我的个人博客 https://chaohang.top 作者张小超 转载请注明出处","categories":[],"tags":[{"name":"node.js","slug":"node-js","permalink":"https://www.chaohang.top/tags/node-js/"},{"name":"mongodb","slug":"mongodb","permalink":"https://www.chaohang.top/tags/mongodb/"},{"name":"vue","slug":"vue","permalink":"https://www.chaohang.top/tags/vue/"}]},{"title":"mongoDB基本操作","slug":"mongoDB基本操作","date":"2017-04-17T03:28:54.000Z","updated":"2017-04-17T03:28:54.000Z","comments":true,"path":"2017/04/17/mongoDB基本操作/","link":"","permalink":"https://www.chaohang.top/2017/04/17/mongoDB%E5%9F%BA%E6%9C%AC%E6%93%8D%E4%BD%9C/","excerpt":"创建数据库use DATABASE_NAME 如果数据库不存在则创建,否则切换到指定数据库 db查看当前数据库名 show dbs 查看所有数据库 show tables 查看集合 删除数据库db.dropDatabase() 删除当前数据库 db.collection.drop() 删除集合,collection为集合名,例db.student.drop() 插入文档db.COLLECTION_NAME.insert(document)实例12345db.col.insert({ title: 'MongoDB教程', tags: ['mongodb', 'database', 'nosql'], url: 'www.runoob.com'})","text":"创建数据库use DATABASE_NAME 如果数据库不存在则创建,否则切换到指定数据库 db查看当前数据库名 show dbs 查看所有数据库 show tables 查看集合 删除数据库db.dropDatabase() 删除当前数据库 db.collection.drop() 删除集合,collection为集合名,例db.student.drop() 插入文档db.COLLECTION_NAME.insert(document)实例12345db.col.insert({ title: 'MongoDB教程', tags: ['mongodb', 'database', 'nosql'], url: 'www.runoob.com'}) 我们也可以把数据定义为变量 1234567document = ({ // 记得这边有个括号 title: 'MongoDB教程', tags: ['mongodb', 'database', 'nosql'], url: 'www.runoob.com'})db.col.insert(document) 插入文档也可以使用 db.col.save(document) 命令,如果不指定 _id 字段save()方法类似于 insert()。如果指定 _id 字段,则会更新该 _id 的数据。 更新文档update()方法123456789db.collection.update( <query>, // 查询条件 <update>, //update的对象和一些更新的操作符 { upsert: <boolean>, // 可选 multi: <boolean>, // 可选 writeConcern: <document> // 可选,抛出异常的级别 }) 例子db.col.update({'title':'MongoDB 教程'},{$set:{'title':'MongoDB'}}) 以上语句只会修改第一条发现的文档,如果你要修改多条相同的文档,则需要设置multi参数为true save()方法123456db.collection.save( <document>, // 传入文档用来替换之前的文档 { writeConcern: <document> }) 例子123456789101112db.col.save({ "_id" : ObjectId("56064f89ade2f21f36b03136"), // id唯一 "title" : "MongoDB", "description" : "MongoDB 是一个 Nosql 数据库", "by" : "Runoob", "url" : "http://www.runoob.com", "tags" : [ "mongodb", "NoSQL" ], "likes" : 110}) 删除文档1234567db.collection.remove( <query>, // 可选,删除的文档的条件 { justOne:<boolean>, // 可选,如果为true或1,则只删除一个文档 writeConcern: <document> // 可选,抛出异常级别 }) 例子db.col.remove({'title':'mongodb'}) db.col.remove({}) 删除所有数据 查询文档db.col.find() 查看已插入的文档db.col.find().pretty() 输出好看的格式db.col.findOne() 只返回一个文档 条件操作符(>) 大于 - $gt(<) 小于 - $lt(>=) 大于等于 - $gte(<= ) 小于等于 - $ltedb.col.find({"likes" : {$gt : 100}}) 查找likes大于100的数据 AND条件传入多个键值然后逗号隔开db.col.find({key1: value1, key2: value2}).pretty() OR条件1234 [ {key1: value1}, {key2: value2} ]}).pretty() $type操作符 类型 数字 Double 1 String 2 Object 3 Array 4 Binary data 5 Object id 7 Boolean 8 Date 9 Null 10 不全,完整表格访问这里 db.col.find({'title' :{$type: 2}}) 如果title为String则输出 limit and skipdb.col.find().limit(2) 只读取两条db.col.find().limit(1).skip(1) 跳过第一条,只显示第二条skip默认为0 sort()方法1升序,-1降序 db.col.find().sort({key: 1}) 例子db.col.find({}, {'title': 1, _id: 0}).sort({'likes': -1})","categories":[],"tags":[{"name":"mongodb","slug":"mongodb","permalink":"https://www.chaohang.top/tags/mongodb/"}]},{"title":"二叉树的递归算法","slug":"二叉树的递归算法","date":"2017-04-16T12:58:20.000Z","updated":"2017-04-16T12:58:20.000Z","comments":true,"path":"2017/04/16/二叉树的递归算法/","link":"","permalink":"https://www.chaohang.top/2017/04/16/%E4%BA%8C%E5%8F%89%E6%A0%91%E7%9A%84%E9%80%92%E5%BD%92%E7%AE%97%E6%B3%95/","excerpt":"二叉树二叉树是一种特殊的数据结构,有一个根节点,根节点下面有一左一右两个子节点,每个子节点又有各自的子节点,层层深入成树状。 二叉树的遍历关于二叉树的遍历我只学习了递归遍历,非递归遍历比较复杂还是很理解。 递归遍历分为先序,中序和后序。用三个字母表示递归遍历可以很好理解: D: 访问根节点,L: 遍历根节点的左子树,R:遍历根节点的右子树。 先序遍历:DLR 中序遍历:LDR 后序遍历:LRD","text":"二叉树二叉树是一种特殊的数据结构,有一个根节点,根节点下面有一左一右两个子节点,每个子节点又有各自的子节点,层层深入成树状。 二叉树的遍历关于二叉树的遍历我只学习了递归遍历,非递归遍历比较复杂还是很理解。 递归遍历分为先序,中序和后序。用三个字母表示递归遍历可以很好理解: D: 访问根节点,L: 遍历根节点的左子树,R:遍历根节点的右子树。 先序遍历:DLR 中序遍历:LDR 后序遍历:LRD 先序遍历的递归算法1234567function preOrder(node) { if (node) { console.log(node.value); preOrder(node.left); preOrder(node.right); }} 中序遍历的递归算法1234567function inOrder(node) { if (node) { inOrder(node.left); console.log(node.value); inOrder(node.left); }} 后序遍历的递归算法1234567function postOrder(node) { if (node) { postOrder(node.left); postOrder(node.right); console.log(node.value); }} 更详细的二叉树算法可以查看这篇文章 定时问题遇到的一个难题是如何实现间隔一段时间(500ms)改变节点的颜色,这就需要用到setTimeout()这个方法。刚开始的想法是把定时函数写进递归函数里面,让每次递归都执行setTimeout(),但是这个方法行不通,会改变每个节点出现的顺序,而且函数执行结束的时间小于定时时间,导致想要达到的效果一瞬间全部执行完毕,而不是按规定的时间一个一个出现,这个理解可能有点错误,但是没办法达到想要的效果,所以放弃。 我的方法是把遍历出来的值放进数组里,然后再用数组完成想要做的各种操作。 12345678910111213function preOrder(node) { if (node) { preOrder(node.left); preOrder(node.right); preList.push(node.value); }}for (let i = 0; i < preList.length; i++) { setTimeout(function() { console.log(preList[i]); // 这样就可以按顺序每隔一段时间打印出一个数字 }, 500 * i);}","categories":[],"tags":[{"name":"算法","slug":"算法","permalink":"https://www.chaohang.top/tags/%E7%AE%97%E6%B3%95/"}]},{"title":"《Algorithms Unlocked》读书笔记3——计数排序","slug":"《Algorithms-Unlocked》读书笔记3——计数排序","date":"2017-04-16T12:55:57.000Z","updated":"2017-04-16T12:55:57.000Z","comments":true,"path":"2017/04/16/《Algorithms-Unlocked》读书笔记3——计数排序/","link":"","permalink":"https://www.chaohang.top/2017/04/16/%E3%80%8AAlgorithms-Unlocked%E3%80%8B%E8%AF%BB%E4%B9%A6%E7%AC%94%E8%AE%B03%E2%80%94%E2%80%94%E8%AE%A1%E6%95%B0%E6%8E%92%E5%BA%8F/","excerpt":"《Algorithms Unlocked》是 《算法导论》的合著者之一 Thomas H. Cormen 写的一本算法基础,算是啃CLRS前的开胃菜和辅助教材。如果CLRS的厚度让人望而生畏,这本200多页的小读本刚好合适带你入门。 书中没有涉及编程语言,直接用文字描述算法,我用 JavaScript 对书中的算法进行描述。 超越下界之前的四个排序算法——选择排序、插入排序、归并排序、快速排序都是依赖于对排序关键字进行的比较。他们的决策依据都是“如果这个元素的排序关键字比另一个元素的排序关键字小,那么就进行相应操作,否则,进行其他操作或者什么也不做。”假如我们还是依赖这一规则,无论是简单或复杂的算法或者还没被发现的算法都无法突破这一下界(最坏情况下所需要的最小时间)。所以我们需要更改游戏规则,不让算法利用比较来进行排序。 计数排序假设我们有一个数组,该数组内的元素都是 0~m-1 范围内的整数。例如 let array = [4, 1, 5, 0, 1, 6, 5, 1, 5, 3] 。如果我们可以知道排序关键字为 5 的元素有三个,并且刚好有 6 个元素的排序关键字小于 5,那么三个 5 应该位于位置6、7、8上。 首先我们要计算出有多少个元素的排序关键字等于某个值。比如有 3 个元素的排序关键字等于 5。 123456789101112131415161718// m:定义了数组array中元素的取值范围 0~m-1function countKeysEqual(array, m) { // 创建一个空数组,长度为m,给每个元素赋值0 // 为什么要有这一步,万一哪个值array里没有就会变成NaN let equal = []; for (let i = 0; i < m; i++) { equal[i] = 0; }; for (let j = 0; j < array.length; j++) { // 把array中的元素作为equal数组的索引值 // 该索引值在equal中对应的值为该元素在array中出现的次数 let key = array[j]; equal[key] += 1; } return equal;}","text":"《Algorithms Unlocked》是 《算法导论》的合著者之一 Thomas H. Cormen 写的一本算法基础,算是啃CLRS前的开胃菜和辅助教材。如果CLRS的厚度让人望而生畏,这本200多页的小读本刚好合适带你入门。 书中没有涉及编程语言,直接用文字描述算法,我用 JavaScript 对书中的算法进行描述。 超越下界之前的四个排序算法——选择排序、插入排序、归并排序、快速排序都是依赖于对排序关键字进行的比较。他们的决策依据都是“如果这个元素的排序关键字比另一个元素的排序关键字小,那么就进行相应操作,否则,进行其他操作或者什么也不做。”假如我们还是依赖这一规则,无论是简单或复杂的算法或者还没被发现的算法都无法突破这一下界(最坏情况下所需要的最小时间)。所以我们需要更改游戏规则,不让算法利用比较来进行排序。 计数排序假设我们有一个数组,该数组内的元素都是 0~m-1 范围内的整数。例如 let array = [4, 1, 5, 0, 1, 6, 5, 1, 5, 3] 。如果我们可以知道排序关键字为 5 的元素有三个,并且刚好有 6 个元素的排序关键字小于 5,那么三个 5 应该位于位置6、7、8上。 首先我们要计算出有多少个元素的排序关键字等于某个值。比如有 3 个元素的排序关键字等于 5。 123456789101112131415161718// m:定义了数组array中元素的取值范围 0~m-1function countKeysEqual(array, m) { // 创建一个空数组,长度为m,给每个元素赋值0 // 为什么要有这一步,万一哪个值array里没有就会变成NaN let equal = []; for (let i = 0; i < m; i++) { equal[i] = 0; }; for (let j = 0; j < array.length; j++) { // 把array中的元素作为equal数组的索引值 // 该索引值在equal中对应的值为该元素在array中出现的次数 let key = array[j]; equal[key] += 1; } return equal;} 接着我们计算出有多少个元素的排序关键字小于该值。比如有 6 个元素的排序关键字小于 5. 1234567891011// equal 为上个函数返回的数组function countKeysLess(equal, m) { let less = []; less[0] = 0; for (let i = 1; i < m; i++) { // less[i] = equal[0] + equal[1] + ... + equal[i - 1] less[i] = less[i - 1] + equal[i - 1]; } return less;} 一旦得到less数组,我们就可以知道每个元素应该放在哪个位置。 12345678910111213141516// 根据less可以得知元素在数组中的位置// 重排数组function rearrange(array, less, m) { let arrB = []; for (let i = 0; i < array.length; i++) { let key = array[i]; // 有几个小于key的元素排在key前面,则为key值在arrB中的索引 // 比如数组[0, 1, 1, 2],有3个排序关键字小于2,则2的索引为3 let index = less[key]; arrB[index] = array[i]; // 自增1,相同值的元素排在该值后一位 less[key] += 1; } return arrB;} 把三个函数组合在一起构成计数排序。 1234567// m:定义了数组array中元素的取值范围 0~m-1function countSort(array, m) { let equal = countKeysEqual(array, m); let less = countKeysLess(equal, m); let arrB = rearrange(array, less, m); return arrB;} 计数排序能够超越比较排序的下界,因为它从来不会对排序关键字进行比较。反之,它将排序关键字作为数组的索引,能进行这样的操作是因为排序关键字均是非常小的整数。如果排序关键字是带有分数的实数,或者是字符串,那么我们就不能使用计数排序了。","categories":[],"tags":[{"name":"算法","slug":"算法","permalink":"https://www.chaohang.top/tags/%E7%AE%97%E6%B3%95/"},{"name":"阅读","slug":"阅读","permalink":"https://www.chaohang.top/tags/%E9%98%85%E8%AF%BB/"}]},{"title":"《Algorithms Unlocked》读书笔记2——二分查找和排序算法","slug":"《Algorithms-Unlocked》读书笔记2——二分查找和排序算法","date":"2017-04-16T12:43:49.000Z","updated":"2017-04-16T12:43:49.000Z","comments":true,"path":"2017/04/16/《Algorithms-Unlocked》读书笔记2——二分查找和排序算法/","link":"","permalink":"https://www.chaohang.top/2017/04/16/%E3%80%8AAlgorithms-Unlocked%E3%80%8B%E8%AF%BB%E4%B9%A6%E7%AC%94%E8%AE%B02%E2%80%94%E2%80%94%E4%BA%8C%E5%88%86%E6%9F%A5%E6%89%BE%E5%92%8C%E6%8E%92%E5%BA%8F%E7%AE%97%E6%B3%95/","excerpt":"《Algorithms Unlocked》是 《算法导论》的合著者之一 Thomas H. Cormen 写的一本算法基础,算是啃CLRS前的开胃菜和辅助教材。如果CLRS的厚度让人望而生畏,这本200多页的小读本刚好合适带你入门。 书中没有涉及编程语言,直接用文字描述算法,我用 JavaScript 对书中的算法进行描述。 二分查找在排好序的数组中查找目标值x。在p到r区间中,总是取索引为q的中间值与x进行比较,如果array[q]大于x,则比较p到q-1区间,否则比较q+1到r区间,直到array[q]等于x或p>r。 1234567891011121314151617181920212223// 利用二分法在已经排好序的数组中查找值xfunction binarySearch(array, x) { let p = 1; let r = array.length - 1; while (p <= r) { let q = Math.round((p + r) / 2); //四舍五入取整 if (array[q] === x) { return q; } else { if (array[q] > x) { // 如果q没有减一,遇到找不到x的情况, // 就会陷入while循环中出不来,因为p会一直等于r r = q - 1; } else { p = q + 1; } } } return 'NOT-FOUND';}","text":"《Algorithms Unlocked》是 《算法导论》的合著者之一 Thomas H. Cormen 写的一本算法基础,算是啃CLRS前的开胃菜和辅助教材。如果CLRS的厚度让人望而生畏,这本200多页的小读本刚好合适带你入门。 书中没有涉及编程语言,直接用文字描述算法,我用 JavaScript 对书中的算法进行描述。 二分查找在排好序的数组中查找目标值x。在p到r区间中,总是取索引为q的中间值与x进行比较,如果array[q]大于x,则比较p到q-1区间,否则比较q+1到r区间,直到array[q]等于x或p>r。 1234567891011121314151617181920212223// 利用二分法在已经排好序的数组中查找值xfunction binarySearch(array, x) { let p = 1; let r = array.length - 1; while (p <= r) { let q = Math.round((p + r) / 2); //四舍五入取整 if (array[q] === x) { return q; } else { if (array[q] > x) { // 如果q没有减一,遇到找不到x的情况, // 就会陷入while循环中出不来,因为p会一直等于r r = q - 1; } else { p = q + 1; } } } return 'NOT-FOUND';} 也可以把二分查找写成递归风格。 123456789101112131415161718192021// 二分法递归风格function recursiveBinarySearch(array, p, r, x) { if (p > r) { // 基础情况 console.log('NOT-FOUND'); return; } let q = Math.round((p + r) / 2); if (array[q] === x) { // 基础情况 console.log(q); return; } else { if (array[q] > x) { recursiveBinarySearch(array, p, q-1, x); } else { recursiveBinarySearch(array, q+1, r, x); } }} 排序选择排序从第一个元素开始遍历,把该元素跟在它之后的所有元素进行比较,选出最小的元素放入该位置。 以书架上的书本排序为例。我们看一眼书架上的第一本书的书名,接着与第二本进行比较,如果第二本书的书名第一个字母的顺序小于第一本,那我们忘掉第一本书的书名,记下第二本书的书名,此时我们并没有对书籍进行移动,只是比较了书名的顺序,并把顺序最小的书名记在脑子里。直到与最后一本进行比较结束,我们把脑子里顺序最小的书名对应的书与第一本书对调了一下位置。 1234567891011121314151617function selectionSort (array) { for (let i = 0; i < array.length - 1; i++) { let smallest = i; let key = array[i]; // 保存当前值 for (let j = i + 1; j < array.length; j++) { // 比较当前值和最小值,如果当前值小于最小值则把当前值的索引赋给smallest if (array[j] < array[smallest]) { smallest = j; } } // 最小值和当前值交换 array[i] = array[smallest]; array[smallest] = key; } return array;} 选择排序效率很低,因为选择排序进行了较多的比较操作,但移动元素的操作次数很少。所以当遇到移动元素相当耗时——或者它们所占空间很大或者它们存储在一个存储较慢的设备中——那么选择排序可能是一个合适的算法。 插入排序以书架为例,假设前4个位置已经排好序了,我们拿起第五本书与第四本进行比较,如果第四本大于第五本,把第四本向右移动一个位置,再把第三本与第五本进行比较,如果第三本还大于第五本,把第三本向右移动一个位置,刚好放入第四本空出来的位置。直到遇到一本小于第五本的书或者已经没有书可以比较了,把第五本书插入小于它的那本书的后面。 12345678910111213141516function insertionSort (array) { for (let i = 1; i < array.length; i++) { let key = array[i]; // 把当前操作值保存到key中 let j = i - 1; // j 为当前值的前一位 // 在j大于等于0且前一位大于当前值时,前一位向右移动一个位置 while (j >= 0 && array[j] > key) { array[j+1] = array[j]; j -= 1; }; // 直到遇到array[j]小于当前操作值或者j小于0时,把当前值插入所空出来的位置 array[j+1] = key; } return array;} 插入排序与选择排序时间差不多,如果移动操作太过耗时最好用选择排序。插入排序适用于数组一开始就已经“基本有序”的状态。 归并排序归并排序中使用一个被称为分治法的通用模式。在分治法中,我们将原问题分解为类似原问题的子问题,并递归的求解这些子问题,然后再合并这些子问题的解来得出原问题的解。 分解:把一个问题分解为多个子问题,这些子问题是更小实例上的原问题。 解决:递归地求解子问题。当子问题足够小时,按照基础情况来求解。 合并:把子问题的解合并成原问题的解。 在归并排序中,我们把数组不断用二分法分解成两个小数组,直到每个数组只剩一个元素(基础情况)。再把小数组排好序并进行合并。 1234567891011121314151617181920// array: 数组// p: 开始索引// r: 末尾索引function mergeSort (array, p, r) { if (p >= r) { return; } else { // 不可以用四舍五入,找了一夜的bug竟然是因为四舍五入这个小蹄子 let q = Math.floor((p + r) / 2); // 递归调用,把数组拆分成两部分,直到每个数组只剩一个元素 mergeSort(array, p, q); mergeSort(array, q + 1, r); // 把两个子数组排序并合并 merge(array, p, q, r); } return array;} 程序的真正工作发生在 merge 函数中。归并排序不是原址的。 假设有两堆已经排好序的书,书堆A和书堆B。把A中的第一本与B中的第一本拿起来比较,小的那本放入书架中,再把A中的“第一本”和B中的“第一本”进行比较,此时的“第一本”不一定是刚才的第一本了,因为已经有一本书放入书架了,不过该书堆的“第一本”任然是该书堆中最小的一本。直到把两堆书全部放入书架。 123456789101112131415161718192021222324252627282930function merge (array, p, q, r) { let n1 = q - p + 1; // 子数组的长度 let n2 = r - q; // 把两个子数组拷贝到B、C数组中 // slice不包含end参数,所以end参数要加一 let arrB = array.slice(p, q + 1); let arrC = array.slice(q + 1, r + 1); // 两个数组的最后一个元素设为无穷大值,确保了无需再检查数组中是否有剩余元素 arrB[n1] = Number.MAX_VALUE; arrC[n2] = Number.MAX_VALUE; // 因为回填入原数组的个数是固定的,所以无穷大值不会被填入,也无需判断是否有剩余 // 一旦B、C两个数组中的所有元素拷贝完就自动终止 // 因为B、C中的元素已经按照非递减顺序排好了,所以最小索引值对应的就是最小值 // 两个子数组的最小值比较,小的则为当前最小值 let i = j = 0; for (let k = p; k < r + 1; k++) { if (arrB[i] < arrC[j]) { array[k] = arrB[i]; i++; } else { array[k] = arrC[j]; j++; } } return;} 由于归并排序不是在原址上工作,需要拷贝出子数组,如果你的储存空间较小或空间非常宝贵,可能不适合使用归并排序。 快速排序与归并排序类似,快速排序也是使用分治模式。与归并排序不同的是,快速排序是在原址上工作的,归并排序是拷贝出两个子数组进行操作并不在原址上工作。 在书架中随机挑选一本书作为主元(这里我们总是选择位于书架最末尾的那本书),所有小于主元的书放在主元左侧,所有大于或等于主元的书放在主元右侧,这时就把书分为左右两组(不包括主元),再分别对这两组书进行相同的操作(递归),直到子数组只剩一本书触发基础情况。 123456789101112131415function quickSort (array, p, r) { if (p >= r) { return; } else { let q = partition(array, p, r); // 递归中不再包含array[q],因为它已经处在正确的位置(左边所有元素都小于它,右边所有元素都大于或等于它) // 如果递归调用还包含array[q],就会陷入死循环 quickSort(array, p, q - 1); quickSort(array, q + 1, r); } return array;} 重要的操作都在 partition 函数中。这个函数把数组按照大于或小于主元分为左右两堆,并返回主元所在位置的索引q。注意,左右两堆数组并不是有序的(见上图),只是大于或小于主元。 在书架中随机挑选一本书作为主元(这里我们总是选择位于书架最末尾的那本书),此时主元位于最末尾。还未进行比较的为未知组,称为组U,位于主元左侧。小于主元的称为组L,位于书架最左侧。大于或小于主元的称为组R,位于组L左侧组U右侧。如下图。 我们拿起组U中最左侧的那本书,与主元进行比较,如果小于主元则放入组L,大于或等于主元则放入组R。放入组R的操作比较简单,只需要把组R和组U的分割线往右移一位,无需移动书籍。 放入组L的操作则比较复杂。我们将它与组R中最左侧的书籍进行调换,并将组L和组R之间的分割线向右移一位,将组R和组U的分割线向右移一位。如下图 12345678910111213141516171819202122232425262728293031// 主元:数组中随机挑选单独的一个数(这里我们总是选数组中的最后一位)array[r]// 组L(左侧组):所有小于主元的数,array[p...q-1]// 组R(右侧组):所有大于或等于主元的数,array[q...u-1]// 组U(未知组):还未进行比较的数,array[u...r-1]function partition(array, p, r) { let q = p; // 遍历array[p...r-1] for (let u = p; u < r; u++) { // 如果未知数小于主元,放入组L if (array[u] < array[r]) { // 把未知数和组R最左侧值(array[q])进行交换,并让q和u往右移一位(加1) let key = array[q]; array[q] = array[u]; array[u] = key; q += 1; } // 如果未知数大于或等于主元,放入组R // 无需其他操作,只需要把u往右移一位 } // 把主元和组R最左侧值(array[q])进行交换,让主元位于组L合组R中间 let key = array[q]; array[q] = array[r]; array[r] = key; return q;} 本例的快速排序总是选择最末尾的元素作为主元,称为确定的快速排序。如果每次选择主元时都从数组中随机选择,则称为随机快速排序,随机快速排序在测试中会快于确定的快速排序。 根据数据量的不同,储存空间的大小,存储速度的快慢,每个排序方法都有不同的表现,并不是说哪个方法一定是最快的,也不一定最快就是最好的,合适才是最好的。","categories":[],"tags":[{"name":"算法","slug":"算法","permalink":"https://www.chaohang.top/tags/%E7%AE%97%E6%B3%95/"},{"name":"阅读","slug":"阅读","permalink":"https://www.chaohang.top/tags/%E9%98%85%E8%AF%BB/"}]},{"title":"《Algorithms Unlocked》读书笔记1——循环和递归","slug":"《Algorithms-Unlocked》读书笔记1——循环和递归","date":"2017-04-10T13:35:03.000Z","updated":"2017-04-10T13:35:03.000Z","comments":true,"path":"2017/04/10/《Algorithms-Unlocked》读书笔记1——循环和递归/","link":"","permalink":"https://www.chaohang.top/2017/04/10/%E3%80%8AAlgorithms-Unlocked%E3%80%8B%E8%AF%BB%E4%B9%A6%E7%AC%94%E8%AE%B01%E2%80%94%E2%80%94%E5%BE%AA%E7%8E%AF%E5%92%8C%E9%80%92%E5%BD%92/","excerpt":"《Algorithms Unlocked》是 《算法导论》的合著者之一 Thomas H. Cormen 写的一本算法基础。 书中没有涉及编程语言,直接用文字描述算法,我用 JavaScript 对书中的算法进行描述。 循环和查找首先是三个简单的查找。目的是从数组中查找一个特定的值。 array: 一个数组 x: 要查找的值 1234567891011121314// 简单的线性查找function linearSearch(array, x) { let answer = 'NOT-FOUND'; for (let i = 0; i < array.length; i++) { if (array[i] === x) { // 虽然找到了i, 但没有返回继续查找,直到 for 结束 answer = i; } } console.log(answer); return;} 虽然找到了目标值,但for循环依然继续遍历直到结束,下面是优化","text":"《Algorithms Unlocked》是 《算法导论》的合著者之一 Thomas H. Cormen 写的一本算法基础。 书中没有涉及编程语言,直接用文字描述算法,我用 JavaScript 对书中的算法进行描述。 循环和查找首先是三个简单的查找。目的是从数组中查找一个特定的值。 array: 一个数组 x: 要查找的值 1234567891011121314// 简单的线性查找function linearSearch(array, x) { let answer = 'NOT-FOUND'; for (let i = 0; i < array.length; i++) { if (array[i] === x) { // 虽然找到了i, 但没有返回继续查找,直到 for 结束 answer = i; } } console.log(answer); return;} 虽然找到了目标值,但for循环依然继续遍历直到结束,下面是优化 1234567891011121314// 优化的查找,找到目标后立刻返回function betterLinearSearch(array, x) { for (let i = 0; i < array.length; i++) { if (array[i] === x) { // 直接返回 console.log(i); return; } } console.log('NOT-FOUND'); return;} 还有一个问题是:假如直到最后都没有找到目标值,将试图访问越过数组末尾的元素。书上说:“在计算机程序中,当你试图访问越过数组末尾的元素时,结果通常是糟糕的。你的程序可能会崩溃,也可能会损坏数据。” 宁可信其有,不可信其无啊。继续优化。 12345678910111213141516171819202122232425// 更优的写法// 总是让 for 循环可以结束function sentinelLinearSearch(array, x) { let n = array.length - 1; // 最后一个元素 // 把数组最后一个值保存到last变量中 let last = array[n] // 把数组最后一个值替换成目标值 array[n] = x; // 判断数组中是否有目标值x,即使没有,数组的最后一个值也一定是目标值,避免越过数组末尾的访问 let i = 0; while (array[i] !== x) { i++; } //如果i小于数组长度,或者最后一个值为目标值x,则返回i array[n] = last; if (i < n || last === x) { console.log(i); return; } return 'NOT-FOUND';} 第三个方案在进行循环遍历的时候只进行了一个判断——array[i]是否等于x,而上面的两种方案在进行for循环时都要进行i是否大于length的判断和array[i]是否等于x两个判断。所以当数组大到一定程度的时候,第三个方案效率大于上面两个方案。 递归递归是指在函数中对函数自身进行调用。 递归有两个特性: 必须有一个或对个基础情况,它是指不用递归而直接计算出结果。比如下面例子中:当 n=0 时,基础情况发生,f(0) = 1; 程序中的每个递归调用一定是通过一系列关于同一个问题的子问题的求解而最终迭代到基础情况。 下面是一个经典的递归例子,计算阶乘。 当n=0时,n! = 1 且 n! = n(n-1)(n-2)…3•2•1 (n≥0) 比如:5! = 5•4•3•2•1 = 120 123456789// 阶乘function factorial(n) { if (n >= 0) { if (n === 0) { return 1; }; return n * factorial(n - 1); };} 之前的查找算法也可以写成递归风格 123456789101112131415// 线性查找的递归风格function recursiveLinearSearch(array, i, x) { if (i < array.length) { if (array[i] === x) { console.log(i); return; }else { return recursiveLinearSearch(array, i+1, x); } } console.log('NOT-FOUND'); return;}","categories":[],"tags":[{"name":"算法","slug":"算法","permalink":"https://www.chaohang.top/tags/%E7%AE%97%E6%B3%95/"},{"name":"阅读","slug":"阅读","permalink":"https://www.chaohang.top/tags/%E9%98%85%E8%AF%BB/"}]},{"title":"Git 的简单使用","slug":"Git-的简单使用","date":"2016-11-20T11:59:39.000Z","updated":"2016-11-20T11:59:39.000Z","comments":true,"path":"2016/11/20/Git-的简单使用/","link":"","permalink":"https://www.chaohang.top/2016/11/20/Git-%E7%9A%84%E7%AE%80%E5%8D%95%E4%BD%BF%E7%94%A8/","excerpt":"一直在使用Git,仅限于简单的使用,但还是记不住几个简单。在这边总结一下,加深印象,也方便查找。 安装Git平常主要在windows和ubuntu上工作,就以windows为例,Linux和Mac平台应该也差不多,反而是windows上坑比较多。 在windows上首先要下载Git,谷歌上搜一下,傻瓜式下载安装。Linux上不同发行版会有点小差异,不过你可以输入git,系统会提示你如何进行安装。 我们可以简单的把Git看成三个部分:本地、暂存区、远程仓库。下面我们来简单介绍各部分的作用。 存入暂存区","text":"一直在使用Git,仅限于简单的使用,但还是记不住几个简单。在这边总结一下,加深印象,也方便查找。 安装Git平常主要在windows和ubuntu上工作,就以windows为例,Linux和Mac平台应该也差不多,反而是windows上坑比较多。 在windows上首先要下载Git,谷歌上搜一下,傻瓜式下载安装。Linux上不同发行版会有点小差异,不过你可以输入git,系统会提示你如何进行安装。 我们可以简单的把Git看成三个部分:本地、暂存区、远程仓库。下面我们来简单介绍各部分的作用。 存入暂存区 在命令行中进入该目录,输入 git init 命令,该命令会把本地文件夹变成Git可以管理的仓库,主要是生成 .git 文件。 git add fileName 命令可以添加想要上传的文件,比如添加 readme.md 就输入 git add readme.md,这个命令可以多次重复使用,添加多个文件。如果要添加一整个文件夹也是同理, git add 文件夹名。比如 git add images。 把添加的文件上传至暂存区,并附上说明:git commit -m "add readme"。 经过这三步就可以把本地文件上传至暂存区。这个时候如果我们在修改本地文件时不小心改乱了,还可以执行版本退回,也可以执行撤销修改,删除文件等操作。由于是简单的入门教程,本文不展开讨论。 添加SSH key之前的操作都是在本地上进行的,现在我们需要把暂存区的文件提交到GitHub仓库中。 首先我们需要有一个GitHub账号,然后利用ssh秘钥,让GitHub可以识别你的电脑。 在本地创建SSH Key: $ ssh-keygen -t rsa -C "youremail@example.com" 把邮箱地址换成你自己的地址,然后一路回车,之后你就可以再用户主目录中找到 .ssh 目录,里面有 id_rsa 和 id_rsa.pub 两个文件,id_rsa 是私钥,不可以泄露,id_rsa.pub 是公钥,我们需要把它复制到GitHub中。 登录你的GitHub账号,在settings中找到SSH and GPG keys选项,点击New SSH key,把 id_rsa.pub 中的内容复制到Key中,并取一个title。点击Add SSH key就完成了。 如果你有多个电脑,可以添加多个key,一个电脑对应一个KEY。光是让GitHub认得这是你的电脑还不够,我们还要把本地仓库与远程仓库进行绑定。 本地仓库其实就是本地文件夹,但我们还没有建立远程仓库,接下来教大家如何建立远程仓库。 创建远程仓库在你的GitHub的头像旁边有一个加号,点击加号在选择New repository,填好Repository name和Description,点击Create repository,一个远程仓库就建立好了。 接下来我们要把本地仓库与远程仓库进行绑定,在本地仓库文件夹中执行: $ git remote add origin git@github.com:yourName/repositoryName.git 把 yourName 换成你的GitHub的账户名,把repositoryName换成刚刚建好的远程仓库名,就可以把本地仓库与远程仓库进行绑定。 本地仓库和远程仓库的名称并不一定要相同,不过相同的名称方便我们管理。 这时我们就可以把本地仓库的内容推送至远程仓库了: $ git push -u origin master 第一次推送时我们加上 -u 参数,可以把本地的 master 分支和远程的 master 分支关联起来,以后就可以简化我们的推送命令。 $ git push origin master 至此,我们已经完成了从本地到远程的所有操作,基本可以满足平时的使用。 题外话如果你在windows上进行工作,我建议使用GitHub的桌面端,因为windows上总是会有一些奇奇怪怪的错误。比如你的用户文件夹是中文名,就会产生问题,而解决这些莫名其妙的问题其实是在浪费时间,最好的解决办法就是换个用户名或者换台电脑。GitHub的桌面端简单易用,不需要我们手动绑定SSH key,只需登录账户就可以使用了。 如果你是新手,遇到难以解决的问题,在试图解决问题但没有成功也没有人能够帮助你的时候,可以先把本地仓库先备份到其他地方,然后删除掉你的本地仓库,重新从远程仓库克隆一份,再进行操作。克隆的命令如下: $ git clone git@github.com:yourName/repositoryName.git 这个方法可以帮助你解决一部分问题,但不保证你遇到的所有问题都能解决。","categories":[],"tags":[{"name":"git","slug":"git","permalink":"https://www.chaohang.top/tags/git/"}]},{"title":"浅谈JavaScript中的apply、call、bind","slug":"浅谈JavaScript中的apply、call、bind","date":"2016-11-17T07:17:19.000Z","updated":"2016-11-17T07:17:19.000Z","comments":true,"path":"2016/11/17/浅谈JavaScript中的apply、call、bind/","link":"","permalink":"https://www.chaohang.top/2016/11/17/%E6%B5%85%E8%B0%88JavaScript%E4%B8%AD%E7%9A%84apply%E3%80%81call%E3%80%81bind/","excerpt":"apply,call,bine 这三兄弟经常让初学者感到疑惑。前两天准备面试时特地做了个比较,其实理解起来也不会太难。","text":"apply,call,bine 这三兄弟经常让初学者感到疑惑。前两天准备面试时特地做了个比较,其实理解起来也不会太难。 applyMDN上的定义:The apply() method calls a function with a given this value and arguments provided as an array (or an array-like object). apply() 方法调用一个函数,指定该函数的 this 值并将一个数组(或类数组对象)作为该函数的参数。 语法 (Syntax)fun.apply(thisArg,[argsArray]) 直接上代码123456function sayColor(arg) { var color = \"red\"; console.log(arg + this.color);}var obj = {color: \"blue\"};sayColor.apply(obj, [\"The color is\"]); //输出\"The color is blue\"此时 this 指向 obj ,则 this.color 为 obj 的 color 属性,则输出 The color is blue。 callcall() 与 apply() 类似,区别在于 apply() 的第二个参数为数组,而 call() 把参数跟在第一个参数后面,并且可以跟多个参数。 语法 (Syntax)fun.call(thisArg, arg1, arg2, arg3 ...) 看代码12345function sayClothe(arg1,arg2) { console.log(arg1 + this.color + arg2);}var obj = {color: \"blue\"};sayClothe.call(obj, \"This is a \", \" clothe\" ); //输出\"This is a blue clothe\" bindbind() 与 call() 类似,有一点不同在于 bind() 返回一个新函数(new function),我们可以随时调用该函数。 语法(Syntax)fun.bind(thisArg, arg1, arg2, arg3 ...) 返回值返回一个具有指定 this 和初始参数的函数副本。 看代码 123456function sayClothe(arg1, arg2) { console.log(arg1 + this.color + arg2);}var obj = {color: \"blue\"};var sayClothe2 = sayClothe.bind(obj, \"This is a \", \" clothe\");sayClothe2(); //输出 \"This is a blue clothe\" 使用有时候我们会碰到 Array.prototype.slice.call(arguments) 这种用法。许多人会疑惑,直接使用 arguments.slice() 不就行了吗,为什么要多此一举。 原因在于,arguments 并不是真正的数组对象,只是 array-like object ,所以它并没有 slice 这个方法。而 Array.prototype.slice.call(arguments) 可以理解为把 slice 的对象指向 arguments ,从而让 arguments 可以使用 slice 方法。如果直接使用 arguments.slice() 则会报错。 bind() 的另一个用法则是让函数拥有预设参数,而又跟预设参数有所不同。 以下例子结合上面两条规则 本例出自MDN 1234567891011121314function list() { return Array.prototype.slice.call(arguments);}var list1 = list(1, 2, 3); //[1, 2, 3]//Create a function with a preset leading argumentsvar leadingThirtysevenList = list.bind(null, 37);var list2 = leadingThirtysevenList();//[37]var list3 = leadingThirtysevenList(1, 2, 3);//[37, 1, 2, 3] 我们想到预设参数就会理所当然的想到 “如果该函数没有参数就使用预设参数,有参数就使用提供的参数”。不过 bind() 所提供的预设参数功能与此不同。 在我们的印象中, list3 应该输出 [1, 2, 3] 但实际输出的却是 [37, 1, 2, 3]。因为 bind() 的特点,leadingThirtysevenList(1, 2, 3) 可以写为 list.bind(null, 37, 1, 2, 3)。 总结apply() call() bind() 三者区别不大,都是用来改变函数的 this 指向。 apply() 把 this 所需参数放入一个数组,作为 apply() 的第二个参数传入。当参数不定时,我们可以传入 arguments。 call() 和 bind() 则把参数按顺序依次传入。 bind() 返回对应函数,便于稍后调用,而 apply()、call()则立即调用 由于其特性,使用起来千奇百怪,有各种各样有趣的用法,还等待我们去挖掘。","categories":[],"tags":[{"name":"JavaScript","slug":"JavaScript","permalink":"https://www.chaohang.top/tags/JavaScript/"}]}]}