开发者博客 – IT技术 尽在开发者博客

开发者博客 – 科技是第一生产力


  • 首页

  • 归档

  • 搜索

代码整洁 vs 代码肮脏 一、命名的艺术 二、注释 三、函数

发表于 2019-09-16

写出整洁的代码,是每个程序员的追求。《clean code》指出,要想写出好的代码,首先得知道什么是肮脏代码、什么是整洁代码;然后通过大量的刻意练习,才能真正写出整洁的代码。

WTF/min是衡量代码质量的唯一标准,Uncle Bob在书中称糟糕的代码为沼泽(wading),这只突出了我们是糟糕代码的受害者。国内有一个更适合的词汇:屎山,虽然不是很文雅但是更加客观,程序员既是受害者也是加害者。

对于什么是整洁的代码,书中给出了大师们的总结:

  • Bjarne Stroustrup:优雅且高效;直截了当;减少依赖;只做好一件事
  • Grady booch:简单直接
  • Dave thomas:可读,可维护,单元测试
  • Ron Jeffries:不要重复、单一职责,表达力(Expressiveness)

其中,我最喜欢的是表达力(Expressiveness)这个描述,这个词似乎道出了好代码的真谛:用简单直接的方式描绘出代码的功能,不多也不少。

本文记录阅读《clean code》之后个人“深有同感”或者“醍醐灌顶”的一些观点。

一、命名的艺术

坦白的说,命名是一件困难的事情,要想出一个恰到好处的命名需要一番功夫,尤其我们的母语还不是编程语言所通用的英语。不过这一切都是值得了,好的命名让你的代码更直观,更有表达力。

好的命名应该有下面的特征:

1.1 名副其实

好的变量名告诉你:是什么东西,为什么存在,该怎么使用

如果需要通过注释来解释变量,那么就先得不那么名副其实了。

下面是书中的一个示例代码,展示了命名对代码质量的提升

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
复制代码# bad code
def getItem(theList):
ret = []
for x in theList:
if x[0] == 4:
ret.append(x)
return ret

# good code
def getFlaggedCell(gameBoard):
'''扫雷游戏,flagged: 翻转'''
flaggedCells = []
for cell in gameBoard:
if cell.IsFlagged():
flaggedCells.append(cell)
return flaggedCells

1.2 避免误导

  • 不要挂羊头卖狗肉
  • 不要覆盖惯用缩略语

这里不得不吐槽前两天才看到的一份代码,居然使用了 l 作为变量名;而且,user居然是一个list(单复数都没学好!!)

1.3 有意义的区分

代码是写给机器执行,也是给人阅读的,所以概念一定要有区分度。

1
2
3
4
5
6
7
复制代码# bad
def copy(a_list, b_list):
pass

# good
def copy(source, destination):
pass

1.4 使用读的出来的单词

如果名称读不出来,那么讨论的时候就会像个傻鸟

1.5 使用方便搜索的命名

名字长短应与其作用域大小相对应

1.6 避免思维映射

比如在代码中写一个temp,那么读者就得每次看到这个单词的时候翻译成其真正的意义

二、注释

有表达力的代码是无需注释的:The proper use of comments is to compensate for our failure to express ourself in code.

注释的适当作用在于弥补我们用代码表达意图时遇到的失败,这听起来让人沮丧,但事实确实如此。The truth is in the code, 注释只是二手信息,二者的不同步或者不等价是注释的最大问题。

书中给出了一个非常形象的例子来展示:用代码来阐述,而非注释

1
2
3
4
5
6
复制代码bad
// check to see if the employee is eligible for full benefit
if ((employee.flags & HOURLY_FLAG) && (employee.age > 65))

good
if (employee.isEligibleForFullBenefits())

因此,当想要添加注释的时候,可以想想是否可以通过修改命名,或者修改函数(代码)的抽象层级来展示代码的意图。

当然,也不能因噎废食,书中指出了以下一些情况属于好的注释

  • 法务信息
  • 对意图的注释,为什么要这么做
  • 警示
  • TODO注释
  • 放大看似不合理之物的重要性

其中个人最赞同的是第2点和第5点,做什么很容易通过命名表达,但为什么要这么做则并不直观,特别涉及到专业知识、算法的时候。另外,有些第一感觉“不那么优雅”的代码,也许有其特殊愿意,那么这样的代码就应该加上注释,说明为什么要这样,比如为了提升关键路径的性能,可能会牺牲部分代码的可读性。

最坏的注释就是过时或者错误的注释,这对于代码的维护者(也许就是几个月后的自己)是巨大的伤害,可惜除了code review,并没有简单易行的方法来保证代码与注释的同步。

三、函数

3.1 函数的单一职责

一个函数应该只做一件事,这件事应该能通过函数名就能清晰的展示。判断方法很简单:看看函数是否还能再拆出一个函数。

函数要么做什么do_sth, 要么查询什么query_sth。最恶心的就是函数名表示只会query_sth, 但事实上却会do_sth, 这使得函数产生了副作用。比如书中的例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
复制代码public class UserValidator {
private Cryptographer cryptographer;
public boolean checkPassword(String userName, String password) {
User user = UserGateway.findByName(userName);
if (user != User.NULL) {
String codedPhrase = user.getPhraseEncodedByPassword();
String phrase = cryptographer.decrypt(codedPhrase, password);
if ("Valid Password".equals(phrase)) {
Session.initialize();
return true;
}
}
return false;
}
}

3.2 函数的抽象层级

每个函数一个抽象层次,函数中的语句都要在同一个抽象层级,不同的抽象层级不能放在一起。比如我们想把大象放进冰箱,应该是这个样子的:

1
2
3
4
复制代码def pushElephantIntoRefrige():
openRefrige()
pushElephant()
closeRefrige()

函数里面的三句代码在同一个层级(高度)描述了要完成把大象放进冰箱这件事顺序相关的三个步骤。显然,pushElephant这个步骤又可能包含很多子步骤,但是在pushElephantIntoRefrige这个层级,是无需知道太多细节的。

当我们想通过阅读代码的方式来了解一个新的项目时,一般都是采取广度优先的策略,自上而下的阅读代码,先了解整体结构,然后再深入感兴趣的细节。如果没有对实现细节进行良好的抽象(并凝练出一个名副其实的函数),那么阅读者就容易迷失在细节的汪洋里。

某种程度看来,这个跟金字塔原理也很像

file

每一个层级都是为了论证其上一层级的观点,同时也需要下一层级的支持;同一层级之间的多个论点又需要以某种逻辑关系排序。pushElephantIntoRefrige就是中心论点,需要多个子步骤的支持,同时这些子步骤之间也有逻辑先后顺序。

3.3 函数参数

函数的参数越多,组合出的输入情况就愈多,需要的测试用例也就越多,也就越容易出问题。

输出参数相比返回值难以理解,这点深有同感,输出参数实在是很不直观。从函数调用者的角度,一眼就能看出返回值,而很难识别输出参数。输出参数通常逼迫调用者去检查函数签名,这个实在不友好。

向函数传入Boolean(书中称之为 Flag Argument)通常不是好主意。尤其是传入True or False后的行为并不是一件事情的两面,而是两件不同的事情时。这很明显违背了函数的单一职责约束,解决办法很简单,那就是用两个函数。

3.4 Dont repear yourself

在函数这个层级,是最容易、最直观实现复用的,很多IDE也难帮助我们讲一段代码重构出一个函数。

不过在实践中,也会出现这样一种情况:一段代码在多个方法中都有使用,但是又不完全一样,如果抽象成一个通用函数,那么就需要加参数、加if else区别。这样就有点尴尬,貌似可以重构,但又不是很完美。

造成上述问题的某种情况是因为,这段代码也违背了单一职责原则,做了不只一件事情,这才导致不好复用,解决办法是进行方法的细分,才能更好复用。也可以考虑template method来处理差异的部分。

四、测试

非常惭愧的是,在我经历的项目中,测试(尤其是单元测试)一直都没有得到足够的重视,也没有试行过TDD。正因为缺失,才更感良好测试的珍贵。

我们常说,好的代码需要有可读性、可维护性、可扩展性,好的代码、架构需要不停的重构、迭代,但自动化测试是保证这一切的基础,没有高覆盖率的、自动化的单元测试、回归测试,谁都不敢去修改代码,只能任其腐烂。

即使针对核心模块写了单元测试,一般也很随意,认为这只是测试代码,配不上生产代码的地位,以为只要能跑通就行了。这就导致测试代码的可读性、可维护性非常差,然后导致测试代码很难跟随生产代码一起更新、演化,最后导致测试代码失效。所以说,脏测试 - 等同于 - 没测试。

因此,测试代码的三要素:可读性,可读性,可读性。

对于测试的原则、准则如下:

  • You are not allowed to write any production code unless it is to make a failing unit test pass. 没有测试之前不要写任何功能代码
  • You are not allowed to write any more of a unit test than is sufficient to fail; and compilation failures are failures. 只编写恰好能够体现一个失败情况的测试代码
  • You are not allowed to write any more production code than is sufficient to pass the one failing unit test. 只编写恰好能通过测试的功能代码

测试的FIRST准则:

  • 快速(Fast)测试应该够快,尽量自动化。
  • 独立(Independent) 测试应该应该独立。不要相互依赖
  • 可重复(Repeatable) 测试应该在任何环境上都能重复通过。
  • 自我验证(Self-Validating) 测试应该有bool输出。不要通过查看日志这种低效率方式来判断测试是否通过
  • 及时(Timely) 测试应该及时编写,在其对应的生产代码之前编写

该文章通过 openwrite.cn/ 工具创作并群发。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

后端程序员必备:RocketMQ相关流程图/原理图

发表于 2019-09-15

前言

整理了一些RocketMQ相关流程图/原理图,做一下笔记,大家一起学习。

RocketMQ是什么

  • 是一个队列模型的消息中间件,具有高性能、高可靠、高实时、分布式特点。
  • Producer、Consumer、队列都可以分布式。
  • Producer 向一些队列轮流发送消息,队列集合称为 Topic,Consumer 如果做广播消费,则一个 consumer
    实例消费这个 Topic 对应的所有队列,如果做集群消费,则多个 Consumer 实例平均消费这个 topic 对应的队列集合。
  • 能够保证严格的消息顺序
  • 提供丰富的消息拉取模式
  • 高效的订阅者水平扩展能力
  • 实时的消息订阅机制
  • 亿级消息堆积能力
  • 较少的依赖

RocketMQ 核心组件图

RocketMQ是开源的消息中间件,它主要由NameServer,Producer,Broker,Consumer四部分构成。

NameServer

NameServer主要负责Topic和路由信息的管理,功能类似Dubbo的zookeeper。

Producer

消息生产者,负责产生消息,一般由业务系统负责产生消息。

Broker

消息中转角色,负责存储消息,转发消息。

Consumer

消息消费者,负责消息消费,一般是后台系统负责异步消费。

RokcetMQ 物理部署图

NameServer

NameServer是一个几乎无状态节点,可集群部署,节点之间无任何信息同步。

Broker

Broker分为Master与Slave,一个Master可以对应多个Slave,但是一个Slave只能对应一个Master,Master与Slave的对应关系通过指定相同的BrokerName,不同的BrokerId来定义,BrokerId为0表示Master,非0表示Slave。Master也可以部署多个。每个Broker与Name Server集群中的所有节点建立长连接,定时注册Topic信息到所有Name Server。

Producer

Producer与Name Server集群中的其中一个节点(随机选择)建立长连接,定期从Name Server取Topic路由信息,并向提供Topic服务的Master建立长连接,且定时向Master发送心跳。Producer完全无状态,可集群部署。

Consumer

Consumer与Name Server集群中的其中一个节点(随机选择)建立长连接,定期从Name Server取Topic路由信息,并向提供Topic服务的Master、Slave建立长连接,且定时向Master、Slave发送心跳。Consumer既可以从Master订阅消息,也可以从Slave订阅消息,订阅规则由Broker配置决定。

RocketMQ 逻辑部署结构

Producer Group

用来表示一个发送消息应用,一个 Producer Group 下包含多个 Producer 实例,可以是多台机器,也可以
是一台机器的多个进程,或者一个进程的多个 Producer 对象。一个 Producer Group 可以发送多个 Topic
消息,Producer Group 作用如下:

  • 标识一类 Producer
  • 可以通过运维工具查询这个发送消息应用下有多个 Producer 实例
  • 发送分布式事务消息时,如果 Producer 中途意外宕机,Broker 会主动回调 Producer Group 内的任意
    一台机器来确认事务状态。

Consumer Group

