无常是常

分享技术见解、学习心得和生活感悟

最新文章

《刻意练习》:以学习编程为例

转载自: https://juejin.im/post/5c3ca16ff265da617573f9a4

《刻意练习》是一本神奇的魔法书,它用大量的事实案例和数据来证明了刻意练习能给一个人带来的巨大改变。更为难能可贵的是,它不仅仅只是介绍刻意练习的好处,还给读者介绍了刻意练习的方法和注意事项,可以说是干货满满的一本书。

本文想以“学习编程”为例,结合《刻意练习》这本书教给我们的方式方法,阐述如何通过刻意练习来提高自己的编程水平。

关于练习

**不断重复只是“天真的练习”,无法带来进步。“正确的练习”需要好导师、有目标、有反馈。**所以如果想要提高编程水平,光靠一味闷着头努力写代码是不够的,有些人写了几年的CRUD,可能整体的技术水平还不及刚毕业的同学。

一个好的导师很难得,他不仅要自己在编程水平是有一定的成就,还应该具有一些教育方面的经验,才能更好的指导你。一般来说,大一点的公司都会有老人带新人的机制,可以利用这个来找一个比较好的前辈带一带。另外就是通过看书或看视频其实也是一种很不错的学习方式,写书和出视频的老师自然是具备上述条件的。

有目标指的是应该要有明确的目标,而不是一个宽泛的目标。比如“我要提高Java水平”明显就是一个宽泛的目标,无法量化。明确的目标应该是什么样子呢?比如,我要学习JVM的基础知识,我要学完23种设计模式等等。一个好的目标还应该是可分解的,有时间或成果量化的。比如,我需要在一个月内学习完Java多线程技术,第一周学习多线程模型,第二周学习多线程基本的类和接口,第三周阅读JDK里面关于多线程方面的工具类的源码,第四周写多线程案例Demo,最后产出一个Java多线程系列笔记或者博客。

有反馈指的是可以量化的反馈。比如写一篇技术博客,有多少阅读量,点赞量,评论量?写一个开源项目有多少star,做一个LeetCode有没有通过,耗时如何?

上述算是对“有目的的练习”的一个阐述。刻意练习是在有目的的练习基础上的。总结起来,有目的的练习应该具有以下四个特点:

1.有目的的练习具有定义明确的特定目标
2.有目的的练习是专注的
3.有目的的练习包含反馈
4.有目的的练习需要走出舒适区

对于任何类型的练习,这是一条基本的真理:如果你从来不迫使自己走出舒适区,便永远无法进步。

关于心理表征

《刻意练习》这本书用了大量的篇幅来介绍心理表征。作者认为,刻意练习的核心之一在于通过练习来创建大量的心理表征,然后再使用这些心理表征反过来帮助练习。

那心理表征到底是什么?

我认为**它是一个人学习某样东西的时候,自己心中形成的一个体系架构。**比如我们学习算法,就会形成一个关于算法的体系架构,也就是心理表征。而当我们再学习到Mysql的底层索引原理的时候,就会想到算法关于平衡树方面的知识;当我们学习JDK源码里关于集合框架的时候,就会想到链表、栈与队列、红黑树等等。

我们的知识形成了一个循环,学得越多,懂得越多。当我们积累了大量的知识以后,再学一样有关联的东西,就能快速学会。

刻意练习

刻意练习提倡“边干边学”,**它使人们熟悉练习的习惯,并思考如何练习。**这与我们学习编程的理念是一样的,几乎没有人会对你说:你只需要看书,看博客,看视频就能学会编程。有经验的前辈一般会告诉你,要有输入,也要有输出。学习了的东西,要通过写代码或者做笔记或者写博客的方式,让知识变成你自己的,更加牢固。

你通过写代码,不断地写代码来提高自己的编程水平,然后通过思考,反思来决定该如何写代码才能写得更好。

传统的方法也一直是先找出关于正确方法的信息,然后很大程度上让学生去运用那些知识。刻意练习则完全相反,它只聚焦于绩效和表现,以及怎样提高绩效和表现。

编程也是一样,老板和上级其实真正在意的并不是你懂多少理论,而是你写得代码质量好不好,你能不能快速地实现他们需要的功能,能不能解决技术难题。

你的代码写得比别人快,你的代码写得比别人漂亮,你能快速解决别人不能解决的难题,那你就牛逼。

关于专注与坚持

刻意练习里面有一个观点颠覆了我以前的认知。我一度认为,一个人能不能专注,能不能坚持做一件事,除了热爱,那必然是意志力越高的人越能专注和坚持。

然而刻意练习告诉我,**意志力根本不存在!**所谓意志力,其实是保持动机与专注的结果。

我们先来看看如何让自己保持专注,**不专注的练习是没有效果的。**刻意练习提供了一个观点,如果练习的时间更短,那就会有更好的注意力。

对于这个观点我是绝对认同的。比如看书,如果想一次性看完一本书,那对于一般人来说绝对是非常困难的。但是如果把它拆成一章一章,每天看一点,那就会好很多。而在日常的工作中或者学习中,我们可以使用“番茄钟”来管理自己的时间与保持专注。这里不具体介绍番茄钟,有兴趣的同学自己去了解一下,亲测有用!一个番茄钟25分钟,不多不少刚刚好。

再来谈谈动机。对于一个程序员来说,动机是再明显不过的了:升职加薪变大佬。但有时候这个动机并不能转化为一种强有力的动力来促使我们去学习,去提高技术水平。而某些太强烈的动机(比如想跳槽)又很难持续保持。

《刻意练习》告诉我们,我们要保持动机,要么强化继续前行的理由,要么弱化停下脚步的理由。

仍然以学习算法为例。要强化继续前行的理由,我们可以在学习到一定阶段给自己一些小的奖励,比如刷完100个算法题,给自己买个好的键盘或者耳机。还有来自朋友、同事、家人乃至默认人的鼓励也很重要。我在网上写博客分享给大家,如果读者的点赞或者好评,那也是非常能激励我们继续学习的!

而弱化停下脚步的理由。比如练习累了就合理休息一下,学习累了就暂时玩一下,如果觉得自己一个人学技术太孤独,就找一群小伙伴一起学习或者加入一个技术社区。目标要精心设置且合理,这样才能得到达到目标的成就感,不会因为达不到目标而气馁放弃。

愿你以梦为马,剑指天涯。

转载自: https://juejin.im/post/5c3ca16ff265da617573f9a4