用来表示一个消费消息应用,一个 Consumer Group 下包含多个 Consumer 实例,可以是多台机器,也可
以是多个进程,或者是一个进程的多个 Consumer 对象。一个 Consumer Group 下的多个 Consumer 以均摊
方式消费消息,如果设置为广播方式,那么这个 Consumer Group 下的每个实例都消费全量数据。

NameServer 路由注册、删除机制

  • Broker每30秒向NameServer发送心跳包,心跳包中包含topic的路由信息
  • NarneServer 收到 Broker 心跳包后 更新 brokerLiveTable 中的信息, 特别记录心跳时间 lastUpdateTime
  • NarneServer 每隔 10s 扫描 brokerLiveTable, 检 测表中上次收到心跳包的时间,比较当前时间 与上一次时间,如果超过120s,则认为 broker 不可用,移除路由表中与该 broker相关的所有 信息
  • 消息生产者拉取主题的路由信息,即消息生产者并不会立即感知 Broker 服务器的新增与删除。

RocketMQ的消息领域模型图

Topic

  • Topic表示消息的第一级类型,比如一个电商系统的消息可以分为:交易消息、物流消息等。一条消息必须有一个Topic。
  • 最细粒度的订阅单位,一个Group可以订阅多个Topic的消息。

Tag

Tag表示消息的第二级类型,比如交易消息又可以分为:交易创建消息,交易完成消息等。RocketMQ提供2级消息分类,方便灵活控制。

Group

组,一个组可以订阅多个Topic。

Message Queue

消息的物理管理单位。一个Topic下可以有多个Queue,Queue的引入使得消息的存储可以分布式集群化,具有了水平扩展能力。

在 RocketMQ 中,所有消息队列都是持久化,长度无限的数据结构,所谓长度无限是指队列中的每个存储单元都是定长,访问其中的存储单元使用 Offset 来访问,offset 为 java long 类型,64 位,理论上在 100年内不会溢出,所以认为是长度无限。

也可以认为 Message Queue 是一个长度无限的数组,Offset 就是下标。

顺序消息原理图

消费消息的顺序要同发送消息的顺序一致,在 RocketMQ 中,主要的是局部顺序,即一类消息为满足顺 序性,必须 Producer 单线程顺序发送,且发送到同一个队列,这样 Consumer 就可以按照 Producer 收送 的顺序去消费消息。

RocketMQ 消息存储设计原理图

CommitLog

消息存储文件,所有消息主题的消息都存储在 CommitLog 文件中。 Commitlog 文件存储的逻辑视图如图所示

ConsumeQueue

消息消费队列,消息到达 CommitLog 文件后,将异步转发到消息 消费队列,供消息消费者消费。ConsumeQueue存储格式如下:

  • 单个 ConsumeQueue 文件中默认包含 30 万个条目,单个文件的长度为 30w × 20 字节, 单个 ConsumeQueue 文件可以看出是一个 ConsumeQueue 条目的数组,其下标为 ConsumeQueue 的逻辑偏移量,消息消费进度存储的偏移量 即逻辑偏移量。
  • ConsumeQueue 即为 Commitlog 文件的索引文件, 其构建机制是当消息到达 Commitlog 文件后, 由专门的线程 产生消息转发任务,从而构建消息消费队列文件与下文提到的索引文件。

IndexFile

消息索引文件,主要存储消息 Key 与 Offset 的对应关系。

消息消费队列是RocketMQ专门为消息订阅构建的索引文件,提高根据主题与消息队 列检索消息的速度 ,另外 RocketMQ 引入了 Hash 索引机制为消息建立索引, HashMap 的设 计包含两个基本点 : Hash 槽与 Hash 冲突的链表结构。 RocketMQ 索引文件布局如图所示

lndexFile 总共包含 lndexHeader、 Hash 槽、 Hash 条目

事务状态服务

存储每条消息的事务状态。

定时消息服务

每一个延迟级别对应一个消息消费队列,存储延迟队列的消息拉取进度。

RMQ文件存储模型层

RocketMQ业务处理器层

Broker端对消息进行读取和写入的业务逻辑入口,这一层主要包含了业务逻辑相关处理操作(根据解析RemotingCommand中的RequestCode来区分具体的业务操作类型,进而执行不同的业务处理流程),比如前置的检查和校验步骤、构造MessageExtBrokerInner对象、decode反序列化、构造Response返回对象等。

RocketMQ数据存储组件层

  • 该层主要是RocketMQ的存储核心类—DefaultMessageStore,其为RocketMQ消息数据文件的访问入口,通过该类的“putMessage()”和“getMessage()”方法完成对CommitLog消息存储的日志数据文件进行读写操作(具体的读写访问操作还是依赖下一层中CommitLog对象模型提供的方法);
  • 另外,在该组件初始化时候,还会启动很多存储相关的后台服务线程,包括AllocateMappedFileService(MappedFile预分配服务线程)、ReputMessageService(回放存储消息服务线程)、HAService(Broker主从同步高可用服务线程)、StoreStatsService(消息存储统计服务线程)、IndexService(索引文件服务线程)等。

RocketMQ存储逻辑对象层

  • 该层主要包含了RocketMQ数据文件存储直接相关的三个模型类IndexFile、ConsumerQueue和CommitLog。
  • IndexFile为索引数据文件提供访问服务,ConsumerQueue为逻辑消息队列提供访问服务,CommitLog则为消息存储的日志数据文件提供访问服务。
  • 这三个模型类也是构成了RocketMQ存储层的整体结构。

封装的文件内存映射层

  • RocketMQ主要采用JDK NIO中的MappedByteBuffer和FileChannel两种方式完成数据文件的读写。
  • 其中,采用MappedByteBuffer这种内存映射磁盘文件的方式完成对大文件的读写,在RocketMQ中将该类封装成MappedFile类。
  • 这里,每一种类的单个文件均由MappedFile类提供读写操作服务(其中,MappedFile类提供了顺序写/随机读、内存数据刷盘、内存清理等和文件相关的服务)。

磁盘存储层

主要指的是部署RocketMQ服务器所用的磁盘。这里,需要考虑不同磁盘类型(如SSD或者普通的HDD)特性以及磁盘的性能参数(如IOPS、吞吐量和访问时延等指标)对顺序写/随机读操作带来的影响。

RocketMQ中消息刷盘

在RocketMQ中消息刷盘主要可以分为同步刷盘和异步刷盘两种。

同步刷盘

  • 在返回写成功状态时,消息已经被写入磁盘。
  • 具体流程是,消息写入内存的PAGECACHE后,立刻通知刷盘线程刷盘,然后等待刷盘完成,刷盘线程执行完成后唤醒等待的线程,返回消息写成功的状态。
  • 一般只用于金融场景。

异步刷盘

在返回写成功状态时,消息可能只是被写入了内存的PAGECACHE,写操作的返回快,吞吐量大;当内存里的消息量积累到一定程度时,统一触发写磁盘操作,快速写入。

消息在系统中流转图

1.Producer 发送消息,消息从 socket 进入 java 堆。

2.Producer 发送消息,消息从 java 堆转入 PAGACACHE,物理内存。

3.Producer 发送消息,由异步线程刷盘,消息从 PAGECACHE 刷入磁盘。

4.Consumer 拉消息(正常消费),消息直接从 PAGECACHE(数据在物理内存)转入 socket,到达 consumer,
不经过 java 堆。这种消费场景最多,线上 96G 物理内存,按照 1K 消息算,可以在物理内存缓存 1 亿条消
息。

5.Consumer 拉消息(异常消费),消息直接从 PAGECACHE(数据在虚拟内存)转入 socket。

6.Consumer 拉消息(异常消费),由于 Socket 访问了虚拟内存,产生缺页中断,此时会产生磁盘 IO,从磁
盘 Load 消息到 PAGECACHE,然后直接从 socket 发出去。

7.同 5 一致。

8.同 6 一致。

参考与感谢

  • 十分钟入门RocketMQ
  • 分布式消息系列:详解RocketMQ的简介与演进、架构设计、关键特性及应用场景
  • RocketMQ消息存储
  • 《RocketMQ 原理简介》

个人公众号

欢迎大家关注,大家一起学习,一起讨论。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

谈谈 Golang, 以及我走的一些弯路

发表于 2019-09-12

在某乎上看到了这个问题, 还是挺有意思的. 撕哪个语言最好, 几乎是工程师当中最好的引战题目了. 今天我只想谈谈我是怎么看待 Go 的, 以及我走的一些弯路.

我是 2010 年在学校的时候了解到 Go 语言的. 当时的 Go 语言还是一塌糊涂, STW GC 是大家嘲讽 Go 语言的最佳标靶. 只要黑一句, Go 粉基本被噎得说不出话来.

我当时正想储备一门带并发编程模型的语言. 因为觉得未来 CPU 主频不再增长的情况下, 带并发编程模型的语言肯定是未来的主流. 是共享内存型语言强有力的竞争对手.

候选名单如下:

Erlang, Golang, Scala 搭配 Akka.

首先 Scala 被我排除掉了, 因为 Akka 的实现我觉得并不好, 而且当时 Scala 并没有本质上的重量级应用 ( Spark 虽然在 2010 年开源了, 但是真正流行起来要到2012年后了 ).
其次就是 Go 和 Erlang 了.

当时我对 Erlang 非常痴迷, 因为 Erlang 是唯一一个实现了软实时调度器的编程语言. 这意味着这东西可以直接用来写电话交换机 ( 当然 Erlang 诞生之初也是为了这个目标而存在的 ), 而如果要用Go来写电话交换机, 很可能会电话打着打着, 碰到了 STW GC, 然后你就听不到电话对面在讲什么了 ( 这也是为什么后来 WhatsApp 用了 Erlang, 50 个工程师写出了支撑 9 亿用户的系统 ).

而且, Erlang 实现的系统, 做到了 9 个 9 的可用性. 这是什么概念? 这意味着全年停机时间不超过 31.56 毫秒. 几乎就是不会停机了. 阿里云都只能说自己的可靠性 6 个 9, AWS 的可用性只有 99.95%. 意味着每年要停机 4.5 小时左右.

Erlang 另外一个设计的好的地方是, 它本身的 runtime 与其说是虚拟机, 不如说是操作系统, 是个运行时容器. 要知道 BEAM ( Erlang 虚拟机的名称 ) 在1992年就被实现了. 而 Docker 2013 年才出现. 这是多么超前的理念.

于是我义无反顾的学了 Erlang, 而 Golang 我只是看了看语法, 写了几个 demo, 观望了下.

时间来到了 2012年, 我去 360 搜索实习. 我被分配的一个任务就是写一个监控程序, 实时收集并展示 nginx 的连接数等状态, 做数据可视化供运维工程师调度机器参考. 机器的数量非常多, 并且要实时展示, 这算是个难点. 我立刻想到了用 Erlang 写, 这简直是为 Erlang 量身定做的场景.

我写完了, 并且顺利的实现了功能. 这时候收到的反馈是, 写得很棒, 但是公司没有用 Erlang 的工程师, 没办法维护, 所以在建议下我又用 Node.js 的 websocket 和 Redis 的订阅机制实现了个伪实时的监控系统… 这是我第一次, 也是最后一次用 Erlang 给企业写应用.

是的, Erlang 输在了这里. Erlang 的发明者 Joe Armstrong 有一篇文章 solving-the-wrong-problem 开头第一句就说了这么一句话:

We’re right and the rest of the world is wrong. We (that is Erlang folks) are solving the right problem, the rest of the world (non Erlang people) are solving the wrong problem.

现在来看, 这句话简直太中二了, 大意就是, 错的不是我, 是世界.

Erlang 为什么没有在 CPU 主频无法继续提升, 而核心数猛增的这么好的生态下火起来. 这个问题其实大佬早就说过了. Erlang 也不是唯一一个倒下去的例子. Richard P. Gabriel ( Common Lisp的发明者之一 ) 在这篇文章中 The Rise of Worse is Better 很好地阐述了为什么 Lisp 会没人用, 这个道理同样适用于 Erlang 身上.

简单来讲就是, Erlang 太好了, 为了完美的解决问题导致设计的很难学很难使用. 曲高和寡. 而那些简单好用的垃圾, 才能流行起来.

很合理, 这个道理再简单不过了. 这也是为什么大家不去看书, 而是喜欢去听喜马拉雅听, 喜欢去看知乎, 喜欢去看掘金, 喜欢这些被咀嚼一遍的东西, 觉得学到了知识. 因为对大家来说, 看书太难了, 太痛苦了.

但当时我年轻啊, 觉得那好办, 我这次选个最简单的, 于是我又跟风学了 Lua (openresty). 这个倒是很简单, 我是看左耳朵耗子老师那篇 LUA简明教程 入门的. 我的确是在厕所蹲坑的时间就学会了, 不到 1 小时 ( 有 JavaScript 经验的同学会更快一些 ). 然后写了很多个支持单机 10 万+级别并发的应用 ( 比如熊猫TV直播间的右侧礼物排行榜, 比如掘金的全局数据缓存等等 ).

但 Golang 也有了类似解决方案. fasthttp 作为 Go 的代表型高性能WEB框架, 轻松也可以支持 10 万级别的并发.

是时间不等人. Golang 在 10 年时间, 成为了怪物. 不但 STW GC 的问题解决了 ( 当然还是比不上软实时的 GC 那么平滑 ), 而且有了 kubernetes 这样的可怕的杀手锏. 也许有同学不理解 kubernetes 的可怕. 未来, 大家写的程序很可能既不是直接运行在物理机上, 也不是运行在 Xen, VMWare 等虚拟机上, 而是都会运行在 Docker 上, 由 kubernetes 进行调度. 甚至连选择的权利都没有 ( 不相信的同学可以问问在大厂的同学, 他们有自己部署目标机的 root 权限么 ).

看到这里, 是不是很熟悉? Erlang 早都实现了这一切, 甚至调度粒度更细, Erlang 实现了内置进程 ( 类似 goroutine ) 级别的调度. 而 Golang 的 goroutine 可不能跨 Docker 调度吧? ( 虽说接个网络通信模拟下也能实现类似的东西 ) . Erlang 提出了 Let it Crash 的概念. 现在看看 kubernetes 疯狂重启你的 docker pod, 是不是似曾相识?

openresty 也说明明是我先的 ( 白学现场 ), openresty 诞生之初, 能轻松支持 10 万级别并发访问的WEB框架屈指可数. 但现在 Golang 也可以了. 甚至更好 ( 少背了个 nginx 这么大个包袱 ).

读到这里, 你也许会问, 你这么作死, 每次几乎都选错了, 怎么还能混口饭吃? 那我只能说, 我编程的入门语言是 PHP, 这玩意比 Golang 还 New Jersey Style. 让我能找到工作的也是 PHP. 而我却在别的语言上持续作死. ……哈哈哈哈…… 这还真是悲哀.

有正在用 PHP 同学也许会问, 那么学 swoole 合适吗? 我的建议是. 都是成年人了, 不要做选择, 全都要. 无论是 swoole, fasthttp, netty, 都值得你看. 我学了 Erlang 也并没觉得自己吃亏. 这不, 还能在这里水一篇文章呢.

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

HTTP 状态码

发表于 2019-09-12

前言

想要真正理解 HTTP 状态码,而不是死记硬背,最好先过一遍下图。每个状态码都不是割裂开的,尝试带着图理解,思考收到 HTTP 请求后的整个处理流程,同时理解 HTTP 协议的 Header, 效率更高。

HTTP decision diagram

图片比较模糊,被压缩了,请转 github 上的 for-GET/http-decision-diagram 项目看原图。

100 Continue

表示目前为止一切正常, 客户端应该继续请求, 如果已完成请求则忽略.

为了让服务器检查请求的首部, 客户端必须在发送请求实体前, 在初始化请求中发送 Expect: 100-continue 首部并接收 100 Continue 响应状态码.

Expect

包含一个期望条件,表示服务器只有在满足此期望条件的情况下才能妥善地处理请求。

规范中只规定了一个期望条件,即 Expect: 100-continue, 对此服务器可以做出如下回应:

  • 100 如果消息头中的期望条件可以得到满足,使得请求可以顺利进行的话,
  • 417 (Expectation Failed) 如果服务器不能满足期望条件的话;也可以是其他任意表示客户端错误的状态码(4xx)。

101 Switching Protocols

表示服务器应客户端升级协议的请求(Upgrade请求头)正在进行协议切换。
例:

1
2
3
复制代码HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade

200 OK

表明请求已经成功. 默认情况下状态码为200的响应可以被缓存。

201 Created

表示请求已经被成功处理,并且创建了新的资源。新的资源在应答返回之前已经被创建。

202 Accepted

表示服务器端已经收到请求消息,但是尚未进行处理。但是对于请求的处理确实无保证的,即稍后无法通过 HTTP 协议给客户端发送一个异步请求来告知其请求的处理结果。

204 No Content

表示目前请求成功,但客户端不需要更新其现有页面。

使用惯例是,在 PUT 请求中进行资源更新,但是不需要改变当前展示给用户的页面,那么返回 204 No Content。如果新创建了资源,那么返回 201 Created 。如果页面需要更新以反映更新后的资源,那么需要返回 200 。

301 Moved Permanently

永久重定向。说明请求的资源已经被移动到了由 Location 头部指定的 url 上,是固定的不会再改变。搜索引擎会根据该响应修正。

尽管标准要求浏览器在收到该响应并进行重定向时不应该修改 http method 和 body,但是有一些浏览器可能会有问题。所以最好是在应对 GET 或 HEAD 方法时使用 301,其他情况使用 308 来替代 301

302 Found

临时重定向。重定向状态码表明请求的资源被暂时的移动到了由 Location 头部指定的 URL 上。浏览器会重定向到这个URL,但是搜索引擎不会对该资源的链接进行更新。

即使规范要求浏览器在重定向时保证请求方法和请求主体不变,但并不是所有的用户代理都会遵循这一点,你依然可以看到有缺陷的软件的存在。所以推荐仅在响应 GET 或 HEAD 方法时采用 302 状态码,而在其他时候使用 307 来替代,因为在这些场景下方法变换是明确禁止的。

在确实需要将重定向请求的方法转换为 GET 的场景下,可以使用 303。例如在使用 PUT 方法进行文件上传操作时,需要返回确认信息(例如“你已经成功上传了xyz”)而不是上传的资源本身,就可以使用这个状态码。

303 See Other

通常作为 PUT 或 POST 操作的返回结果,它表示重定向链接指向的不是新上传的资源,而是另外一个页面,比如消息确认页面或上传进度页面。而请求重定向页面的方法要总是使用 GET。

304 Not Modified

说明无需再次传输请求的内容,也就是说可以使用缓存的内容。这通常是在一些安全的方法(safe),例如GET 或HEAD, 或在请求中附带了头部信息: If-None-Match 或If-Modified-Since。

如果返回 200,响应会带有头部 Cache-Control, Content-Location, Date, ETag, Expires,和 Vary.

Last-Modified 和 If-Modified-Since

  1. 客户端请求一个文件(A)。 服务器返回文件A,并返回 Last-Modified。
  2. 客户端收到响应后,缓存文件A 和 Last-Modified。
  3. 客户端再次请求文件A 时,发现该文件有 Last-Modified ,那么 header 离包含 If-Modified-Since,这个时间就是缓存文件的 Last-Modified。
  4. 服务端收到请求,只需要判断这个时间和当前请求的文件的修改时间就可以确定是返回 304 还是 200

If-Modified-Since 的主要缺点是只能精确到秒的级别,一旦在一秒内出现多次修改,是无法判断出已修改的状态。所以一般用在对时间不太敏感的静态资源。

ETag 和 If-None-Match

  1. 客户端请求一个文件(A)。 服务器返回文件A,并在给A加上一个 ETag。
  2. 客户端收到响应后,并将文件连同 ETag 一起缓存。
  3. 客户再次请求文件A,会发送 If-None-Match,内容是缓存该文件A的 Etag 值
  4. 服务器检查该 ETag,和计算出来的 Etag 匹配,来判断文件是否未被修改。如果未修改就直接返回 304 和一个空的响应体。否则返回 200 和 文件。

当与 If-Modified-Since 一同使用的时候,If-None-Match 优先级更高(假如服务器支持的话)

307 Temporary Redirect

临时重定向。类似 302,区别在于能够确保请求方法和消息主体不会发生改变。

308 Permanent Redirect

永久重定向。类似 301,区别在于能够确保请求方法和消息主体不会发生改变。

400 Bad Request

表示由于语法无效,服务器无法理解该请求。客户端不应该在未经修改的情况下重复此请求。

401 Unauthorized

说明由于缺乏目标资源要求的身份验证凭证,发送的请求未得到满足。

这个状态码会与 WWW-Authenticate 首部一起发送,其中包含有如何进行验证的信息。

WWW-Authenticate 和 Authorization

WWW-Authenticate 定义了应该用来访问资源的认证方法。语法:

1
复制代码WWW-Authenticate: <type> realm=<realm>
  • type : Authentication type. A common type is “Basic”.
  • realm= : A description of the protected area. If no realm is specified, clients often display a formatted hostname instead.

例:

1
2
3
复制代码WWW-Authenticate: Basic

WWW-Authenticate: Basic realm="Access to the staging site", charset="UTF-8"

Authorization 请求消息头含有服务器用于验证用户代理身份的凭证,通常会在服务器返回401 Unauthorized 状态码以及WWW-Authenticate 消息头之后在后续请求中发送此消息头。语法:

1
复制代码Authorization: <type> <credentials>
  • type : Authentication type. A common type is “Basic”.
  • credentials :
    1. The username and the password are combined with a colon (aladdin:opensesame).
    2. The resulting string is base64 encoded (YWxhZGRpbjpvcGVuc2VzYW1l).

403 Forbidden

指的是服务器端有能力处理该请求,但是拒绝授权访问。进入该状态后,不能再继续进行验证。该访问是永久禁止的,并且与应用逻辑密切相关(例如不正确的密码)

404 Not Found

说明服务器端无法找到所请求的资源。返回该响应的链接通常称为坏链(broken link)或死链(dead link),它们会导向链接出错处理

404 不能说明请求的资源是临时还是永久丢失。如果服务器知道该资源是永久丢失,那么应该返回 410 (Gone) 而不是 404 。

405 Method Not Allowed

表明服务器禁止了使用当前 HTTP 方法的请求。需要注意的是,GET 与 HEAD 两个方法不得被禁止,当然也不得返回状态码 405。

406 Not Acceptable

表示服务器端不支持 Accept、Accept-Charset、Accept-Encoding、 Accept-Language header 所要求的。

Accept 和 Content-Type

Accept 用来告知客户端可以处理的内容类型,这种内容类型用 MIME 类型来表示。服务器从中选择一项进行应用,并使用 Content-Type 应答头通知客户端。

1
2
3
4
5
6
复制代码Accept: <MIME_type>/<MIME_subtype>
Accept: <MIME_type>/*
Accept: */*