《刻意练习》是一本神奇的魔法书,它用大量的事实案例和数据来证明了刻意练习能给一个人带来的巨大改变。更为难能可贵的是,它不仅仅只是介绍刻意练习的好处,还给读者介绍了刻意练习的方法和注意事项,可以说是干货满满的一本书。

本文想以“学习编程”为例,结合《刻意练习》这本书教给我们的方式方法,阐述如何通过刻意练习来提高自己的编程水平。

关于练习

**不断重复只是“天真的练习”,无法带来进步。“正确的练习”需要好导师、有目标、有反馈。**所以如果想要提高编程水平,光靠一味闷着头努力写代码是不够的,有些人写了几年的CRUD,可能整体的技术水平还不及刚毕业的同学。

一个好的导师很难得,他不仅要自己在编程水平是有一定的成就,还应该具有一些教育方面的经验,才能更好的指导你。一般来说,大一点的公司都会有老人带新人的机制,可以利用这个来找一个比较好的前辈带一带。另外就是通过看书或看视频其实也是一种很不错的学习方式,写书和出视频的老师自然是具备上述条件的。

有目标指的是应该要有明确的目标,而不是一个宽泛的目标。比如“我要提高Java水平”明显就是一个宽泛的目标,无法量化。明确的目标应该是什么样子呢?比如,我要学习JVM的基础知识,我要学完23种设计模式等等。一个好的目标还应该是可分解的,有时间或成果量化的。比如,我需要在一个月内学习完Java多线程技术,第一周学习多线程模型,第二周学习多线程基本的类和接口,第三周阅读JDK里面关于多线程方面的工具类的源码,第四周写多线程案例Demo,最后产出一个Java多线程系列笔记或者博客。

有反馈指的是可以量化的反馈。比如写一篇技术博客,有多少阅读量,点赞量,评论量?写一个开源项目有多少star,做一个LeetCode有没有通过,耗时如何?

上述算是对“有目的的练习”的一个阐述。刻意练习是在有目的的练习基础上的。总结起来,有目的的练习应该具有以下四个特点:

1.有目的的练习具有定义明确的特定目标
2.有目的的练习是专注的
3.有目的的练习包含反馈
4.有目的的练习需要走出舒适区

对于任何类型的练习,这是一条基本的真理:如果你从来不迫使自己走出舒适区,便永远无法进步。

关于心理表征

《刻意练习》这本书用了大量的篇幅来介绍心理表征。作者认为,刻意练习的核心之一在于通过练习来创建大量的心理表征,然后再使用这些心理表征反过来帮助练习。

那心理表征到底是什么?

我认为**它是一个人学习某样东西的时候,自己心中形成的一个体系架构。**比如我们学习算法,就会形成一个关于算法的体系架构,也就是心理表征。而当我们再学习到Mysql的底层索引原理的时候,就会想到算法关于平衡树方面的知识;当我们学习JDK源码里关于集合框架的时候,就会想到链表、栈与队列、红黑树等等。

我们的知识形成了一个循环,学得越多,懂得越多。当我们积累了大量的知识以后,再学一样有关联的东西,就能快速学会。

刻意练习

刻意练习提倡“边干边学”,**它使人们熟悉练习的习惯,并思考如何练习。**这与我们学习编程的理念是一样的,几乎没有人会对你说:你只需要看书,看博客,看视频就能学会编程。有经验的前辈一般会告诉你,要有输入,也要有输出。学习了的东西,要通过写代码或者做笔记或者写博客的方式,让知识变成你自己的,更加牢固。

你通过写代码,不断地写代码来提高自己的编程水平,然后通过思考,反思来决定该如何写代码才能写得更好。

传统的方法也一直是先找出关于正确方法的信息,然后很大程度上让学生去运用那些知识。刻意练习则完全相反,它只聚焦于绩效和表现,以及怎样提高绩效和表现。

编程也是一样,老板和上级其实真正在意的并不是你懂多少理论,而是你写得代码质量好不好,你能不能快速地实现他们需要的功能,能不能解决技术难题。

你的代码写得比别人快,你的代码写得比别人漂亮,你能快速解决别人不能解决的难题,那你就牛逼。

关于专注与坚持

刻意练习里面有一个观点颠覆了我以前的认知。我一度认为,一个人能不能专注,能不能坚持做一件事,除了热爱,那必然是意志力越高的人越能专注和坚持。

然而刻意练习告诉我,**意志力根本不存在!**所谓意志力,其实是保持动机与专注的结果。

我们先来看看如何让自己保持专注,**不专注的练习是没有效果的。**刻意练习提供了一个观点,如果练习的时间更短,那就会有更好的注意力。

对于这个观点我是绝对认同的。比如看书,如果想一次性看完一本书,那对于一般人来说绝对是非常困难的。但是如果把它拆成一章一章,每天看一点,那就会好很多。而在日常的工作中或者学习中,我们可以使用“番茄钟”来管理自己的时间与保持专注。这里不具体介绍番茄钟,有兴趣的同学自己去了解一下,亲测有用!一个番茄钟25分钟,不多不少刚刚好。

再来谈谈动机。对于一个程序员来说,动机是再明显不过的了:升职加薪变大佬。但有时候这个动机并不能转化为一种强有力的动力来促使我们去学习,去提高技术水平。而某些太强烈的动机(比如想跳槽)又很难持续保持。

《刻意练习》告诉我们,我们要保持动机,要么强化继续前行的理由,要么弱化停下脚步的理由。

仍然以学习算法为例。要强化继续前行的理由,我们可以在学习到一定阶段给自己一些小的奖励,比如刷完100个算法题,给自己买个好的键盘或者耳机。还有来自朋友、同事、家人乃至默认人的鼓励也很重要。我在网上写博客分享给大家,如果读者的点赞或者好评,那也是非常能激励我们继续学习的!

而弱化停下脚步的理由。比如练习累了就合理休息一下,学习累了就暂时玩一下,如果觉得自己一个人学技术太孤独,就找一群小伙伴一起学习或者加入一个技术社区。目标要精心设置且合理,这样才能得到达到目标的成就感,不会因为达不到目标而气馁放弃。

愿你以梦为马,剑指天涯。

Redis常用命令

工作中常用到的Reids命令

  • 模糊匹配key并批量删除
1
redis-cli -u redis://password@ip:port/database keys "business:data:10103*" | xargs redis-cli -u redis://password@ip:port/database del

Git修改commit信息

修改commit信息主要有这几种情况:

  1. 刚刚commit,还没有push,使用git commit --amend
  2. 刚刚push,要修改最近一个push的commit信息,使用git commit --amend
  3. 修改历史push的commit信息,使用git rebase -i HEAD~n【其中的n为记录数】,配合2中的命令

注意:
其中1、2两种情况的修改方式是一样的,但是git log的记录是不同的,第三种方式也是把需要修改的记录调整为最新的提交,然后使用2的方式修改。

Linux常用命令

grep

查看配置文件只匹配不是以“#”开头的行

1
cat .zshrc | grep -v "^#" > profile.txt
1
2
grep -A 200 -B 20 "BadSqlGrammarException" ai-server.log
grep -B 20 "BadSqlGrammarException" ai-server.log

使用tail查看指定行以后的N行日志

1
tail -n +358653 catalina.2021-01-12.out|head -n 20

hash

1
2
3
4
5
6
hash 显示hash缓存
hash –l 显示hash缓存,加参数-l既可以看到hash表命令的路径,也可以看到它的名字,说不定会有别名
hash –p path name 将命令全路径path起别名为name
hash –t name 显示指定命令的完整路径
hash –d name 清除name缓存
hash –r 清除缓存

awk

查询指定日志行数

1
awk '/error/{print; for(i=1; i<=10; i++) {getline; print}}' ai-server.log

grep

查看配置文件只匹配不是以“#”开头的行

1
cat .zshrc | grep -v "^#" > profile.txt
1
2
grep -A 200 -B 20 "BadSqlGrammarException" ai-server.log
grep -B 20 "BadSqlGrammarException" ai-server.log

使用tail查看指定行以后的N行日志

1
tail -n +358653 catalina.2021-01-12.out|head -n 20

hash

1
2
3
4
5
6
hash 显示hash缓存
hash –l 显示hash缓存,加参数-l既可以看到hash表命令的路径,也可以看到它的名字,说不定会有别名
hash –p path name 将命令全路径path起别名为name
hash –t name 显示指定命令的完整路径
hash –d name 清除name缓存
hash –r 清除缓存

awk

查询指定日志行数

1
awk '/error/{print; for(i=1; i<=10; i++) {getline; print}}' ai-server.log

git commit之后,撤销commit

场景

一般情况下,我们写完代码后会执行:

1
2
git add .
git commit -m "xxx"

但是执行完后,想撤回怎么办?可以执行以下命令:

1
git reset --soft HEAD^

这样就可以撤回你的提交,并且不会丢失提交前修改的内容.

理解

HEAD^ 代表上一个版本,同等于HEAD1,如果进行了两次提交,可以写成HEAD2

参数

--mixed

默认参数,不删除工作空间改动的代码,只撤回提交,并且撤回git add .操作

--soft

不删除工作空间改动的代码,撤销提交,但是不撤回git add . 操作

--hard

删除工作空间改动的代码,撤销commit,撤销git add .,直接回退到上次commit

最后

如果commit注释写错了,只是想改一下注释,只需要:

1
git commit --amend

此时会进入默认vim编辑器,修改注释完毕后保存就好了。

场景

一般情况下,我们写完代码后会执行:

1
2
git add .
git commit -m "xxx"

但是执行完后,想撤回怎么办?可以执行以下命令:

1
git reset --soft HEAD^

这样就可以撤回你的提交,并且不会丢失提交前修改的内容.

理解

HEAD^ 代表上一个版本,同等于HEAD1,如果进行了两次提交,可以写成HEAD2

参数

--mixed

默认参数,不删除工作空间改动的代码,只撤回提交,并且撤回git add .操作

--soft

不删除工作空间改动的代码,撤销提交,但是不撤回git add . 操作

--hard

删除工作空间改动的代码,撤销commit,撤销git add .,直接回退到上次commit

最后

如果commit注释写错了,只是想改一下注释,只需要:

1
git commit --amend

此时会进入默认vim编辑器,修改注释完毕后保存就好了。

git rebase和git merge的区别

Description

git rebasegit merge 一样都是用于从一个分支获取并且合并到当前分支,但是他们采取不同的工作方式,以下面的一个工作场景说明其区别.

如图所示:你在一个feature分支进行新特性的开发,与此同时,master 分支的也有新的提交。

http://oss.buzhidao.cc/b454bf1d01ec3dee808830b24dd87c2e.png
为了将master 上新的提交合并到你的feature分支上,你有两种选择:merging or rebasing

merge

执行以下命令:

1
2
git checkout feature
git merge master

或者执行更简单的:
git merge master feature

那么此时在feature上git 自动会产生一个新的commit(merge commit)
look like this:

http://oss.buzhidao.cc/2cebea59e5f82803cb35f99f85b6653d.png

**merge 特点:**自动创建一个新的commit,如果合并的时候遇到冲突,仅需要修改后重新commit
**优点:**记录了真实的commit情况,包括每个分支的详情
**缺点:**因为每次merge会自动产生一个merge commit,所以在使用一些git 的GUI tools,特别是commit比较频繁时,看到分支很杂乱。

rebase

本质是变基,执行以下命令:

1
2
git checkout feature
git rebase master

http://oss.buzhidao.cc/245938aba30e1d7ff14f759eea81eb37.png
**rebase 特点:**会合并之前的commit历史
**优点:**得到更简洁的项目历史,去掉了merge commit
**缺点:**如果合并出现代码问题不容易定位,因为re-write了history
合并时如果出现冲突需要按照如下步骤解决

  • 修改冲突部分
  • git add
  • git rebase --continue
  • (如果第三步无效可以执行 git rebase --skip)

不要在git add 之后习惯性的执行 git commit命令
The Golden Rule of Rebasing rebase 的黄金法则:
never use it on public branches(不要在公共分支上使用)
比如说如下场景:如图所示
http://oss.buzhidao.cc/ac37304e85f5ddf56f1fc302b9e42781.png

如果你rebase master 到你的feature分支:
rebase 将所有master的commit移动到你的feature 的顶端。问题是:其他人还在original master上开发,由于你使用了rebase移动了master,git 会认为你的主分支的历史与其他人的有分歧,会产生冲突。
所以在执行git rebase 之前 问问自己,

会有其他人看这个分支么?
IF YES 不要采用这种带有破坏性的修改commit 历史的rebase命令
IF NO OK,随你便,可以使用rebase

Summary 总结

如果你想要一个干净的,没有merge commit的线性历史树,那么你应该选择git rebase
如果你想保留完整的历史记录,并且想要避免重写commit history的风险,你应该选择使用git merge

参考资料