// Multiple types, weighted with the quality value syntax:
Accept: text/html, application/xhtml+xml, application/xml;q=0.9, */*;q=0.8
  • <MIME_type>/<MIME_subtype> : A single, precise MIME type, like text/html.
  • <MIME_type>/* : A MIME type, but without any subtype. image/* will match image/png, image/svg, image/gif and any other image types.
  • / : Any MIME type
  • ;q= (q-factor weighting)

Accept-Encoding 和 Content-Encoding

Accept-Encoding 会将客户端能够理解的内容编码方式——通常是某种压缩算法——进行通知。服务端会从中选择一个使用,并在响应报文首部 Content-Encoding 中通知客户端。

1
2
3
复制代码Accept-Encoding: deflate, gzip;q=1.0, *;q=0.5

Content-Encoding: gzip

409 Conflict

表示请求与服务器端目标资源的当前状态相冲突。

冲突最有可能发生在对 PUT 请求的响应中。例如,当上传文件的版本比服务器上已存在的要旧,从而导致版本冲突的时候,那么就有可能收到状态码为 409 的响应。

410 Gone

说明请求的内容在服务器上不存在了,同时是永久性的丢失。如果不清楚是否为永久或临时的丢失,应该使用404。

413 Payload Too Large

表示请求主体的大小超过了服务器愿意或有能力处理的限度,服务器可能会(may)关闭连接以防止客户端继续发送该请求。

如果“超出限度”是暂时性的,服务器应该返回 Retry-After 首部字段,说明这是暂时性的,以及客户端可以在什么时间后重试。

412 Precondition Failed

表示客户端错误,意味着对于目标资源的访问请求被拒绝。这通常发生在采用除 GET 和 HEAD 之外的方法进行条件请求时,由首部字段 If-Unmodified-Since 或 If-None-Match 规定的先决条件不成立的情况下。

414 URI Too Long

表示客户端所请求的 URI 超过了服务器允许的范围。

431 Request Header Fields Too Large

表示由于请求中的首部字段的值过大,服务器拒绝接受客户端的请求。客户端可以在缩减首部字段的体积后再次发送请求。

该响应码可以用于首部总体体积过大的情况,也可以用于单个首部体积过大的情况。

500 Internal Server Error

表示所请求的服务器遇到意外的情况并阻止其执行请求。

举例:代码语法错误;php代码运行内存超了内存限制 memory_limit;nginx config 配置错误;

501 Not Implemented

表示request header 里的 method 或 Content-* 时不被服务器支持,无法被处理。另,服务器必须支持的方法(即不会返回这个状态码的方法)只有 GET 和 HEAD。501 响应默认是可缓存的。

502 Bad Gateway

表示作为网关或代理角色的服务器,从上游服务器(如tomcat、php-fpm)中接收到的响应是无效的。

举例: php-fpm 挂掉了

503 Service Unavailable

表示服务器尚未处于可以接受请求的状态。通常造成这种情况的原因是由于服务器停机维护或者已超载。该种响应应该用于临时状况下,与之同时,在可行的情况下,应该在 Retry-After 首部字段中包含服务恢复的预期时间。

举例:服务器停机维护时,主动用503响应请求;nginx 设置限速之类的,超过限速,会返回503。

Retry-After

表示用户代理需要等待多长时间之后才能继续发送请求。这个首部主要应用于以下几种场景:

  • When sent with a 503 response, this indicates how long the service is expected to be unavailable.
  • When sent with a 429 response, this indicates how long to wait before making a new request.
  • When sent with a redirect response, such as 301, this indicates the minimum time that the user agent is asked to wait before issuing the redirected request.

语法:

1
2
复制代码Retry-After: <http-date>
Retry-After: <delay-seconds>
  • : A date after which to retry.
  • : A non-negative decimal integer indicating the seconds to delay after the response is received.

例:

1
2
复制代码Retry-After: Wed, 21 Oct 2015 07:28:00 GMT
Retry-After: 120

504 Gateway Timeout

表示网关或者代理的服务器无法在规定的时间内获得想要的响应。

举例:代码执行时间超时,或死循环了。

问题

500 和 503 分别表示什么,以及哪些情况下会用到。

401 和 403 的区别

  1. 401 Unauthorized 用于丢失或错误的身份验证,响应头包含 www-Authenticate 来描述如何验证,通常由 Web 服务器 返回,而不是应用程序,具有暂时性,服务器会要求重试
  2. 403 Forbidden 是指已经验过身份验证,但是在某项请求资源操作上没有权限,具有永久性,与应用程序逻辑相关。

参考自

  • Etag 和 If-None-Match
  • HTTP response status codes
  • HTTP Headers
  • 403 Forbidden vs 401 Unauthorized HTTP responses

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

Spring-boot搭建邮件服务

发表于 2019-09-12

前言:

发送邮件,肯定是每个公司都会有的基本业务。很多公司都会选择把发送邮件作为一个基础服务,对外提供接口。直接调用就可发邮件了。但是我们都知道发送邮件耗时都比较长。那么今天就介绍下使用Spring boot+eventbus来打造一个简单邮件服务

规划接口列表

发送邮件的类型准备的有三种

  1. 发送普通邮件
  2. 发送html邮件
  3. 发送图文邮件

还有一个细节,如果我们同步的取发送邮件会有两个问题。

  1. 接口响应时间比较长
  2. 遇到并发的情况,容易导致服务器压力过大或者邮箱服务封ip

所以我们准备使用队列来执行发送邮件的操作。可以解决这个问题。队列我选用的是Google的eventbus。是一款很轻量的队列。直接走的内存

准备工作

首先要在pom.xml中引入 需要使用的包

1
2
3
4
5
6
7
8
9
10
11
12
13
复制代码    <dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-mail</artifactId>
</dependency>
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>23.0</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
  • spring-boot-starter-mail :spring-boot提供的发邮件的maven库
  • guava:google提供的开源库。里面包含来很多工具
  • lombok:可以帮你省去编写实体类的工具

引入之后,我们还需要配置发送邮件所需要的必要配置
在application.properties中配置邮箱

1
2
3
4
5
6
7
复制代码spring.mail.host=smtp.mail.me.com //邮箱发送服务器
spring.mail.port=587//服务器端口
spring.mail.username=xxx6666@icloud.com//发件人邮箱
spring.mail.password=password//客户端专用密码
//如果和我一样使用的icloud邮箱 还需要下列两个配置,别的有的邮箱不需要
spring.mail.properties.mail.smtp.starttls.enable=true
spring.mail.properties.mail.smtp.starttls.required=true

做到这里其实就已经完成了,发邮件所需要的配置了。但是我们是要用队列来发送,所以还需要配置下队列

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
复制代码@Configuration
public class AsyncEventBusConfig {
//实例化bean,采用单例形式注入容器
@Bean
@Scope("singleton")
public AsyncEventBus asyncEventBus(){
//创建线程池对象
final ThreadPoolExecutor executor=executor();
return new AsyncEventBus(executor);
}
//创建线程池方法
private ThreadPoolExecutor executor(){
return new
ThreadPoolExecutor(2,
2,0L,
TimeUnit.MICROSECONDS,
new LinkedBlockingQueue<>());
}
}

封装EmailService

准备好了之后,就可以直接来封装发送邮件的业务了。之前有提到我们需要三个接口,同样的,我们也需要三个service方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
复制代码@Service
public class EmailService {

@Autowired
private JavaMailSender javaMailSender;

/**
* 发件人。这里发件人一般是同使用的发件邮箱一致
*/
@Value("${spring.mail.username}")
private String from;


/**
* 发送文本邮件
* @param to 收件人邮箱地址
* @param subject 主题
* @param content 内容
*/
public void sendTextMail(String to,
String subject,
String content) {
SimpleMailMessage simpleMailMessage = new SimpleMailMessage();
simpleMailMessage.setTo(to);
simpleMailMessage.setSubject(subject);
simpleMailMessage.setText(content);
simpleMailMessage.setFrom(from);
javaMailSender.send(simpleMailMessage);
}


/**
* 发送html内容的邮件
* @param to 收件人
* @param htmlContent html内容
* @param subject 主题
* @throws MessagingException
*/
public void sendHtmlMail(String to,
String htmlContent,
String subject) throws MessagingException {
MimeMessage message = javaMailSender.createMimeMessage();
MimeMessageHelper messageHelper = new MimeMessageHelper(message, true);
messageHelper.setTo(to);
messageHelper.setSubject(subject);
messageHelper.setFrom(from);
messageHelper.setText(htmlContent, true);
javaMailSender.send(message);
}

/**
* 发送图文邮件
* @param to 收件人
* @param imgContent 图文内容
* @param subject 主题
* @param rscId 资源id
* @param imgPath 资源路径
* @throws MessagingException
*/
public void sendImgMail(String to,
String imgContent,
String subject,
String rscId,
String imgPath) throws MessagingException {
MimeMessage message = javaMailSender.createMimeMessage();
MimeMessageHelper messageHelper = new MimeMessageHelper(message, true);
messageHelper.setTo(to);
messageHelper.setSubject(subject);
messageHelper.setFrom(from);
messageHelper.setText(imgContent, true);
messageHelper.addInline(rscId, new File(imgPath));
javaMailSender.send(message);
}
}

队列监听

既然封装好了方法,那么就需要调用。调用的方式,其实就是将接口传来的数据传到队列里。队列的消费者接收到了消息就将消息拿来调用发送邮件的方法
我们首先创建一个消费类,用来接受消息,处理消息。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
复制代码@Service
public class EventBusListener {

/**
* 引入bean
*/
@Autowired
private AsyncEventBus asyncEventBus;

@Autowired
private EmailService emailService;

/**
* 注册服务类
*/
@PostConstruct
public void init(){
asyncEventBus.register(this);
}

/**
* 线程安全,消费 文本消息
* @param textEmailDTO
*/
@AllowConcurrentEvents
@Subscribe
public void sendTextMail(TextEmailDTO textEmailDTO){
emailService.sendTextMail(
textEmailDTO.getTo(),
textEmailDTO.getSubject(),
textEmailDTO.getContent()
);
}

/**
* 线程安全 消费 html消息
* @param htmlEmailDTO
*/
@AllowConcurrentEvents
@Subscribe
public void sendHtmlMail(HtmlEmailDTO htmlEmailDTO){
try {
emailService.sendHtmlMail(
htmlEmailDTO.getTo(),
htmlEmailDTO.getHtmlContent(),
htmlEmailDTO.getSubject()
);
} catch (MessagingException e) {
// nothing to do
}
}

/**
* 线程安全 消费 图文消息
* @param imgEmailDTO
*/
@AllowConcurrentEvents
@Subscribe
public void sendImgMail(ImgEmailDTO imgEmailDTO){
try {
emailService.sendImgMail(
imgEmailDTO.getTo(),
imgEmailDTO.getImgContent(),
imgEmailDTO.getSubject(),
imgEmailDTO.getRscId(),
imgEmailDTO.getImgPath()
);
} catch (MessagingException e) {
// nothing to do
}
}
}

其实eventbus抛消息都是使用的post方法来抛消息。走到不同的方法里面是利用了类的多态,抛入不同的实体类就可以进行区分了。走进了不同的方法,就调用相应Service方法。

控制器与测试

控制器部分,没什么好说的,我就贴出图文的代码。其余代码可以在我的github上面看

先看眼实体类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
复制代码@Data
public class ImgEmailDTO implements Serializable {
public ImgEmailDTO() {
}

/**
* 图片路径
*/
private String imgPath;

/**
* 资源id
*/
private String rscId;

/**
* 主题
*/
private String subject;

/**
* 图片正文(同样可以使用html)
*/
private String imgContent;

/**
* 收件人
*/
private String to;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
复制代码    /**
* 发送图文邮件
* @param request
* @return
*/
@RequestMapping(value = "/sendImgMail", method = RequestMethod.POST)
public Result<Integer> sendImgMail(@RequestBody Request<ImgEmailDTO> request) {
Result<Integer> result = Result.create();
ImgEmailDTO imgEmailDTO=request.getData();
StringBuilder sb=new StringBuilder();
sb.append(imgEmailDTO.getImgContent());
//cid:资源id。在spring中会自动绑定
sb.append("<img src=\'cid:").append(imgEmailDTO.getRscId()).append("\'></img>");
imgEmailDTO.setImgContent(sb.toString());
asyncEventBus.post(imgEmailDTO);
return result.success(1);
}

图文要稍微特殊一点,需要拼接下正文内容。然后将实体类中的content替换。最后将实体类抛入队列。直接返回接口请求。队列那边就会排着队搞定所有的邮件
下面来做个测试

请求接口

请求很迅速的返回了结果
然后去邮箱中查看结果
邮件结果

好了今天对邮件服务的介绍就写到这里。知识点并不深奥,主要介绍一个思路。如有不对的地方,请大神指出。谢谢

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

如何设计一个高并发系统?

发表于 2019-09-11

面试题

如何设计一个高并发系统?

面试官心理分析

说实话,如果面试官问你这个题目,那么你必须要使出全身吃奶劲了。为啥?因为你没看到现在很多公司招聘的 JD 里都是说啥,有高并发就经验者优先。

如果你确实有真才实学,在互联网公司里干过高并发系统,那你确实拿 offer 基本如探囊取物,没啥问题。面试官也绝对不会这样来问你,否则他就是蠢。

假设你在某知名电商公司干过高并发系统,用户上亿,一天流量几十亿,高峰期并发量上万,甚至是十万。那么人家一定会仔细盘问你的系统架构,你们系统啥架构?怎么部署的?部署了多少台机器?缓存咋用的?MQ 咋用的?数据库咋用的?就是深挖你到底是如何扛住高并发的。

因为真正干过高并发的人一定知道,脱离了业务的系统架构都是在纸上谈兵,真正在复杂业务场景而且还高并发的时候,那系统架构一定不是那么简单的,用个 redis,用 mq 就能搞定?当然不是,真实的系统架构搭配上业务之后,会比这种简单的所谓“高并发架构”要复杂很多倍。

如果有面试官问你个问题说,如何设计一个高并发系统?那么不好意思,一定是因为你实际上没干过高并发系统。面试官看你简历就没啥出彩的,感觉就不咋地,所以就会问问你,如何设计一个高并发系统?其实说白了本质就是看看你有没有自己研究过,有没有一定的知识积累。

最好的当然是招聘个真正干过高并发的哥儿们咯,但是这种哥儿们人数稀缺,不好招。所以可能次一点的就是招一个自己研究过的哥儿们,总比招一个啥也不会的哥儿们好吧!

所以这个时候你必须得做一把个人秀了,秀出你所有关于高并发的知识!

面试题剖析

其实所谓的高并发,如果你要理解这个问题呢,其实就得从高并发的根源出发,为啥会有高并发?为啥高并发就很牛逼?

我说的浅显一点,很简单,就是因为刚开始系统都是连接数据库的,但是要知道数据库支撑到每秒并发两三千的时候,基本就快完了。所以才有说,很多公司,刚开始干的时候,技术比较 low,结果业务发展太快,有的时候系统扛不住压力就挂了。

当然会挂了,凭什么不挂?你数据库如果瞬间承载每秒 5000/8000,甚至上万的并发,一定会宕机,因为比如 mysql 就压根儿扛不住这么高的并发量。

所以为啥高并发牛逼?就是因为现在用互联网的人越来越多,很多 app、网站、系统承载的都是高并发请求,可能高峰期每秒并发量几千,很正常的。如果是什么双十一之类的,每秒并发几万几十万都有可能。

那么如此之高的并发量,加上原本就如此之复杂的业务,咋玩儿?真正厉害的,一定是在复杂业务系统里玩儿过高并发架构的人,但是你没有,那么我给你说一下你该怎么回答这个问题:

可以分为以下 6 点:

  • 系统拆分
  • 缓存
  • MQ
  • 分库分表
  • 读写分离
  • ElasticSearch
    file

系统拆分

将一个系统拆分为多个子系统,用 dubbo 来搞。然后每个系统连一个数据库,这样本来就一个库,现在多个数据库,不也可以扛高并发么。

缓存

缓存,必须得用缓存。大部分的高并发场景,都是读多写少,那你完全可以在数据库和缓存里都写一份,然后读的时候大量走缓存不就得了。毕竟人家 redis 轻轻松松单机几万的并发。所以你可以考虑考虑你的项目里,那些承载主要请求的读场景,怎么用缓存来抗高并发。

MQ

MQ,必须得用 MQ。可能你还是会出现高并发写的场景,比如说一个业务操作里要频繁搞数据库几十次,增删改增删改,疯了。那高并发绝对搞挂你的系统,你要是用 redis 来承载写那肯定不行,人家是缓存,数据随时就被 LRU 了,数据格式还无比简单,没有事务支持。所以该用 mysql 还得用 mysql 啊。那你咋办?用 MQ 吧,大量的写请求灌入 MQ 里,排队慢慢玩儿,后边系统消费后慢慢写,控制在 mysql 承载范围之内。所以你得考虑考虑你的项目里,那些承载复杂写业务逻辑的场景里,如何用 MQ 来异步写,提升并发性。MQ 单机抗几万并发也是 ok 的,这个之前还特意说过。

分库分表

分库分表,可能到了最后数据库层面还是免不了抗高并发的要求,好吧,那么就将一个数据库拆分为多个库,多个库来扛更高的并发;然后将一个表拆分为多个表,每个表的数据量保持少一点,提高 sql 跑的性能。

读写分离

读写分离,这个就是说大部分时候数据库可能也是读多写少,没必要所有请求都集中在一个库上吧,可以搞个主从架构,主库写入,从库读取,搞一个读写分离。读流量太多的时候,还可以加更多的从库。

ElasticSearch

Elasticsearch,简称 es。es 是分布式的,可以随便扩容,分布式天然就可以支撑高并发,因为动不动就可以扩容加机器来扛更高的并发。那么一些比较简单的查询、统计类的操作,可以考虑用 es 来承载,还有一些全文搜索类的操作,也可以考虑用 es 来承载。

上面的 6 点,基本就是高并发系统肯定要干的一些事儿,大家可以仔细结合之前讲过的知识考虑一下,到时候你可以系统的把这块阐述一下,然后每个部分要注意哪些问题,之前都讲过了,你都可以阐述阐述,表明你对这块是有点积累的。

说句实话,毕竟你真正厉害的一点,不是在于弄明白一些技术,或者大概知道一个高并发系统应该长什么样?其实实际上在真正的复杂的业务系统里,做高并发要远远比上面提到的点要复杂几十倍到上百倍。你需要考虑:哪些需要分库分表,哪些不需要分库分表,单库单表跟分库分表如何 join,哪些数据要放到缓存里去,放哪些数据才可以扛住高并发的请求,你需要完成对一个复杂业务系统的分析之后,然后逐步逐步的加入高并发的系统架构的改造,这个过程是无比复杂的,一旦做过一次,并且做好了,你在这个市场上就会非常的吃香。

其实大部分公司,真正看重的,不是说你掌握高并发相关的一些基本的架构知识,架构中的一些技术,RocketMQ、Kafka、Redis、Elasticsearch,高并发这一块,你了解了,也只能是次一等的人才。对一个有几十万行代码的复杂的分布式系统,一步一步架构、设计以及实践过高并发架构的人,这个经验是难能可贵的。

本文在米兜公众号链接:https://mp.weixin.qq.com/s/9p-XPL\_FRqN5nhbiwmOXSw

欢迎关注米兜Java,一个注在共享、交流的Java学习平台。

file

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

3分钟搞清楚进程与线程的区别

发表于 2019-09-11

前言

进程(process)和线程(thread)是操作系统的基本概念,但是它们比较抽象,不容易掌握。
最近,我读到一篇材料,发现有一个很好的类比,可以把它们解释地清晰易懂。1.
–

计算机的核心是CPU,它承担了所有的计算任务。它就像一座工厂,时刻在运行。
2.
–

假定工厂的电力有限,一次只能供给一个车间使用。也就是说,一个车间开工的时候,其他车间都必须停工。背后的含义就是,单个CPU一次只能运行一个任务。
3.
–

进程就好比工厂的车间,它代表CPU所能处理的单个任务。任一时刻,CPU总是运行一个进程,其他进程处于非运行状态。
4.
–

一个车间里,可以有很多工人。他们协同完成一个任务。
5.
–

线程就好比车间里的工人。一个进程可以包括多个线程。
6.
–

车间的空间是工人们共享的,比如许多房间是每个工人都可以进出的。这象征一个进程的内存空间是共享的,每个线程都可以使用这些共享内存。
7.
–

可是,每间房间的大小不同,有些房间最多只能容纳一个人,比如厕所。里面有人的时候,其他人就不能进去了。这代表一个线程使用某些共享内存时,其他线程必须等它结束,才能使用这一块内存。
8.
–

一个防止他人进入的简单方法,就是门口加一把锁。先到的人锁上门,后到的人看到上锁,就在门口排队,等锁打开再进去。这就叫”互斥锁”(Mutual exclusion,缩写 Mutex),防止多个线程同时读写某一块内存区域。
9.
–

还有些房间,可以同时容纳n个人,比如厨房。也就是说,如果人数大于n,多出来的人只能在外面等着。这好比某些内存区域,只能供给固定数目的线程使用。
10.


这时的解决方法,就是在门口挂n把钥匙。进去的人就取一把钥匙,出来时再把钥匙挂回原处。后到的人发现钥匙架空了,就知道必须在门口排队等着了。这种做法叫做”信号量”(Semaphore),用来保证多个线程不会互相冲突。不难看出,mutex是semaphore的一种特殊情况(n=1时)。也就是说,完全可以用后者替代前者。但是,因为mutex较为简单,且效率高,所以在必须保证资源独占的情况下,还是采用这种设计。
11.


操作系统的设计,因此可以归结为三点:(1)以多进程形式,允许多个任务同时运行;(2)以多线程形式,允许单个任务分成不同的部分运行;(3)提供协调机制,一方面防止进程之间和线程之间产生冲突,另一方面允许进程之间和线程之间共享资源。
最后
–

欢迎大家关注我的公众号【程序员追风】,文章都会在里面更新,整理的资料也会放在里面。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

学习 lodash 源码整体架构,打造属于自己的函数式编程类

发表于 2019-09-10

前言

你好,我是若川。这是学习源码整体架构系列第三篇。整体架构这词语好像有点大,姑且就算是源码整体结构吧,主要就是学习是代码整体结构,不深究其他不是主线的具体函数的实现。本篇文章学习的是打包整合后的代码,不是实际仓库中的拆分的代码。

学习源码整体架构系列文章如下:

1.学习 jQuery 源码整体架构,打造属于自己的 js 类库

2.学习 underscore 源码整体架构,打造属于自己的函数式编程类库

3.学习 lodash 源码整体架构,打造属于自己的函数式编程类库

4.学习 sentry 源码整体架构,打造属于自己的前端异常监控SDK

5.学习 vuex 源码整体架构,打造属于自己的状态管理库

6.学习 axios 源码整体架构,打造属于自己的请求库

7.学习 koa 源码的整体架构,浅析koa洋葱模型原理和co原理

8.学习 redux 源码整体架构,深入理解 redux 及其中间件原理

感兴趣的读者可以点击阅读。

underscore源码分析的文章比较多,而lodash源码分析的文章比较少。原因之一可能是由于lodash源码行数太多。注释加起来一万多行。

分析lodash整体代码结构的文章比较少,笔者利用谷歌、必应、github等搜索都没有找到,可能是找的方式不对。于是打算自己写一篇。平常开发大多数人都会使用lodash,而且都或多或少知道,lodash比underscore性能好,性能好的主要原因是使用了惰性求值这一特性。

本文章学习的lodash的版本是:v4.17.15。unpkg.com地址 https://unpkg.com/lodash@4.17.15/lodash.js

文章篇幅可能比较长,可以先收藏再看,所以笔者使用了展开收缩的形式。

导读:

文章主要学习了runInContext() 导出_ lodash函数使用baseCreate方法原型继承LodashWrapper和LazyWrapper,mixin挂载方法到lodash.prototype、后文用结合例子解释lodash.prototype.value(wrapperValue)和Lazy.prototype.value(lazyValue)惰性求值的源码具体实现。

匿名函数执行

1
2
3
复制代码;(function() {

}.call(this));

暴露 lodash

1
复制代码var _ = runInContext();

runInContext 函数

这里的简版源码,只关注函数入口和返回值。

1
2
3
4
5
6
7
8
9
10
复制代码var runInContext = (function runInContext(context) {
// 浏览器中处理context为window
// ...
function lodash(value) {}{
// ...
return new LodashWrapper(value);
}
// ...
return lodash;
});

可以看到申明了一个runInContext函数。里面有一个lodash函数,最后处理返回这个lodash函数。

再看lodash函数中的返回值 new LodashWrapper(value)。

LodashWrapper 函数

1
2
3
4
5
6
7
复制代码function LodashWrapper(value, chainAll) {
this.__wrapped__ = value;
this.__actions__ = [];
this.__chain__ = !!chainAll;
this.__index__ = 0;
this.__values__ = undefined;
}

设置了这些属性:

__wrapped__:存放参数value。

__actions__:存放待执行的函数体func, 函数参数 args,函数执行的this 指向 thisArg。

__chain__、undefined两次取反转成布尔值false,不支持链式调用。和underscore一样,默认是不支持链式调用的。

__index__:索引值 默认 0。

__values__:主要clone时使用。

接着往下搜索源码,LodashWrapper,
会发现这两行代码。

1
2
复制代码LodashWrapper.prototype = baseCreate(baseLodash.prototype);
LodashWrapper.prototype.constructor = LodashWrapper;

接着往上找baseCreate、baseLodash这两个函数。

baseCreate 原型继承

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
复制代码//  立即执行匿名函数
// 返回一个函数,用于设置原型 可以理解为是 __proto__
var baseCreate = (function() {
// 这句放在函数外,是为了不用每次调用baseCreate都重复申明 object
// underscore 源码中,把这句放在开头就申明了一个空函数 `Ctor`
function object() {}
return function(proto) {
// 如果传入的参数不是object也不是function 是null
// 则返回空对象。
if (!isObject(proto)) {
return {};
}
// 如果支持Object.create方法,则返回 Object.create
if (objectCreate) {
// Object.create
return objectCreate(proto);
}
// 如果不支持Object.create 用 ployfill new
object.prototype = proto;
var result = new object;
// 还原 prototype
object.prototype = undefined;
return result;
};
}());

// 空函数
function baseLodash() {
// No operation performed.
}

// Ensure wrappers are instances of `baseLodash`.
lodash.prototype = baseLodash.prototype;
// 为什么会有这一句?因为上一句把lodash.prototype.construtor 设置为Object了。这一句修正constructor
lodash.prototype.constructor = lodash;

LodashWrapper.prototype = baseCreate(baseLodash.prototype);
LodashWrapper.prototype.constructor = LodashWrapper;

笔者画了一张图,表示这个关系。

lodash 原型关系图

lodash 原型关系图

衍生的 isObject 函数

判断typeof value不等于null,并且是object或者function。

1
2
3
4
复制代码function isObject(value) {
var type = typeof value;
return value != null && (type == 'object' || type == 'function');
}

Object.create() 用法举例

面试官问:能否模拟实现JS的new操作符 之前这篇文章写过的一段,所以这里收缩起来了。

点击 查看 Object.create() 用法举例
笔者之前整理的一篇文章中也有讲过,可以翻看JavaScript 对象所有API解析

MDN Object.create()

Object.create(proto, [propertiesObject])
方法创建一个新对象,使用现有的对象来提供新创建的对象的__proto__。
它接收两个参数,不过第二个可选参数是属性描述符(不常用,默认是undefined)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
复制代码var anotherObject = {
name: '若川'
};
var myObject = Object.create(anotherObject, {
age: {
value:18,
},
});
// 获得它的原型
Object.getPrototypeOf(anotherObject) === Object.prototype; // true 说明anotherObject的原型是Object.prototype
Object.getPrototypeOf(myObject); // {name: "若川"} // 说明myObject的原型是{name: "若川"}
myObject.hasOwnProperty('name'); // false; 说明name是原型上的。
myObject.hasOwnProperty('age'); // true 说明age是自身的
myObject.name; // '若川'
myObject.age; // 18;

对于不支持ES5的浏览器,MDN上提供了ployfill方案。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
复制代码if (typeof Object.create !== "function") {
Object.create = function (proto, propertiesObject) {
if (typeof proto !== 'object' && typeof proto !== 'function') {
throw new TypeError('Object prototype may only be an Object: ' + proto);
} else if (proto === null) {
throw new Error("This browser's implementation of Object.create is a shim and doesn't support 'null' as the first argument.");
}

if (typeof propertiesObject != 'undefined') throw new Error("This browser's implementation of Object.create is a shim and doesn't support a second argument.");

function F() {}
F.prototype = proto;
return new F();
};
}

lodash上有很多方法和属性,但在lodash.prototype也有很多与lodash上相同的方法。肯定不是在lodash.prototype上重新写一遍。而是通过mixin挂载的。

mixin

mixin 具体用法

1
复制代码_.mixin([object=lodash], source, [options={}])

添加来源对象自身的所有可枚举函数属性到目标对象。 如果 object 是个函数,那么函数方法将被添加到原型链上。

注意: 使用 _.runInContext 来创建原始的 lodash 函数来避免修改造成的冲突。

添加版本

0.1.0

参数

[object=lodash] (Function|Object): 目标对象。

source (Object): 来源对象。

[options={}] (Object): 选项对象。

[options.chain=true] (boolean): 是否开启链式操作。

返回

(*): 返回 object.

mixin 源码

点击这里展开mixin源码,后文注释解析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
复制代码function mixin(object, source, options) {
var props = keys(source),
methodNames = baseFunctions(source, props);

if (options == null &&
!(isObject(source) && (methodNames.length || !props.length))) {
options = source;
source = object;
object = this;
methodNames = baseFunctions(source, keys(source));
}
var chain = !(isObject(options) && 'chain' in options) || !!options.chain,
isFunc = isFunction(object);

arrayEach(methodNames, function(methodName) {
var func = source[methodName];
object[methodName] = func;
if (isFunc) {
object.prototype[methodName] = function() {
var chainAll = this.__chain__;
if (chain || chainAll) {
var result = object(this.__wrapped__),
actions = result.__actions__ = copyArray(this.__actions__);

actions.push({ 'func': func, 'args': arguments, 'thisArg': object });
result.__chain__ = chainAll;
return result;
}
return func.apply(object, arrayPush([this.value()], arguments));
};
}
});

return object;
}

接下来先看衍生的函数。
其实看到具体定义的函数代码就大概知道这个函数的功能。为了不影响主线,导致文章篇幅过长。具体源码在这里就不展开。

感兴趣的读者可以自行看这些函数衍生的其他函数的源码。

mixin 衍生的函数 keys

在 mixin 函数中 其实最终调用的就是 Object.keys

1
2
3
复制代码function keys(object) {
return isArrayLike(object) ? arrayLikeKeys(object) : baseKeys(object);
}

mixin 衍生的函数 baseFunctions

返回函数数组集合

1
2
3
4
5
复制代码function baseFunctions(object, props) {
return arrayFilter(props, function(key) {
return isFunction(object[key]);
});
}

mixin 衍生的函数 isFunction

判断参数是否是函数

1
2
3
4
5
6
7
8
9
复制代码function isFunction(value) {
if (!isObject(value)) {
return false;
}
// The use of `Object#toString` avoids issues with the `typeof` operator
// in Safari 9 which returns 'object' for typed arrays and other constructors.
var tag = baseGetTag(value);
return tag == funcTag || tag == genTag || tag == asyncTag || tag == proxyTag;
}

mixin 衍生的函数 arrayEach

类似 [].forEarch

1
2
3
4
5
6
7
8
9
10
11
复制代码function arrayEach(array, iteratee) {
var index = -1,
length = array == null ? 0 : array.length;

while (++index < length) {
if (iteratee(array[index], index, array) === false) {
break;
}
}
return array;
}

mixin 衍生的函数 arrayPush

类似 [].push

1
2
3
4
5
6
7
8
9
10
复制代码function arrayPush(array, values) {
var index = -1,
length = values.length,
offset = array.length;

while (++index < length) {
array[offset + index] = values[index];
}
return array;
}

mixin 衍生的函数 copyArray

拷贝数组

1
2
3
4
5
6
7
8
9
10
复制代码function copyArray(source, array) {
var index = -1,
length = source.length;

array || (array = Array(length));
while (++index < length) {
array[index] = source[index];
}
return array;
}

mixin 源码解析

lodash 源码中两次调用 mixin

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
复制代码// Add methods that return wrapped values in chain sequences.
lodash.after = after;
// code ... 等 153 个支持链式调用的方法

// Add methods to `lodash.prototype`.
// 把lodash上的静态方法赋值到 lodash.prototype 上
mixin(lodash, lodash);

// Add methods that return unwrapped values in chain sequences.
lodash.add = add;
// code ... 等 152 个不支持链式调用的方法


// 这里其实就是过滤 after 等支持链式调用的方法,获取到 lodash 上的 add 等 添加到lodash.prototype 上。
mixin(lodash, (function() {
var source = {};
// baseForOwn 这里其实就是遍历lodash上的静态方法,执行回调函数
baseForOwn(lodash, function(func, methodName) {
// 第一次 mixin 调用了所以赋值到了lodash.prototype
// 所以这里用 Object.hasOwnProperty 排除不在lodash.prototype 上的方法。也就是 add 等 152 个不支持链式调用的方法。
if (!hasOwnProperty.call(lodash.prototype, methodName)) {
source[methodName] = func;
}
});
return source;
// 最后一个参数options 特意注明不支持链式调用
}()), { 'chain': false });

结合两次调用mixin 代入到源码解析如下

点击这里展开mixin源码及注释

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
复制代码function mixin(object, source, options) {
// source 对象中可以枚举的属性
var props = keys(source),
// source 对象中的方法名称数组
methodNames = baseFunctions(source, props);

if (options == null &&
!(isObject(source) && (methodNames.length || !props.length))) {
// 如果 options 没传为 undefined undefined == null 为true
// 且 如果source 不为 对象或者不是函数
// 且 source对象的函数函数长度 或者 source 对象的属性长度不为0
// 把 options 赋值为 source
options = source;
// 把 source 赋值为 object
source = object;
// 把 object 赋值为 this 也就是 _ (lodash)
object = this;
// 获取到所有的方法名称数组
methodNames = baseFunctions(source, keys(source));
}
// 是否支持 链式调用
// options 不是对象或者不是函数,是null或者其他值
// 判断options是否是对象或者函数,如果不是或者函数则不会执行 'chain' in options 也就不会报错
// 且 chain 在 options的对象或者原型链中
// 知识点 in [MDN in : https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Operators/in
// 如果指定的属性在指定的对象或其原型链中,则in 运算符返回true。

// 或者 options.chain 转布尔值
var chain = !(isObject(options) && 'chain' in options) || !!options.chain,
// object 是函数
isFunc = isFunction(object);

// 循环 方法名称数组
arrayEach(methodNames, function(methodName) {
// 函数本身
var func = source[methodName];
// object 通常是 lodash 也赋值这个函数。
object[methodName] = func;
if (isFunc) {
// 如果object是函数 赋值到 object prototype 上,通常是lodash
object.prototype[methodName] = function() {
// 实例上的__chain__ 属性 是否支持链式调用
// 这里的 this 是 new LodashWrapper 实例 类似如下
/**
{
__actions__: [],
__chain__: true
__index__: 0
__values__: undefined
__wrapped__: []
}
**/

var chainAll = this.__chain__;
// options 中的 chain 属性 是否支持链式调用
// 两者有一个符合链式调用 执行下面的代码
if (chain || chainAll) {
// 通常是 lodash
var result = object(this.__wrapped__),
// 复制 实例上的 __action__ 到 result.__action__ 和 action 上
actions = result.__actions__ = copyArray(this.__actions__);

// action 添加 函数 和 args 和 this 指向,延迟计算调用。
actions.push({ 'func': func, 'args': arguments, 'thisArg': object });
//实例上的__chain__ 属性 赋值给 result 的 属性 __chain__
result.__chain__ = chainAll;
// 最后返回这个实例
return result;
}

// 都不支持链式调用。直接调用
// 把当前实例的 value 和 arguments 对象 传递给 func 函数作为参数调用。返回调用结果。
return func.apply(object, arrayPush([this.value()], arguments));
};
}
});

// 最后返回对象 object
return object;
}

小结:简单说就是把lodash上的静态方法赋值到lodash.prototype上。分两次第一次是支持链式调用(lodash.after等 153个支持链式调用的方法),第二次是不支持链式调用的方法(lodash.add等152个不支持链式调用的方法)。

lodash 究竟在_和_.prototype挂载了多少方法和属性

再来看下lodash究竟挂载在_函数对象上有多少静态方法和属性,和挂载_.prototype上有多少方法和属性。

使用for in循环一试便知。看如下代码:

1
2
3
4
5
6
7
8
9
10
11
12
复制代码var staticMethods = [];
var staticProperty = [];
for(var name in _){
if(typeof _[name] === 'function'){
staticMethods.push(name);
}
else{
staticProperty.push(name);
}
}
console.log(staticProperty); // ["templateSettings", "VERSION"] 2个
console.log(staticMethods); // ["after", "ary", "assign", "assignIn", "assignInWith", ...] 305个

其实就是上文提及的 lodash.after 等153个支持链式调用的函数 、lodash.add 等 152不支持链式调用的函数赋值而来。

1
2
3
4
5
6
7
8
9
10
11
12
复制代码var prototypeMethods = [];
var prototypeProperty = [];
for(var name in _.prototype){
if(typeof _.prototype[name] === 'function'){
prototypeMethods.push(name);
}
else{
prototypeProperty.push(name);
}
}
console.log(prototypeProperty); // []
console.log(prototypeMethods); // ["after", "all", "allKeys", "any", "assign", ...] 317个

相比lodash上的静态方法多了12个,说明除了 mixin 外,还有12个其他形式赋值而来。

支持链式调用的方法最后返回是实例对象,获取最后的处理的结果值,最后需要调用value方法。

笔者画了一张表示lodash的方法和属性挂载关系图。

的方法和属性挂载关系

lodash的方法和属性挂载关系

请出贯穿下文的简单的例子

1
2
3
4
5
6
7
8
9
10
复制代码var result = _.chain([1, 2, 3, 4, 5])
.map(el => {
console.log(el); // 1, 2, 3
return el + 1;
})
.take(3)
.value();
// lodash中这里的`map`仅执行了`3`次。
// 具体功能也很简单 数组 1-5 加一,最后获取其中三个值。
console.log('result:', result);

也就是说这里lodash聪明的知道了最后需要几个值,就执行几次map循环,对于很大的数组,提升性能很有帮助。

而underscore执行这段代码其中map执行了5次。
如果是平常实现该功能也简单。

1
2
复制代码var result = [1, 2, 3, 4, 5].map(el => el + 1).slice(0, 3);
console.log('result:', result);

而相比lodash这里的map执行了5次。

1
2
3
4
5
6
7
复制代码// 不使用 map、slice
var result = [];
var arr = [1, 2, 3, 4, 5];
for (var i = 0; i < 3; i++){
result[i] = arr[i] + 1;
}
console.log(result, 'result');

简单说这里的map方法,添加 LazyWrapper 的方法到 lodash.prototype存储下来,最后调用 value时再调用。
具体看下文源码实现。

添加 LazyWrapper 的方法到 lodash.prototype

主要是如下方法添加到到 lodash.prototype 原型上。

1
2
复制代码// "constructor"
["drop", "dropRight", "take", "takeRight", "filter", "map", "takeWhile", "head", "last", "initial", "tail", "compact", "find", "findLast", "invokeMap", "reject", "slice", "takeRightWhile", "toArray", "clone", "reverse", "value"]

点击这里展开具体源码及注释

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
复制代码// Add `LazyWrapper` methods to `lodash.prototype`.
// baseForOwn 这里其实就是遍历LazyWrapper.prototype上的方法,执行回调函数
baseForOwn(LazyWrapper.prototype, function(func, methodName) {
// 检测函数名称是否是迭代器也就是循环
var checkIteratee = /^(?:filter|find|map|reject)|While$/.test(methodName),
// 检测函数名称是否head和last
// 顺便提一下 ()这个是捕获分组 而加上 ?: 则是非捕获分组 也就是说不用于其他操作
isTaker = /^(?:head|last)$/.test(methodName),
// lodashFunc 是 根据 isTaker 组合 takeRight take methodName
lodashFunc = lodash[isTaker ? ('take' + (methodName == 'last' ? 'Right' : '')) : methodName],
// 根据isTaker 和 是 find 判断结果是否 包装
retUnwrapped = isTaker || /^find/.test(methodName);

// 如果不存在这个函数,就不往下执行
if (!lodashFunc) {
return;
}
// 把 lodash.prototype 方法赋值到lodash.prototype
lodash.prototype[methodName] = function() {
// 取实例中的__wrapped__ 值 例子中则是 [1,2,3,4,5]
var value = this.__wrapped__,
// 如果是head和last 方法 isTaker 返回 [1], 否则是arguments对象
args = isTaker ? [1] : arguments,
// 如果value 是LayeWrapper的实例
isLazy = value instanceof LazyWrapper,
// 迭代器 循环
iteratee = args[0],
// 使用useLazy isLazy value或者是数组
useLazy = isLazy || isArray(value);

var interceptor = function(value) {
// 函数执行 value args 组合成数组参数
var result = lodashFunc.apply(lodash, arrayPush([value], args));
// 如果是 head 和 last (isTaker) 支持链式调用 返回结果的第一个参数 否则 返回result
return (isTaker && chainAll) ? result[0] : result;
};

// useLazy true 并且 函数checkIteratee 且迭代器是函数,且迭代器参数个数不等于1
if (useLazy && checkIteratee && typeof iteratee == 'function' && iteratee.length != 1) {
// Avoid lazy use if the iteratee has a "length" value other than `1`.
// useLazy 赋值为 false
// isLazy 赋值为 false
isLazy = useLazy = false;
}
// 取实例上的 __chain__
var chainAll = this.__chain__,
// 存储的待执行的函数 __actions__ 二次取反是布尔值 也就是等于0或者大于0两种结果
isHybrid = !!this.__actions__.length,
// 是否不包装 用结果是否不包装 且 不支持链式调用
isUnwrapped = retUnwrapped && !chainAll,
// 是否仅Lazy 用isLazy 和 存储的函数
onlyLazy = isLazy && !isHybrid;

// 结果不包装 且 useLazy 为 true
if (!retUnwrapped && useLazy) {
// 实例 new LazyWrapper 这里的this 是 new LodashWrapper()
value = onlyLazy ? value : new LazyWrapper(this);
// result 执行函数结果
var result = func.apply(value, args);

/*
*
// _.thru(value, interceptor)
// 这个方法类似 _.tap, 除了它返回 interceptor 的返回结果。该方法的目的是"传递" 值到一个方法链序列以取代中间结果。
_([1, 2, 3])
.tap(function(array) {
// 改变传入的数组
array.pop();
})
.reverse()
.value();
// => [2, 1]
*/

// thisArg 指向undefined 或者null 非严格模式下是指向window,严格模式是undefined 或者nll
result.__actions__.push({ 'func': thru, 'args': [interceptor], 'thisArg': undefined });
// 返回实例 lodashWrapper
return new LodashWrapper(result, chainAll);
}
// 不包装 且 onlyLazy 为 true
if (isUnwrapped && onlyLazy) {
// 执行函数
return func.apply(this, args);
}
// 上面都没有执行,执行到这里了
// 执行 thru 函数,回调函数 是 interceptor
result = this.thru(interceptor);
return isUnwrapped ? (isTaker ? result.value()[0] : result.value()) : result;
};
});

小结一下,写了这么多注释,简单说:其实就是用LazyWrapper.prototype 改写原先在lodash.prototype的函数,判断函数是否需要使用惰性求值,需要时再调用。

读者可以断点调试一下,善用断点进入函数功能,对着注释看,可能会更加清晰。

点击查看断点调试的部分截图
例子的chain和map执行后的debugger截图

例子的chain和map执行后的debugger截图

例子的chain和map执行后的结果截图

例子的chain和map执行后的结果截图

链式调用最后都是返回实例对象,实际的处理数据的函数都没有调用,而是被存储存储下来了,最后调用value方法,才执行这些函数。

lodash.prototype.value 即 wrapperValue

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
复制代码function baseWrapperValue(value, actions) {
var result = value;
// 如果是lazyWrapper的实例,则调用LazyWrapper.prototype.value 方法,也就是 lazyValue 方法
if (result instanceof LazyWrapper) {
result = result.value();
}
// 类似 [].reduce(),把上一个函数返回结果作为参数传递给下一个函数
return arrayReduce(actions, function(result, action) {
return action.func.apply(action.thisArg, arrayPush([result], action.args));
}, result);
}
function wrapperValue() {
return baseWrapperValue(this.__wrapped__, this.__actions__);
}
lodash.prototype.toJSON = lodash.prototype.valueOf = lodash.prototype.value = wrapperValue;

如果是惰性求值,则调用的是 LazyWrapper.prototype.value 即 lazyValue。

LazyWrapper.prototype.value 即 lazyValue 惰性求值

点击这里展开lazyValue源码及注释

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
复制代码function LazyWrapper(value) {
// 参数 value
this.__wrapped__ = value;
// 执行的函数
this.__actions__ = [];
this.__dir__ = 1;
// 过滤
this.__filtered__ = false;
// 存储迭代器函数
this.__iteratees__ = [];
// 默认最大取值个数
this.__takeCount__ = MAX_ARRAY_LENGTH;
// 具体取值多少个,存储函数和类型
this.__views__ = [];
}
/**
* Extracts the unwrapped value from its lazy wrapper.
*
* @private
* @name value
* @memberOf LazyWrapper
* @returns {*} Returns the unwrapped value.
*/
function lazyValue() {
// this.__wrapped__ 是 new LodashWrapper 实例 所以执行.value 获取原始值
var array = this.__wrapped__.value(),
//
dir = this.__dir__,
// 是否是函数
isArr = isArray(array),
// 是否从右边开始
isRight = dir < 0,
// 数组的长度。如果不是数组,则是0
arrLength = isArr ? array.length : 0,
// 获取 take(3) 上述例子中 则是 start: 0,end: 3
view = getView(0, arrLength, this.__views__),
start = view.start,
end = view.end,
// 长度 3
length = end - start,
// 如果是是从右开始
index = isRight ? end : (start - 1),
// 存储的迭代器数组
iteratees = this.__iteratees__,
// 迭代器数组长度
iterLength = iteratees.length,
// 结果resIndex
resIndex = 0,
// 最后获取几个值,也就是 3
takeCount = nativeMin(length, this.__takeCount__);

// 如果不是数组,或者 不是从右开始 并且 参数数组长度等于take的长度 takeCount等于长度
// 则直接调用 baseWrapperValue 不需要
if (!isArr || (!isRight && arrLength == length && takeCount == length)) {
return baseWrapperValue(array, this.__actions__);
}
var result = [];

// 标签语句 label
// MDN label 链接
// https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Statements/label
// 标记语句可以和 break 或 continue 语句一起使用。标记就是在一条语句前面加个可以引用的标识符(identifier)。
outer:
while (length-- && resIndex < takeCount) {
index += dir;

var iterIndex = -1,
// 数组第一项
value = array[index];

while (++iterIndex < iterLength) {
// 迭代器数组 {iteratee: function{}, typy: 2}
var data = iteratees[iterIndex],
iteratee = data.iteratee,
type = data.type,
// 结果 迭代器执行结果
computed = iteratee(value);

if (type == LAZY_MAP_FLAG) {
// 如果 type 是 map 类型,结果 computed 赋值给value
value = computed;
} else if (!computed) {
if (type == LAZY_FILTER_FLAG) {
// 退出当前这次循环,进行下一次循环
continue outer;
} else {
// 退出整个循环
break outer;
}
}
}
// 最终数组
result[resIndex++] = value;
}
// 返回数组 例子中则是 [2, 3, 4]
return result;
}
// Ensure `LazyWrapper` is an instance of `baseLodash`.
LazyWrapper.prototype = baseCreate(baseLodash.prototype);
LazyWrapper.prototype.constructor = LazyWrapper;

LazyWrapper.prototype.value = lazyValue;

笔者画了一张 lodash和LazyWrapper的关系图来表示。

和的关系图

lodash和LazyWrapper的关系图

小结:lazyValue简单说实现的功能就是把之前记录的需要执行几次,把记录存储的函数执行几次,不会有多少项数据就执行多少次,而是根据需要几项,执行几项。
也就是说以下这个例子中,map函数只会执行3次。如果没有用惰性求值,那么map函数会执行5次。

1
2
3
4
复制代码var result = _.chain([1, 2, 3, 4, 5])
.map(el => el + 1)
.take(3)
.value();

总结

行文至此,基本接近尾声,最后总结一下。

文章主要学习了runInContext() 导出_ lodash函数使用baseCreate方法原型继承LodashWrapper和LazyWrapper,mixin挂载方法到lodash.prototype、后文用结合例子解释lodash.prototype.value(wrapperValue)和Lazy.prototype.value(lazyValue)惰性求值的源码具体实现。

分享一个只知道函数名找源码定位函数申明位置的VSCode 技巧:Ctrl + p。输入 @functionName 定位函数functionName在源码文件中的具体位置。如果知道调用位置,那直接按alt+鼠标左键即可跳转到函数申明的位置。

如果读者发现有不妥或可改善之处,再或者哪里没写明白的地方,欢迎评论指出。另外觉得写得不错,对您有些许帮助,可以点赞、评论、转发分享,也是对笔者的一种支持。万分感谢。

推荐阅读

lodash 仓库 | lodash 官方文档 | lodash 中文文档

打造一个类似于lodash的前端工具库

惰性求值——lodash源码解读

luobo tang:lazy.js 惰性求值实现分析

lazy.js github 仓库

本文章学习的lodash的版本v4.17.15 unpkg.com链接

笔者往期文章

面试官问:JS的继承

面试官问:JS的this指向

面试官问:能否模拟实现JS的call和apply方法

面试官问:能否模拟实现JS的bind方法

面试官问:能否模拟实现JS的new操作符

前端使用puppeteer 爬虫生成《React.js 小书》PDF并合并

关于

作者:常以若川为名混迹于江湖。前端路上 | PPT爱好者 | 所知甚少,唯善学。

个人博客-若川,使用vuepress重构了,阅读体验可能更好些

掘金专栏,欢迎关注~

segmentfault前端视野专栏,欢迎关注~

知乎前端视野专栏,欢迎关注~

github blog,相关源码和资源都放在这里,求个star^_^~

欢迎加微信交流 微信公众号

可能比较有趣的微信公众号,长按扫码关注。欢迎加笔者微信ruochuan12(注明来源,基本来者不拒),拉您进【前端视野交流群】,长期交流学习~

若川视野

若川视野

本文使用 mdnice 排版

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

3步轻松搞定Spring Boot缓存

发表于 2019-09-06

作者:谭朝红前言

本次内容主要介绍基于Ehcache 3.0来快速实现Spring Boot应用程序的数据缓存功能。在Spring Boot应用程序中,我们可以通过Spring Caching来快速搞定数据缓存。
接下来我们将介绍如何在三步之内搞定 Spring Boot 缓存。1. 创建一个Spring Boot工程


你所创建的Spring Boot应用程序的maven依赖文件至少应该是下面的样子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
复制代码<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.1.3.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.ramostear</groupId>
<artifactId>cache</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>cache</name>
<description>Demo project for Spring Boot</description>
<properties>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-cache</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.ehcache</groupId>
<artifactId>ehcache</artifactId>
</dependency>
<dependency>
<groupId>javax.cache</groupId>
<artifactId>cache-api</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>

依赖说明:* spring-boot-starter-cache为Spring Boot应用程序提供缓存支持