https://www.atlassian.com/git/tutorials/merging-vs-rebasing/conceptual-overview
https://git-scm.com/book/zh/v2/Git-分支-变基
https://git-scm.com/book/zh/v2/Git-分支-分支的新建与合并#_basic_merging

Description

git rebasegit merge 一样都是用于从一个分支获取并且合并到当前分支,但是他们采取不同的工作方式,以下面的一个工作场景说明其区别.

如图所示:你在一个feature分支进行新特性的开发,与此同时,master 分支的也有新的提交。

http://oss.buzhidao.cc/b454bf1d01ec3dee808830b24dd87c2e.png
为了将master 上新的提交合并到你的feature分支上,你有两种选择:merging or rebasing

merge

执行以下命令:

1
2
git checkout feature
git merge master

或者执行更简单的:
git merge master feature

那么此时在feature上git 自动会产生一个新的commit(merge commit)
look like this:

http://oss.buzhidao.cc/2cebea59e5f82803cb35f99f85b6653d.png

**merge 特点:**自动创建一个新的commit,如果合并的时候遇到冲突,仅需要修改后重新commit
**优点:**记录了真实的commit情况,包括每个分支的详情
**缺点:**因为每次merge会自动产生一个merge commit,所以在使用一些git 的GUI tools,特别是commit比较频繁时,看到分支很杂乱。

rebase

本质是变基,执行以下命令:

1
2
git checkout feature
git rebase master

http://oss.buzhidao.cc/245938aba30e1d7ff14f759eea81eb37.png
**rebase 特点:**会合并之前的commit历史
**优点:**得到更简洁的项目历史,去掉了merge commit
**缺点:**如果合并出现代码问题不容易定位,因为re-write了history
合并时如果出现冲突需要按照如下步骤解决

  • 修改冲突部分
  • git add
  • git rebase --continue
  • (如果第三步无效可以执行 git rebase --skip)

不要在git add 之后习惯性的执行 git commit命令
The Golden Rule of Rebasing rebase 的黄金法则:
never use it on public branches(不要在公共分支上使用)
比如说如下场景:如图所示
http://oss.buzhidao.cc/ac37304e85f5ddf56f1fc302b9e42781.png

如果你rebase master 到你的feature分支:
rebase 将所有master的commit移动到你的feature 的顶端。问题是:其他人还在original master上开发,由于你使用了rebase移动了master,git 会认为你的主分支的历史与其他人的有分歧,会产生冲突。
所以在执行git rebase 之前 问问自己,

会有其他人看这个分支么?
IF YES 不要采用这种带有破坏性的修改commit 历史的rebase命令
IF NO OK,随你便,可以使用rebase

Summary 总结

如果你想要一个干净的,没有merge commit的线性历史树,那么你应该选择git rebase
如果你想保留完整的历史记录,并且想要避免重写commit history的风险,你应该选择使用git merge

参考资料

https://www.atlassian.com/git/tutorials/merging-vs-rebasing/conceptual-overview
https://git-scm.com/book/zh/v2/Git-分支-变基
https://git-scm.com/book/zh/v2/Git-分支-分支的新建与合并#_basic_merging

Nginx反向代理配置

使用nginx做反向代理的时候,可以简单的直接把请求原封不动的转发给下一个服务。设置proxy_pass请求只会替换域名,如果要根据不同的url后缀来访问不同的服务,则需要通过如下方法:

方法一:加"/"

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
server {
listen 8000;
server_name abc.com;
access_log "pipe:rollback /data/log/nginx/access.log interval=1d baknum=7 maxsize=1G" main;

location ^~/user/ {
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-NginX-Proxy true;

proxy_pass http://user/;
}

location ^~/order/ {
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-NginX-Proxy true;

proxy_pass http://order/;
}
}

^~/user/表示匹配前缀是user的请求,proxy_pass的结尾有/, 则会把/user/*后面的路径直接拼接到后面,即移除user。

方法二:rewrite

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
location ^~/user/ {
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-NginX-Proxy true;

rewrite ^/user/(.*)$ /$1 break;
proxy_pass http://user;
}

location ^~/order/ {
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-NginX-Proxy true;

rewrite ^/order/(.*)$ /$1 break;
proxy_pass http://order;
}
}

proxy_pass结尾没有/, rewrite重写了url。

location匹配命令

1
2
3
4
5
~     #波浪线表示执行一个正则匹配,区分大小写
~* #表示执行一个正则匹配,不区分大小写
^~ #^~表示普通字符匹配,如果该选项匹配,只匹配该选项,不匹配别的选项,一般用来匹配目录
= #进行普通字符精确匹配
@ #"@" 定义一个命名的 location,使用在内部定向时,例如 error_page, try_files

location 匹配的优先级(与location在配置文件中的顺序无关)

= 精确匹配会第一个被处理。如果发现精确匹配,nginx停止搜索其他匹配。

普通字符匹配,正则表达式规则和长的块规则将被优先和查询匹配,也就是说如果该项匹配还需去看有没有正则表达式匹配和更长的匹配。

^~ 则只匹配该规则,nginx停止搜索其他匹配,否则nginx会继续处理其他location指令。

最后匹配理带有""和"*"的指令,如果找到相应的匹配,则nginx停止搜索其他匹配;当没有正则表达式或者没有正则表达式被匹配的情况下,那么匹配程度最高的逐字匹配指令会被使用。

使用nginx做反向代理的时候,可以简单的直接把请求原封不动的转发给下一个服务。设置proxy_pass请求只会替换域名,如果要根据不同的url后缀来访问不同的服务,则需要通过如下方法:

方法一:加"/"

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
server {
listen 8000;
server_name abc.com;
access_log "pipe:rollback /data/log/nginx/access.log interval=1d baknum=7 maxsize=1G" main;

location ^~/user/ {
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-NginX-Proxy true;

proxy_pass http://user/;
}

location ^~/order/ {
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-NginX-Proxy true;

proxy_pass http://order/;
}
}

^~/user/表示匹配前缀是user的请求,proxy_pass的结尾有/, 则会把/user/*后面的路径直接拼接到后面,即移除user。

方法二:rewrite

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
location ^~/user/ {
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-NginX-Proxy true;

rewrite ^/user/(.*)$ /$1 break;
proxy_pass http://user;
}

location ^~/order/ {
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-NginX-Proxy true;

rewrite ^/order/(.*)$ /$1 break;
proxy_pass http://order;
}
}

proxy_pass结尾没有/, rewrite重写了url。

location匹配命令

1
2
3
4
5
~     #波浪线表示执行一个正则匹配,区分大小写
~* #表示执行一个正则匹配,不区分大小写
^~ #^~表示普通字符匹配,如果该选项匹配,只匹配该选项,不匹配别的选项,一般用来匹配目录
= #进行普通字符精确匹配
@ #"@" 定义一个命名的 location,使用在内部定向时,例如 error_page, try_files

location 匹配的优先级(与location在配置文件中的顺序无关)

= 精确匹配会第一个被处理。如果发现精确匹配,nginx停止搜索其他匹配。

普通字符匹配,正则表达式规则和长的块规则将被优先和查询匹配,也就是说如果该项匹配还需去看有没有正则表达式匹配和更长的匹配。

^~ 则只匹配该规则,nginx停止搜索其他匹配,否则nginx会继续处理其他location指令。

最后匹配理带有""和"*"的指令,如果找到相应的匹配,则nginx停止搜索其他匹配;当没有正则表达式或者没有正则表达式被匹配的情况下,那么匹配程度最高的逐字匹配指令会被使用。

Redis分布式锁

简介

在一些分布式系统中,应用与应用之间是相互独立部署的,Java应用运行在不同的JVM中,所以,在操作一些共享资源的时候,使用JDK提供的Lock工具类时就有些力不从心,这时候就需要借助外力来实现分布式一致性问题。

通常会使用以下三种方式进行实现:

  • 基于数据库实现分布式锁(悲观锁机制)
  • Zookeeper分布式锁
  • Redis分布式锁

下面简单对比几种方式的优缺点:

方式 优点 缺点
数据库 实现简单、易于理解 对数据库压力大
Redis 易于理解 自己实现、不支持阻塞
Zookeeper 支持阻塞 需要理解Zookeeper、程序复杂
Curator 提供锁的方法 依赖Zookeeper、强一致
Redisson 提供锁的方法、可阻塞

安全和活性的保证

Redis官方文档提出以下三点作为分布式锁的最低保证:

  1. 互斥,在任何给定时刻,只有一个客户端可以持有锁
  2. 无死锁,最终,即使锁定资源的客户端崩溃或分区,也始终可以获得锁
  3. 容错能力,只要大多数Redis节点都处于运行状态,客户端就可以获取和释放锁

基于主备架构实现的不足

使用Redis实现分布式锁最简单的方法就是加锁的时候创建一个带有过期时间Key(这样是为了防止出现死锁),当客户端需要释放锁的时候删除这个Key。

从表面上看没有什么问题,但是当Redis出现宕机的时候怎么办?为了解决单点故障问题,我们可以添加一个从节点,当主节点不可用的时候,切换到从节点,但是这样实际上是不可行的,因为Redis使用的是异步复制。

该模型明显存在的竞争条件:

  1. 客户端A获取主节点的锁
  2. 主节点将Key同步到从节点之前发生宕机
  3. 从节点切换成主节点
  4. 客户端B获取到相同资源的锁,此时A和B同时持有锁,违反了安全性

单实例方案

假设可以克服以上单节点不足的问题,我们可以使用以下命令实现分布式锁:

1
SET resource_name my_random_value NX PX 30000

该命令仅在Key不存在(NX)、且到期时间(PX)为30000毫秒的情况下才设置Key。Key的值为一个随机数,该值要求必须全局唯一,使用全局唯一值是为了在删除Key的时候,Key的值是我们之前设置的值时,才删除Key。(Tips:我们总不能删除其他客户端设置的Key吧?)

可以使用以下Lua脚本完成,因为Lua脚本可以保证两个操作的原子性。

1
2
3
4
5
if redis.call("get",KEYS[1]) == ARGV[1] then
return redis.call("del",KEYS[1])
else
return 0
end

RedLock算法

在算法的分布式版本中,我们假设有5个Master节点,这些节点是完全独立的,我们将各个节点部署在不同的服务器中,以保证他们同时出现故障的概率。

为了获取锁,客户端执行以下操作:

  1. 客户端获取当前时间的时间戳
  2. 客户端尝试在N(N=5)个节点上以相同的Key和Value获取一个锁(此处和单实例方式相同)
  3. 客户端通过从当前时间中减去在步骤1中获得的时间戳,来计算获取锁所花费的时间,当客户端能够在大多数实例(至少3个)中获取锁时 ,并且获取锁所花费的总时间小于锁有效时间,则认为已获取锁。
  4. 如果获取了锁,则将其有效时间视为初始有效时间减去经过的时间
  5. 如果客户端由于某种原因(无法锁定N / 2 + 1实例或有效时间为负数)而未能获得该锁,它将尝试解锁所有实例

释放锁

释放锁很简单,只需在所有实例中释放锁(即使之前在某个实例中没有获取到锁)。

RedLock注意点

  1. 先假设client获取所有实例,所有实例包含相同的key和过期时间(TTL) ,但每个实例set命令时间不同导致不能同时过期,第一个set命令之前是T1,最后一个set命令后为T2,则此client有效获取锁的最小时间为TTL-(T2-T1)-时钟漂移
  2. 对于以N/2+ 1(也就是一半以 上)的方式判断获取锁成功,是因为如果小于一半判断为成功的话,有可能出现多个client都成功获取锁的情况,从而使锁失效
  3. 一个client锁定大多数事例耗费的时间大于或接近锁的过期时间,就认为锁无效,并且解锁这个redis实例(不执行业务) ;只要在TTL时间内成功获取一半以上的锁便是有效锁;否则无效

系统具有活性的三大特征

  1. 能够自动释放锁
  2. 再获取锁失败(不到一半以上),或任务完成后能够释放锁,不用等到其自动过期
  3. 再客户端重试获取锁之前(第一次失败到第二次失败之间的间隔时间)大于获取锁消耗的时间

参考Redis官方文档 https://redis.io/topics/distlock
RedLock分析 [http://martin.kleppmann.com/2016/02/08/how-to-do-distributed-locking.html][http://martin.kleppmann.com/2016/02/08/how-to-do-distributed-locking.html]

简介

在一些分布式系统中,应用与应用之间是相互独立部署的,Java应用运行在不同的JVM中,所以,在操作一些共享资源的时候,使用JDK提供的Lock工具类时就有些力不从心,这时候就需要借助外力来实现分布式一致性问题。

通常会使用以下三种方式进行实现:

  • 基于数据库实现分布式锁(悲观锁机制)
  • Zookeeper分布式锁
  • Redis分布式锁

下面简单对比几种方式的优缺点:

方式 优点 缺点
数据库 实现简单、易于理解 对数据库压力大
Redis 易于理解 自己实现、不支持阻塞
Zookeeper 支持阻塞 需要理解Zookeeper、程序复杂
Curator 提供锁的方法 依赖Zookeeper、强一致
Redisson 提供锁的方法、可阻塞

安全和活性的保证

Redis官方文档提出以下三点作为分布式锁的最低保证:

  1. 互斥,在任何给定时刻,只有一个客户端可以持有锁
  2. 无死锁,最终,即使锁定资源的客户端崩溃或分区,也始终可以获得锁
  3. 容错能力,只要大多数Redis节点都处于运行状态,客户端就可以获取和释放锁

基于主备架构实现的不足

使用Redis实现分布式锁最简单的方法就是加锁的时候创建一个带有过期时间Key(这样是为了防止出现死锁),当客户端需要释放锁的时候删除这个Key。

从表面上看没有什么问题,但是当Redis出现宕机的时候怎么办?为了解决单点故障问题,我们可以添加一个从节点,当主节点不可用的时候,切换到从节点,但是这样实际上是不可行的,因为Redis使用的是异步复制。

该模型明显存在的竞争条件:

  1. 客户端A获取主节点的锁
  2. 主节点将Key同步到从节点之前发生宕机
  3. 从节点切换成主节点
  4. 客户端B获取到相同资源的锁,此时A和B同时持有锁,违反了安全性

单实例方案

假设可以克服以上单节点不足的问题,我们可以使用以下命令实现分布式锁:

1
SET resource_name my_random_value NX PX 30000

该命令仅在Key不存在(NX)、且到期时间(PX)为30000毫秒的情况下才设置Key。Key的值为一个随机数,该值要求必须全局唯一,使用全局唯一值是为了在删除Key的时候,Key的值是我们之前设置的值时,才删除Key。(Tips:我们总不能删除其他客户端设置的Key吧?)

可以使用以下Lua脚本完成,因为Lua脚本可以保证两个操作的原子性。

1
2
3
4
5
if redis.call("get",KEYS[1]) == ARGV[1] then
return redis.call("del",KEYS[1])
else
return 0
end

RedLock算法

在算法的分布式版本中,我们假设有5个Master节点,这些节点是完全独立的,我们将各个节点部署在不同的服务器中,以保证他们同时出现故障的概率。

为了获取锁,客户端执行以下操作:

  1. 客户端获取当前时间的时间戳
  2. 客户端尝试在N(N=5)个节点上以相同的Key和Value获取一个锁(此处和单实例方式相同)
  3. 客户端通过从当前时间中减去在步骤1中获得的时间戳,来计算获取锁所花费的时间,当客户端能够在大多数实例(至少3个)中获取锁时 ,并且获取锁所花费的总时间小于锁有效时间,则认为已获取锁。
  4. 如果获取了锁,则将其有效时间视为初始有效时间减去经过的时间
  5. 如果客户端由于某种原因(无法锁定N / 2 + 1实例或有效时间为负数)而未能获得该锁,它将尝试解锁所有实例

释放锁

释放锁很简单,只需在所有实例中释放锁(即使之前在某个实例中没有获取到锁)。

RedLock注意点

  1. 先假设client获取所有实例,所有实例包含相同的key和过期时间(TTL) ,但每个实例set命令时间不同导致不能同时过期,第一个set命令之前是T1,最后一个set命令后为T2,则此client有效获取锁的最小时间为TTL-(T2-T1)-时钟漂移
  2. 对于以N/2+ 1(也就是一半以 上)的方式判断获取锁成功,是因为如果小于一半判断为成功的话,有可能出现多个client都成功获取锁的情况,从而使锁失效
  3. 一个client锁定大多数事例耗费的时间大于或接近锁的过期时间,就认为锁无效,并且解锁这个redis实例(不执行业务) ;只要在TTL时间内成功获取一半以上的锁便是有效锁;否则无效

系统具有活性的三大特征

  1. 能够自动释放锁
  2. 再获取锁失败(不到一半以上),或任务完成后能够释放锁,不用等到其自动过期
  3. 再客户端重试获取锁之前(第一次失败到第二次失败之间的间隔时间)大于获取锁消耗的时间

参考Redis官方文档 https://redis.io/topics/distlock
RedLock分析 [http://martin.kleppmann.com/2016/02/08/how-to-do-distributed-locking.html][http://martin.kleppmann.com/2016/02/08/how-to-do-distributed-locking.html]

Redis哨兵

Sentinel的分布式特征

Redis Sentinel是一个分布式系统:
Sentinel本身的设计是在为多个Sentinel进程协同合作的配置中运行。

  • 当多个哨兵就给定的主机不再可用的事实达成共识时,将执行故障检测,降低了误报的可能性。
  • 即使不是所有的Sentinel进程都在工作,Sentinel仍可正常工作,从而使系统能够应对故障。毕竟,拥有故障转移系统本身就是一个单点故障,

运行哨兵

1
redis-sentinel /path/to/sentinel.conf

redis-sentinelredis-server的一个软链接,所以也可以使用以下方法:

1
redis-server /path/to/sentinel.conf --sentinel

两种方法的工作原理相同。

但是,再运行Sentinel必须指定配置文件,因为系统将使用此文件来保存当前状态,以便在重新启动时重新加载。如果未指定文件,则会启动失败。

Sentinels默认情况下会监听TCP端口26379连接,因此必须打开26379端口。

Sentinel的基础知识

  1. 一个健壮的集群至少需要三个Sentinel实例
  2. 应该将三个Sentinel实例部署在不同的机器上
  3. 因为Redis使用的是异步复制,所以不能保证在故障转移期间保证数据的写入。

配置哨兵

Redis的源码包包含一个sentinel.conf文件可用于配置Sentinel,典型的最小配置如下所示:

1
2
3
4
sentinel monitor mymaster 127.0.0.1 6379 2
sentinel down-after-milliseconds mymaster 60000
sentinel failover-timeout mymaster 180000
sentinel parallel-syncs mymaster 1

sentinel monitor含义如下:

1
sentinel monitor <master-group-name> <ip> <port> <quorum>

<master-group-name> 指主节点名称,<ip> IP地址 <port>端口号,重点说一下<quorum>
假如有5个Sentinel进程,并且给定主服务器的quorum置为2,则将发生以下情况:

  • 如果有两个哨兵同时发现主节点不可访问,则其中一个哨兵将尝试启动故障转移。
  • 如果有三个哨兵同时发现主节点不可访问,则将启动故障转移。
1
sentinel down-after-milliseconds

down-after-milliseconds是指Sentinel在指定的时间内没有获得实例的响应,则认为实例已关闭。

1
sentinel parallel-syncs

parallel-syncs 设置每次可以对几个副本进行同步数据

Redis 哨兵部署示例

经典三节点最小部署

1
2
3
4
5
6
7
8
9
10
11
        +----+
| M1 |
| S1 |
+----+
|
+----+ | +----+
| R2 |----+----| R3 |
| S2 | | S3 |
+----+ +----+

Configuration: quorum = 2

如果主M1发生故障,则S2和S3将达成协议,并能够开启故障转移,从而使客户端能够继续使用。

模拟发生网络分区

1
2
3
4
5
6
7
8
9
10
11
12
         +----+
| M1 |
| S1 | <- C1 (writes will be lost)
+----+
|
/
/
+------+ | +----+
| [M2] |----+----| R3 |
| S2 | | S3 |
+------+ +----+

在这种情况下,网络分区隔离了旧的主数据库M1,因此副本R2被提升为主数据库。但是,与旧主服务器位于同一分区中的客户端(例如C1)可能会继续向旧主服务器写入数据。该数据将永远丢失,因为当分区恢复正常时,主服务器将被重新配置为新主服务器的副本,从而造成数据丢失。

使用以下Redis复制功能可以缓解此问题,如果主服务器检测到副本数量没有达到指定的副本数据,则停止接受数据写入。

1
2
3
4
# 最小副本数量
min-replicas-to-write 1
# 发送异步确认的最大时间
min-replicas-max-lag 10

Sentinel的分布式特征

Redis Sentinel是一个分布式系统:
Sentinel本身的设计是在为多个Sentinel进程协同合作的配置中运行。

  • 当多个哨兵就给定的主机不再可用的事实达成共识时,将执行故障检测,降低了误报的可能性。
  • 即使不是所有的Sentinel进程都在工作,Sentinel仍可正常工作,从而使系统能够应对故障。毕竟,拥有故障转移系统本身就是一个单点故障,

运行哨兵

1
redis-sentinel /path/to/sentinel.conf

redis-sentinelredis-server的一个软链接,所以也可以使用以下方法:

1
redis-server /path/to/sentinel.conf --sentinel

两种方法的工作原理相同。

但是,再运行Sentinel必须指定配置文件,因为系统将使用此文件来保存当前状态,以便在重新启动时重新加载。如果未指定文件,则会启动失败。

Sentinels默认情况下会监听TCP端口26379连接,因此必须打开26379端口。

Sentinel的基础知识

  1. 一个健壮的集群至少需要三个Sentinel实例
  2. 应该将三个Sentinel实例部署在不同的机器上
  3. 因为Redis使用的是异步复制,所以不能保证在故障转移期间保证数据的写入。

配置哨兵

Redis的源码包包含一个sentinel.conf文件可用于配置Sentinel,典型的最小配置如下所示:

1
2
3
4
sentinel monitor mymaster 127.0.0.1 6379 2
sentinel down-after-milliseconds mymaster 60000
sentinel failover-timeout mymaster 180000
sentinel parallel-syncs mymaster 1

sentinel monitor含义如下:

1
sentinel monitor <master-group-name> <ip> <port> <quorum>

<master-group-name> 指主节点名称,<ip> IP地址 <port>端口号,重点说一下<quorum>
假如有5个Sentinel进程,并且给定主服务器的quorum置为2,则将发生以下情况:

  • 如果有两个哨兵同时发现主节点不可访问,则其中一个哨兵将尝试启动故障转移。
  • 如果有三个哨兵同时发现主节点不可访问,则将启动故障转移。
1
sentinel down-after-milliseconds

down-after-milliseconds是指Sentinel在指定的时间内没有获得实例的响应,则认为实例已关闭。

1
sentinel parallel-syncs

parallel-syncs 设置每次可以对几个副本进行同步数据

Redis 哨兵部署示例

经典三节点最小部署

1
2
3
4
5
6
7
8
9
10
11
        +----+
| M1 |
| S1 |
+----+
|
+----+ | +----+
| R2 |----+----| R3 |
| S2 | | S3 |
+----+ +----+

Configuration: quorum = 2

如果主M1发生故障,则S2和S3将达成协议,并能够开启故障转移,从而使客户端能够继续使用。

模拟发生网络分区

1
2
3
4
5
6
7
8
9
10
11
12
         +----+
| M1 |
| S1 | <- C1 (writes will be lost)
+----+
|
/
/
+------+ | +----+
| [M2] |----+----| R3 |
| S2 | | S3 |
+------+ +----+

在这种情况下,网络分区隔离了旧的主数据库M1,因此副本R2被提升为主数据库。但是,与旧主服务器位于同一分区中的客户端(例如C1)可能会继续向旧主服务器写入数据。该数据将永远丢失,因为当分区恢复正常时,主服务器将被重新配置为新主服务器的副本,从而造成数据丢失。

使用以下Redis复制功能可以缓解此问题,如果主服务器检测到副本数量没有达到指定的副本数据,则停止接受数据写入。

1
2
3
4
# 最小副本数量
min-replicas-to-write 1
# 发送异步确认的最大时间
min-replicas-max-lag 10

消息中间件选型

本文主要对比常用的三种中间件优劣势,包含RocketMQ、Kafka、RabbitMQ。

Kafka

优势

  • Kafka的吞吐量几乎是行业里最优秀的,在常规的机器配置下,一台机器可以达到每秒十几万的QPS,相当的强悍。
  • Kafka性能也很高,基本上发送消息给Kafka都是毫秒级的性能。可用性也很高,Kafka是可以支持集群部署的,其中部分机器宕机是可以继续运行的。
  • Kafka技术在各大公司里的使用,基本行业里的一个标准,是把Kafka用在用户行为日志的采集和传输上,比如大数据团队要收集APP上用户的一些行为日志,这种日志就是用Kafka来收集和传输的。

劣势

  • Kafka比较为人诟病的一点,似乎是丢数据方面的问题,因为Kafka收到消息之后会写入一个磁盘缓冲区里,并没有直接落地到物理磁盘上去,所以要是机器本身故障了,可能会导致磁盘缓冲区里的数据丢失。
  • Kafka另外一个比较大的缺点,就是功能非常的单一,主要是支持发送消息给他,然后从里面消费消息,其他就没有什么额外的高级功能了。所以基于Kafka有限的功能,可能适用的场景并不是很多。

总结

Kafka适用于那种日志适当丢失数据是没有关系的,而且一般量特别大,要求吞吐量要高,一般就是收发消息,不需要太多的高级功能,所以Kafka是非常适合这种场景的。

RabbitMQ

优势

  • RabbitMQ的优势在于可以保证数据不丢失,也能保证高可用性,即集群部署的时候部分机器宕机可以继续运行,然后支持部分高级功能,比如说死信队列,消息重试之类的,这些是他的优点。
  • 在RocketMQ出现之前,国内大部分公司都从ActiveMQ切换到RabbitMQ来使用,包括很多一线互联网大厂,而且直到现在都有很多中小型公司在使用RabbitMQ,所以说RabbitMQ网上的资料相对多,而且社区非常活跃,更新频率非常快。

劣势

  • RabbitMQ的吞吐量是比较低,一般就是每秒几万的级别,所以如果遇到特别特别高并发的情况下,支撑起来是有点困难的。
  • RabbitMQ进行集群扩展的时候(也就是加机器部署),比较麻烦。
  • 另外还有一个较为致命的缺陷,就是他的开发语言是erlang,国内很少有精通erlang语言的工程师,因此也没办法去阅读他的源代码,甚至修改他的源代码。

总结

现在行业里的一个情况是,很多BAT等一线互联网大厂都切换到使用更加优秀的RocketMQ了,但是很多中小型公司觉得RabbitMQ基本可以满足自己的需求还在继续使用中,因为中小型公司并不需要特别高的吞吐量,RabbitMQ已经足以满足他们的需求了,而且也不需要部署特别大规模的集群,也没必要去阅读和修改RabbitMQ的源码。

RocketMQ

优势

  • RocketMQ的吞吐量很高,单机可以达到10万QPS以上,而且可以保证高可用性,性能很高,而且支持通过配置保证数据绝对不丢失,可以部署大规模的集群,还支持各种高级的功能,比如说延迟消息、事务消息、消息回溯、死信队列、消息积压,等等。

  • RocketMQ是基于Java开发的,符合国内大多数公司的技术栈,很容易就可以阅读他的源码,甚至是修改他的源码。

  • RocketMQ是非常适合用在Java业务系统架构中的,因为他很高的性能表现,还有他的高阶功能的支持,可以让我们解决各种业务问题。

劣势

  • RocketMQ也有一点美中不足的地方,就是经过调查发现,RocketMQ的官方文档相对简单一些,但是Kafka和RabbitMQ的官方文档就非常的全面和详细,这可能是RocketMQ目前唯一的缺点。

总结

RocketMQ相对来说比较完美的一个消息中间件,在支撑高吞吐量的同时,而且还支持很多高阶的功能,很适合以Java为技术栈的公司使用。

本文主要对比常用的三种中间件优劣势,包含RocketMQ、Kafka、RabbitMQ。

Kafka

优势

  • Kafka的吞吐量几乎是行业里最优秀的,在常规的机器配置下,一台机器可以达到每秒十几万的QPS,相当的强悍。
  • Kafka性能也很高,基本上发送消息给Kafka都是毫秒级的性能。可用性也很高,Kafka是可以支持集群部署的,其中部分机器宕机是可以继续运行的。
  • Kafka技术在各大公司里的使用,基本行业里的一个标准,是把Kafka用在用户行为日志的采集和传输上,比如大数据团队要收集APP上用户的一些行为日志,这种日志就是用Kafka来收集和传输的。

劣势

  • Kafka比较为人诟病的一点,似乎是丢数据方面的问题,因为Kafka收到消息之后会写入一个磁盘缓冲区里,并没有直接落地到物理磁盘上去,所以要是机器本身故障了,可能会导致磁盘缓冲区里的数据丢失。
  • Kafka另外一个比较大的缺点,就是功能非常的单一,主要是支持发送消息给他,然后从里面消费消息,其他就没有什么额外的高级功能了。所以基于Kafka有限的功能,可能适用的场景并不是很多。

总结

Kafka适用于那种日志适当丢失数据是没有关系的,而且一般量特别大,要求吞吐量要高,一般就是收发消息,不需要太多的高级功能,所以Kafka是非常适合这种场景的。

RabbitMQ

优势

  • RabbitMQ的优势在于可以保证数据不丢失,也能保证高可用性,即集群部署的时候部分机器宕机可以继续运行,然后支持部分高级功能,比如说死信队列,消息重试之类的,这些是他的优点。
  • 在RocketMQ出现之前,国内大部分公司都从ActiveMQ切换到RabbitMQ来使用,包括很多一线互联网大厂,而且直到现在都有很多中小型公司在使用RabbitMQ,所以说RabbitMQ网上的资料相对多,而且社区非常活跃,更新频率非常快。

劣势

  • RabbitMQ的吞吐量是比较低,一般就是每秒几万的级别,所以如果遇到特别特别高并发的情况下,支撑起来是有点困难的。
  • RabbitMQ进行集群扩展的时候(也就是加机器部署),比较麻烦。
  • 另外还有一个较为致命的缺陷,就是他的开发语言是erlang,国内很少有精通erlang语言的工程师,因此也没办法去阅读他的源代码,甚至修改他的源代码。

总结

现在行业里的一个情况是,很多BAT等一线互联网大厂都切换到使用更加优秀的RocketMQ了,但是很多中小型公司觉得RabbitMQ基本可以满足自己的需求还在继续使用中,因为中小型公司并不需要特别高的吞吐量,RabbitMQ已经足以满足他们的需求了,而且也不需要部署特别大规模的集群,也没必要去阅读和修改RabbitMQ的源码。

RocketMQ

优势

  • RocketMQ的吞吐量很高,单机可以达到10万QPS以上,而且可以保证高可用性,性能很高,而且支持通过配置保证数据绝对不丢失,可以部署大规模的集群,还支持各种高级的功能,比如说延迟消息、事务消息、消息回溯、死信队列、消息积压,等等。

  • RocketMQ是基于Java开发的,符合国内大多数公司的技术栈,很容易就可以阅读他的源码,甚至是修改他的源码。

  • RocketMQ是非常适合用在Java业务系统架构中的,因为他很高的性能表现,还有他的高阶功能的支持,可以让我们解决各种业务问题。

劣势

  • RocketMQ也有一点美中不足的地方,就是经过调查发现,RocketMQ的官方文档相对简单一些,但是Kafka和RabbitMQ的官方文档就非常的全面和详细,这可能是RocketMQ目前唯一的缺点。

总结

RocketMQ相对来说比较完美的一个消息中间件,在支撑高吞吐量的同时,而且还支持很多高阶的功能,很适合以Java为技术栈的公司使用。