  • ehcache提供了Ehcache的缓存实现
  • cache-api 提供了基于JSR-107的缓存规范
  1. 配置Ehcache缓存

现在,需要告诉Spring Boot去哪里找缓存配置文件,这需要在Spring Boot配置文件中进行设置:

1
复制代码spring.cache.jcache.config=classpath:ehcache.xml

然后使用@EnableCaching注解开启Spring Boot应用程序缓存功能,你可以在应用主类中进行操作:

1
2
3
4
5
6
7
8
9
10
11
复制代码package com.ramostear.cache;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cache.annotation.EnableCaching;
@SpringBootApplication
@EnableCaching
public class CacheApplication {
public static void main(String[] args) {
SpringApplication.run(CacheApplication.class, args);
}
}

接下来,需要创建一个 ehcache 的配置文件,该文件放置在类路径下,如resources目录下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
复制代码<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="http://www.ehcache.org/v3"
xmlns:jsr107="http://www.ehcache.org/v3/jsr107"
xsi:schemaLocation="
http://www.ehcache.org/v3 http://www.ehcache.org/schema/ehcache-core-3.0.xsd
http://www.ehcache.org/v3/jsr107 http://www.ehcache.org/schema/ehcache-107-ext-3.0.xsd">
<service>
<jsr107:defaults enable-statistics="true"/>
</service>
<cache alias="person">
<key-type>java.lang.Long</key-type>
<value-type>com.ramostear.cache.entity.Person</value-type>
<expiry>
<ttl unit="minutes">1</ttl>
</expiry>
<listeners>
<listener>
<class>com.ramostear.cache.config.PersonCacheEventLogger</class>
<event-firing-mode>ASYNCHRONOUS</event-firing-mode>
<event-ordering-mode>UNORDERED</event-ordering-mode>
<events-to-fire-on>CREATED</events-to-fire-on>
<events-to-fire-on>UPDATED</events-to-fire-on>
<events-to-fire-on>EXPIRED</events-to-fire-on>
<events-to-fire-on>REMOVED</events-to-fire-on>
<events-to-fire-on>EVICTED</events-to-fire-on>
</listener>
</listeners>
<resources>
<heap unit="entries">2000</heap>
<offheap unit="MB">100</offheap>
</resources>
</cache>
</config>

最后,还需要定义个缓存事件监听器,用于记录系统操作缓存数据的情况,最快的方法是实现CacheEventListener接口:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
复制代码package com.ramostear.cache.config;
import org.ehcache.event.CacheEvent;
import org.ehcache.event.CacheEventListener;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* @author ramostear
* @create-time 2019/4/7 0007-0:48
* @modify by :
* @since:
*/
public class PersonCacheEventLogger implements CacheEventListener<Object,Object>{
private static final Logger logger = LoggerFactory.getLogger(PersonCacheEventLogger.class);
@Override
public void onEvent(CacheEvent cacheEvent) {
logger.info("person caching event {} {} {} {}",
cacheEvent.getType(),
cacheEvent.getKey(),
cacheEvent.getOldValue(),
cacheEvent.getNewValue());
}
}
  1. 使用@Cacheable注解

要让Spring Boot能够缓存我们的数据,还需要使用@Cacheable注解对业务方法进行注释,告诉Spring Boot该方法中产生的数据需要加入到缓存中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
复制代码package com.ramostear.cache.service;
import com.ramostear.cache.entity.Person;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.stereotype.Service;
/**
* @author ramostear
* @create-time 2019/4/7 0007-0:51
* @modify by :
* @since:
*/
@Service(value = "personService")
public class PersonService {
@Cacheable(cacheNames = "person",key = "#id")
public Person getPerson(Long id){
Person person = new Person(id,"ramostear","ramostear@163.com");
return person;
}
}

通过以上三个步骤,我们就完成了Spring Boot的缓存功能。接下来,我们将测试一下缓存的实际情况。4. 缓存测试

为了测试我们的应用程序,创建一个简单的Restful端点,它将调用PersonService返回一个Person对象:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
复制代码package com.ramostear.cache.controller;
import com.ramostear.cache.entity.Person;
import com.ramostear.cache.service.PersonService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* @author ramostear
* @create-time 2019/4/7 0007-0:54
* @modify by :
* @since:
*/
@RestController
@RequestMapping("/persons")
public class PersonController {
@Autowired
private PersonService personService;
@GetMapping("/{id}")
public ResponseEntity<Person> person(@PathVariable(value = "id") Long id){
return new ResponseEntity<>(personService.getPerson(id), HttpStatus.OK);
}
}

Person是一个简单的POJO类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
复制代码package com.ramostear.cache.entity;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import java.io.Serializable;
/**
* @author ramostear
* @create-time 2019/4/7 0007-0:45
* @modify by :
* @since:
*/
@Getter
@Setter
@AllArgsConstructor
@NoArgsConstructor
public class Person implements Serializable{
private Long id;
private String username;
private String email;
}

以上准备工作都完成后,让我们编译并运行应用程序。项目成功启动后,使用浏览器打开:http://localhost:8080/persons/1 ,你将在浏览器页面中看到如下的信息:

1
复制代码{"id":1,"username":"ramostear","email":"ramostear@163.com"}

此时在观察控制台输出的日志信息:

1
2
3
4
5
复制代码2019-04-07 01:08:01.001  INFO 6704 --- [nio-8080-exec-1] 
o.s.web.servlet.DispatcherServlet : Completed initialization in 5 ms
2019-04-07 01:08:01.054 INFO 6704 --- [e [_default_]-0]
c.r.cache.config.PersonCacheEventLogger : person caching event CREATED 1
null com.ramostear.cache.entity.Person@ba8a729

由于我们是第一次请求API,没有任何缓存数据。因此,Ehcache创建了一条缓存数据,可以通过CREATED看一了解到。我们在ehcache.xml文件中将缓存过期时间设置成了1分钟(1),因此在一分钟之内我们刷新浏览器,不会看到有新的日志输出,一分钟之后,缓存过期,我们再次刷新浏览器,将看到如下的日志输出:

1
2
3
4
5
6
复制代码2019-04-07 01:09:28.612  INFO 6704 --- [e [_default_]-1]
c.r.cache.config.PersonCacheEventLogger : person caching event EXPIRED 1
com.ramostear.cache.entity.Person@a9f3c57 null
2019-04-07 01:09:28.612 INFO 6704 --- [e [_default_]-1]
c.r.cache.config.PersonCacheEventLogger : person caching event CREATED 1
null com.ramostear.cache.entity.Person@416900ce

第一条日志提示缓存已经过期,第二条日志提示Ehcache重新创建了一条缓存数据。总结

在本次案例中,通过简单的三个步骤,讲解了基于 Ehcache 的 Spring Boot 应用程序缓存实现。文章内容重在缓存实现的基本步骤与方法,简化了具体的业务代码,有兴趣的朋友可以自行扩展。最后

欢迎大家关注我的公众号【程序员追风】,文章都会在里面更新,整理的资料也会放在里面。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

从零开始开发IM(即时通讯)服务端

发表于 2019-09-04

前言

首先讲讲IM(即时通讯)技术可以用来做什么:

聊天:qq、微信

直播:斗鱼直播、抖音

实时位置共享、游戏多人互动等等

可以说几乎所有高实时性的应用场景都需要用到IM技术。

本篇将带大家从零开始搭建一个轻量级的IM服务端,麻雀虽小,五脏俱全,我们搭建的IM服务端实现以下功能:

  1. 一对一的文本消息、文件消息通信
  2. 每个消息有“已发送”/“已送达”/“已读”回执
  3. 存储离线消息
  4. 支持用户登录,好友关系等基本功能。
  5. 能够方便地水平扩展

通过这个项目能学到什么?

这个项目涵盖了很多后端必备知识:

  • rpc通信
  • 数据库
  • 缓存
  • 消息队列
  • 分布式、高并发的架构设计
  • docker部署

消息通信

文本消息

我们先从最简单的特性开始实现:一个普通消息的发送

消息格式如下:

1
2
3
4
5
6
7
8
9
10
ini复制代码message ChatMsg{
id = 1;
//消息id
fromId = Alice
//发送者userId
destId = Bob
//接收者userId
msgBody = hello
//消息体
}

msg.png
如上图,我们现在有两个用户:Alice和Bob连接到了服务器,当Alice发送消息message(hello)给Bob,服务端接收到消息,根据消息的destId进行转发,转发给Bob。

发送回执

那我们要怎么来实现回执的发送呢?

我们定义一种回执数据格式ACK,MsgType有三种,分别是sent(已发送), delivered(已送达), read(已读):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
ini复制代码message AckMsg {
id;
//消息id
fromId;
//发送者id
destId;
//接收者id
msgType;
//消息类型
ackMsgId;
//确认的消息id
}

enum MsgType {
DELIVERED;
READ;
}

当服务端接受到Alice发来的消息时:

  1. 向Alice发送一个sent(hello)表示消息已经被发送到服务器。
1
2
3
4
5
6
7
ini复制代码message AckMsg {
id = 2;
fromId = Alice;
destId = Bob;
msgType = SENT;
ackMsgId = 1;
}

sent.png
2. 服务器把hello转发给Bob后,立刻向Alice发送delivered(hello)表示消息已经发送给Bob。

1
2
3
4
5
6
7
ini复制代码message AckMsg {
id = 3;
fromId = Bob;
destId = Alice;
msgType = DELIVERED;
ackMsgId = 1;
}

delivered.png
3. Bob阅读消息后,客户端向服务器发送read(hello)表示消息已读

1
2
3
4
5
6
7
ini复制代码message AckMsg {
id = 4;
fromId = Bob;
destId = Alice;
msgType = READ;
ackMsgId = 1;
}

这个消息会像一个普通聊天消息一样被服务器处理,最终发送给Alice。
read.png

在服务器这里不区分ChatMsg和AckMsg,处理过程都是一样的:解析消息的destId并进行转发。

水平扩展

当用户量越来越大,必然需要增加服务器的数量,用户的连接被分散在不同的机器上。此时,就需要存储用户连接在哪台机器上。

我们引入一个新的模块来管理用户的连接信息。

管理用户状态

userstatus.png

模块叫做user status,共有三个接口:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
typescript复制代码public interface UserStatusService {

/**
* 用户上线,存储userId与机器id的关系
*
* @param userId
* @param connectorId
* @return 如果当前用户在线,则返回他连接的机器id,否则返回null
*/
String online(String userId, String connectorId);

/**
* 用户下线
*
* @param userId
*/
void offline(String userId);

/**
* 通过用户id查找他当前连接的机器id
*
* @param userId
* @return
*/
String getConnectorId(String userId);
}

这样我们就能够对用户连接状态进行管理了,具体的实现应考虑服务的用户量、期望性能等进行实现。

此处我们使用redis来实现,将userId和connectorId的关系以key-value的形式存储。

消息转发

除此之外,还需要一个模块在不同的机器上转发消息,如下结构:
im-structure1.png

此时我们的服务被拆分成了connector和transfer两个模块,connector模块用于维持用户的长链接,而transfer的作用是将消息在多个connector之间转发。

现在Alice和Bob连接到了两台connector上,那么消息要如何传递呢?

  1. Alice上线,连接到机器[1]上时
    • 将Alice和它的连接存入内存中。
    • 调用user status的online方法记录Alice上线。
  2. Alice发送了一条消息给Bob
    • 机器[1]收到消息后,解析destId,在内存中查找是否有Bob。
    • 如果没有,代表Bob未连接到这台机器,则转发给transfer。
  3. transfer调用user status的getConnectorId(Bob)方法找到Bob所连接的connector,返回机器[2],则转发给机器[2]。

流程图:
transfer.png

总结:

  • 引入user status模块管理用户连接,transfer模块在不同的机器之间转发,使服务可以水平扩展。
  • 为了满足实时转发,transfer需要和每台connector机器都保持长链接。

离线消息

如果用户当前不在线,就必须把消息持久化下来,等待用户下次上线再推送,这里使用mysql存储离线消息。

为了方便地水平扩展,我们使用消息队列进行解耦。

  • transfer接收到消息后如果发现用户不在线,就发送给消息队列入库。
  • 用户登录时,服务器从库里拉取离线消息进行推送。

用户登录、好友关系

用户的注册登录、账户管理、好友关系链等功能更适合使用http协议,因此我们将这个模块做成一个restful服务,对外暴露http接口供客户端调用。

至此服务端的基本架构就完成了:
im-structure.png

总结

以上就是这篇博客的所有内容,本篇帮大家构建了IM服务端的架构,但还有很多细节需要我们去思考,例如:

  • 如何保证消息的顺序和唯一
  • 多个设备在线如何保证消息一致性
  • 如何处理消息发送失败
  • 消息的安全性
  • 如果要存储聊天记录要怎么做
  • 数据库分表分库
  • 服务高可用

……

更多细节实现就留到下一篇啦~

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

1…858859860…956

开发者博客

9558 日志
1953 标签
RSS
© 2025 开发者博客
本站总访问量次
由 Hexo 强力驱动
|
主题 — NexT.Muse v5.1.4
0%