欢迎光临
我们一直在努力

聊聊golang的defer

lei阅读(2)

本文主要研究一下golang的defer

defer

  • return先赋值(对于命名返回值),然后执行defer,最后函数返回
  • defer函数调用的执行顺序与它们分别所属的defer语句的执行顺序相反
  • defer后面的表达式可以是func或者是method的调用,如果defer的函数为nil,则会panic

实例

实例1

// f returns 42
func f() (result int) {
    defer func() {
        // result is accessed after it was set to 6 by the return statement
        result *= 7
    }()
    return 6
}

这里return先给result赋值为6,之后执行defer,result变为42,最后返回42

实例2

func f() int {
    result := 6
    defer func() {
        // result is accessed after it was set to 6 by the return statement
        result *= 7
    }()
    return result
}

这里return确定返回值6,之后defer修改result,最后函数返回return确定的返回值

实例3

func multiDefer() {
    for i := 3; i > 0; i-- {
        defer func(n int) {
            fmt.Print(n, " ")
        }(i)
    }

    for i := 3; i > 0; i-- {
        defer fmt.Print(i, " ")
    }
}

多个defer函数,按顺序逆序执行,这里输出1 2 3

实例4

var fc func() string

func main() {
    fmt.Println("hello")
    defer fc()
}

由于defer指定的func为nil,这里panic

实例5

func main() {
    for i := 3; i > 0; i-- {
        defer func() {
            fmt.Print(i, " ")
        }()
    }
}

由于defer这里调用的func没有参数,等执行的时候,i已经为0,因而这里输出3个0

小结

defer可以拆解为return赋值,defer执行,最后代码返回三步;defer的顺序按逆序执行。

doc

https://segmentfault.com/a/1190000038348618

如何使用Gitlab+Jenkins实现多分支自动独立部署?

lei阅读(2)

开发免不了要测试,同一个项目一两个人开发的时候,开发、测试、部署一个分支就行,但当项目变大,开发人员变多时,如果还是这样操作,你可能就会经常遇到不同需求的代码同时测试时的相互干扰问题。比如,1、合并代码时经常发生冲突;2、一人代码写错,影响所有人等。

那么如何解决这个问题呢?

这里我介绍一个多分支同时部署测试的方案。具体就是每一个开发者的分支代码都可以独立部署到测试服务器(比如,不同的根目录,不同的容器),然后,各开发者可以在各自的测试分支独立调试。

下面以PHP项目为例来具体说明。

首先列一下我们可能用到的工具清单:

  1. Linux:这是一切操作的基础,本文中主要用到的Linux版本为Centos8
  2. Gitlab:负责管理源代码
  3. Jenkins:负责持续集成部署,
  4. Docker:负责搭建Gitlab、Jenkins、Web应用。
  5. Nginx:Web应用服务器、反向代理
  6. PHP:解析PHP代码
  7. GIT:管理源代码

需要说明的是,我们安装这些工具主要使用yum命令,因此在执行后面步骤前,请先确保你的Linux系统已经安装了yum命令。

具体步骤如下:

1、安装Docker

yum install docker

2、安装Gitlab

下载Gitlab镜像:

docker pull gitlab-ce

创建Gitlab容器了:

docker run --name gitlab -p 443:443 -p 80:80 -p 22:22 -v /data/www/gitlab/config:/etc/gitlab -v /data/www/gitlab/logs:/var/log/gitlab -v /data/www/gitlab/data:/var/opt/gitlab gitlab/gitlab-ce /bin/bash

访问localhost,查看效果:

如何使用Gitlab+Jenkins实现多分支自动独立部署?

3、安装Jenkins

制作Jenkins镜像

我们的仓库代码是不包含vendor目录的,需要git checkout 后,使用composer install命令自动生成。由于官方Jenkins镜像不包含Composer、PHP、Git等我们的PHP项目需要用到的命令,因此,在实际使用时,我们以官方镜像为基础制作了自己的Jenkins镜像。

下载Centos镜像,如下:

docker pull centos:latest

如何使用Gitlab+Jenkins实现多分支自动独立部署?

以Centos镜像为基础镜像,创建Jenkins容器:

docker run -idt --name jenkins docker.io/centos /bin/bash

进入jenkins容器内部:

docker exec -it jenkins /bin/bash

使用yum命令安装java、php、composer、git:

yum -y php git java
php -r "copy('https://getcomposer.org/installer', 'composer-setup.php');"
php -r "if (hash_file('sha384', 'composer-setup.php') === 'c31c1e292ad7be5f49291169c0ac8f683499edddcfd4e42232982d0fd193004208a58ff6f353fde0012d35fdd72bc394') { echo 'Installer verified'; } else { echo 'Installer corrupt'; unlink('composer-setup.php'); } echo PHP_EOL;"
php composer-setup.php
php -r "unlink('composer-setup.php');"
mv composer.phar /usr/bin/composer

如果提示PHP拓展缺失,请使用yum命令安装相关拓展。

使用yum命令,安装官方jenkins:

wget -O /etc/yum.repos.d/jenkins.repo https://pkg.jenkins.io/redhat/jenkins.repo
rpm --import https://pkg.jenkins.io/redhat/jenkins.io.key
yum install jenkins

修改Jenkins配置,配置文件默认位置为:/etc/sysconfig/jenkins

  • JENKINS_HOME
  • JENKINS_USER
  • JENKINS_PORT

JENKINS_HOME是Jenkins的主目录,Jenkins工作的目录都放在这里,Jenkins储存文件的地址,Jenkins的插件,生成的文件都在这个目录下。

JENKINS_USER是Jenkins的用户,拥有$JENKINS_HOME和/var/log/jenkins的权限,一般使用root用户。

JENKINS_PORT是Jenkins的端口,默认端口是8080。

启动Jenkins:

service jenkins start

如果出现错误,可能要考虑文件/etc/init.d/jenkins是否缺少配置,Jenkins的启动需要Java的支持。

如何使用Gitlab+Jenkins实现多分支自动独立部署?

如果Jenkins安装没有问题,下面我们就需要设置开机自启了。这是为了我们后面创建Jenkins容器的时候,Jenkins可以自动启动,而不需要我们进入容器中手动启动。jenkins.sh重启脚本如下:

如何使用Gitlab+Jenkins实现多分支自动独立部署?

给jenkins.sh设置可执行权限:

chmod +x /usr/local/jenkins/jenkins.sh

将文件添加系统自动启动程序列表:

echo "/usr/local/jenkins/jenkins.sh">> /etc/rc.d/rc.local

使用exit命令退出容器,然后使用docker commit命令制作新的Jenkins镜像:

docker commit -m 'Jenkins with docker/composer/git/php' jenkins jenkins:latest

以新镜像创建Jenkins容器,如下:

docker run -idt --name jenkins -p 8080:8080 -p 50000:50000 -v /var/run/docker.sock:/var/run/docker.sock -v /data/www/jenkins/jenkins_home:/var/lib/jenkins:rw jenkins /bin/bash

至此,Jenkins就安装好了。

4、在Gitlab创建项目

1、设置access token,记录生成的秘钥,后面创建Jenkins任务需要使用。

如何使用Gitlab+Jenkins实现多分支自动独立部署?

2、创建代码仓库

如何使用Gitlab+Jenkins实现多分支自动独立部署?

3、为代码仓库,设置Webhook

如何使用Gitlab+Jenkins实现多分支自动独立部署?

5、在Jenkins创建任务

在Jenkins中,Jenkins的主要功能都是由一个个插件提供的。因此,在搭建环境之前我们也需要先安装几个用到的插件。

如何使用Gitlab+Jenkins实现多分支自动独立部署?

如何使用Gitlab+Jenkins实现多分支自动独立部署?

如何使用Gitlab+Jenkins实现多分支自动独立部署?

我们用到的插件主要有以下几个:

  1. GitLab Plugin
  2. Gitlab Hook Plugin
  3. Gitlab API Plugin
  4. ruby-runtime

从名字我们也可以看到,这主要是用于Jenkins与Gitlab的交互。

说明一下,Jenkins与Gitlab的交互包括两部分,一部分是Gitlab通过Webhook提交Git Push事件,触发Jenkins开始执行集成任务,另一部分是Jenkins通过Git从Gitlab拉取源代码。

1、配置Gitlab链接

如何使用Gitlab+Jenkins实现多分支自动独立部署?

如何使用Gitlab+Jenkins实现多分支自动独立部署?

这个是Gitlab上创建的access token

2、创建集成任务

如何使用Gitlab+Jenkins实现多分支自动独立部署?

如何使用Gitlab+Jenkins实现多分支自动独立部署?

如何使用Gitlab+Jenkins实现多分支自动独立部署?

这是gitlab项目的仓库地址。

如何使用Gitlab+Jenkins实现多分支自动独立部署?

如何使用Gitlab+Jenkins实现多分支自动独立部署?

如何使用Gitlab+Jenkins实现多分支自动独立部署?

3、在本地Push代码到Gitlab,查看Jenkins任务执行情况

如何使用Gitlab+Jenkins实现多分支自动独立部署?

至此,多分支同时部署测试的环境就搭建好了。

image

https://segmentfault.com/a/1190000038347586

MySQL查询性能优化前,必须先掌握MySQL索引理论

lei阅读(2)

越努力,越幸运,
本文已收藏在GitHub中JavaCommunity, 里面有面试分享、源码分析系列文章,欢迎收藏,点赞
https://github.com/Ccww-lx/JavaCommunity

数据库索引在平时的工作是必备的,怎么建索引,怎么使用索引,可以提高数据的查询效率。而且在面试过程,数据库的索引也是必问的知识点,比如:

  • 索引底层结构选型,那为什么选择B+树?
  • 不同存储引擎的索引的体现形式有哪些?
  • 索引的类型

    • 组合索引存储方式
    • 查询方式
    • 最左前缀匹配原则
  • 覆盖索引是什么?

看着这些,能说出多少,理解多少呢?因此我们需要去探究其内在原理。

那索引是什么?

索引的目的为了加速检索数据而设计的一种分散存储(索引常常很大,属于硬盘级的东西,所以是分散存储)的数据结构,其原理以空间换时间。

而快速检索的实现的本质是数据结构,通过不同数据结构的选择,实现各种数据快速检索,索引有哈希索引和B+树索引。

索引底层结构选型,那为什么选择B+树?

数据库索引底层选型归根到底就是为提高检索效率,那么就需要考虑几个问题:

  • 算法时间复杂度
  • 是否存在排序
  • 磁盘IO与预读

NOTE: 考虑到磁盘IO是非常高昂的操作,计算机操作系统做了一些优化,当一次IO时,不光把当前磁盘地址的数据,而是把相邻的数据也都读取到内存缓冲区内,因为局部预读性原理告诉我们,当计算机访问一个地址的数据的时候,与其相邻的数据也会很快被访问到。每一次IO读取的数据我们称之为一页(page)。

哈希表( Hash Table,散列表 )

哈希表是根据键(Key)而直接访问在内存存储位置的数据结构。

image.png

通过计算一个关于键值的函数,将所需查询的数据映射到表中一个位置来访问记录,这加快了查找速度。虽然查询时间复杂度为O(1),但存在着碰撞问题,最坏情况会导致时间复杂急剧增加;

而且哈希表其只适合精准key(等于)检索,不适合范围式检索,范围检索就需要一次把所有数据找出来加载到内存,没有效率,因此不适合Mysql的底层索引的数据结构。

普通的二叉查找树

为了优化高效范围查询,且时间复杂度小,引入二叉查找树

image.png

二叉查找树的时间复杂度是 O(lgn),由于数据已排序好了,所以范围查询是可以高效查询,

但会存在的问题:左右子节点的深度可能相差很大,最极端的情况只有左子树或者右子树,此时查找的效率为O(n),检索性能急剧下降,因此也不适合Mysql的底层索引的数据结构。

image.png

平衡二叉树(AVL树)

为了优化二叉树左右子树深度相差太大的问题,我们引入了平衡二叉树,即左右子节点的深度差不超过1
平衡二叉树看来好像适合,可以实现:

  • 可以实现范围查找、数据排序
  • 查询性能良好O(logn)

image.png

NOTE:上图中一个磁盘块,代表硬盘上的一个存储位置

但是我们还有一个最重要因素需要考虑,磁盘IO与预读,且数据库查询数据的瓶颈在于磁盘 IO,使用平衡二叉树根据索引进行查找时,每读一个磁盘块就进行一次IO,这样没有实现计算机的预读,导致检索效率,总结出平衡二叉树作为索引的问题(上图中一个磁盘块,代表硬盘上的一个存储位置):

  • 太深了(即它只有二条路),深度越大进行的IO操作也就越多
  • 太小了,每一次IO才查询磁盘块这么一点数据,太浪费IO了。操作系统规定一次IO最小4K,Mysql一次IO 16K,而图上的磁盘块能明显达不到4K

B+树

为了优化磁盘IO和预读,减少IO操作,条路太少了,那么换成多条路,那么会想到使用B树B+树,但B树每个节点限制最多存储两个 key,也会造成IO操作过于频繁,因此优化思路为:尽可能在一次磁盘 IO 中多读一点数据到内存,那么B+树也该出场:

  • B+树一个节点能存很多索引,且只有B+树叶子节点存储数据
  • 相邻节点之间有一些前驱后继关系
  • 叶子节点是顺序排列的
    image.png

相对于B树,B+树的优势有

  • B+树扫库扫表的能力更强

    • B树的数据是存放在每一个节点中的,节点所在的物理地址又是随机的,所以扫表的话,进行的是随机IO
    • B+树的数据是存放在叶子节点的,且在一个叶子节点中的数据是连续的,所以扫表的话,进行的相对的顺序IO
  • B+树的磁盘读写能力更强,枝节点不保存数据,而保存更多的关键字。一次IO就能读出更多的关键字
  • B+树的排序能力更强,B+树的叶子节点存储的数据是已经排好序的

索引的体现形式

索引在不同的存储引擎中体现形式步一样, 最常见的是:

  • Innodb 引擎中体现为聚集索引方式 (索引和数据是存放在同一个文件的)
  • Myisam引擎中体现为非聚集索引方式 (索引和数据是存放在两个文件中的)

聚集索引方式(InnoDB存储引擎)

InnoDB存储引擎中,索引和数据是存放在同一个文件的,属于聚集索引 。而且InnoDB会自动建立好主键 ID 索引树, 因此建表时要求必须指定主键的原因。

其中,主键索引(聚集索引)的叶子节点记录了数据,而不是数据的物理地址。辅助索引的叶子节点存放的是主键key。所以当利用辅助索引查找数据时,实际上查了两遍索引(辅助索引和主键索引):

  • 先查询辅助索引树找出主键
  • 然后在主键索引树中根据主键查询数据

image.png

非聚集索引方式(Myisam存储引擎)

Myisam存储引擎中,索引和数据是存放在两个文件中的,属于非聚集索引 。不管是主键索引还是辅助索引,其叶子节点都是记录了数据的物理地址。

image.png

MySQL的索引类型

MySQL索引可以分为:

  • 普通索引(index): 加速查找
  • 唯一索引:

    • 主键索引:primary key :加速查找+约束(不为空且唯一)
    • 唯一索引:unique:加速查找+约束 (唯一)
  • 联合索引:

    • primary key(id,name):联合主键索引
    • unique(id,name):联合唯一索引
    • index(id,name):联合普通索引
  • 全文索引full text :用于搜索很长一篇文章的时候,效果最好。

其中,主要理解一下联合索引的问题,存储结构,查询方式。

联合索引

联合索引,多个列组成的索引叫做联合索引,单列索引是特殊的联合索引。其存储结构如下:

<font color=’red’>对于联合索引来说其存储结构只不过比单值索引多了几列,组合索引列数据都记录在索引树上,(不同的组合索引,B+树也是不同的),且存储引擎会首先根据第一个索引列排序后,其他列再依次将相等值的进行排序。</font>

image.png

NOTE:叶节点第一排,按顺序排序好,第二列,会基于第一列排序好的,将第一列相等的再下一列再排序,依次类推。

<font color=’red’>联合索引查询方式,存储引擎首先从根节点(一般常驻内存)开始查找,然后再依次在其他列中查询,直到找到该索引下的data元素即ID值,再从主键索引树上找到最终数据。</font>

而且联合索引其选择的原则:

  • 最左前缀匹配原则(经常使用的列优先)
  • 离散度高的列优先
  • 宽度小的列优先
最左前缀匹配原则

最左前缀匹配原则和联合索引的索引构建方式及存储结构是有关系的。根据上述理解分析,可以得出联合索引只能从多列索引的第一列开始查找索引才会生效,比如:

假设表user上有个联合索引(a,b,c),那么 select * from user where b = 1 and c = 2将不会命中索引

原因是联合索引的是存储引擎先按第一个字段排序,再按第二个字段排序,依次排序。

离散度

当索引中的一列离散度过低时,优化器可能直接不走索引,离散度计算方法:

离散度 = 列中不重复的数据量 / 这一列的总数据量

覆盖索引

如果一个索引包含(或覆盖)所有需要查询的字段的值,称为覆盖索,即只需扫描索引而无须回表查询 。覆盖索引可减少数据库IO,将随机IO变为顺序IO,可提高查询性能。

对于InnoDB辅助索引在叶子节点中保存了行的主键值,所以如果辅助索引(包括联合索引)能够覆盖查询,则可以避免对主键索引的二次查询。比如:

--创建联合索引
create index name_phone_idx on user(name,phoneNum);
--此时是覆盖索引,原因是根据name来查,命中索引name_phone_idx,
--其关键字为name,phoneNum,本身就已经包含了查询的列。
select name,phoneNum where name = "张三";  
--如果id为主键的话,此时也称作覆盖索引,原因:辅助索引的叶子节点存的就是主键
select id,name,phoneNum where name = "张三"; 

总结

MySQL的索引有很多知识点要掌握,已学习了索引的底层存储结构,不同存储引擎中的索引体现,以及索引类型的基础原理知识分析,可以为后续的数据库优化提供理论知识的支撑,也会更好的理解优化方案。后续会有优化篇章

谢谢各位点赞,没点赞的点个赞支持支持
最后,微信搜《Ccww技术博客》观看更多文章,也欢迎关注一波。

https://segmentfault.com/a/1190000038346710

Hyperf 发布 v2.0.21 版本,企业级的 PHP 微服务云原生协程框架

lei阅读(2)

更新内容

本周主要新增了部分特性,并修复了一些组件的 🐛Bug,继续提升 Hyperf 的稳定性,发布于 2.0.21 版。

建议用户使用以下命令更新此版本。

composer update "hyperf/*" -o

直接访问 官网 hyperf.io 或 文档 hyperf.wiki 查看更新内容

新增

  • #2857service-governance 组件新增 ConsulACL Token 支持。
  • #2870 为脚本 vendor:publish 支持发布配置目录的能力。
  • #2875watcher 组件新增可选项 no-restart,允许动态修改注解缓存,但不重启服务。
  • #2883scout 组件数据导入脚本,增加可选项 --chunk--column|c,允许用户指定任一字段,进行数据插入,解决偏移量过大导致查询效率慢的问题。
  • #2891crontab 组件新增可用于发布的配置文件。

修复

  • #2874 修复在使用 watcher 组件时, scan.ignore_annotations 配置不生效的问题。
  • #2878 修复 nsq 组件中,nsqd 配置无法正常工作的问题。

变更

  • #2851 修改 view 组件默认的配置文件,使用 view-engine 引擎,而非第三方 blade 引擎。

优化

  • #2785 优化 watcher 组件,使其异常信息更加人性化。
  • #2861 优化 Guzzle Coroutine Handler,当其 statusCode 小于 0 时,抛出对应异常。
  • #2868 优化 Guzzlesink 配置,使其支持传入 resource

关于 Hyperf

Hyperf 是基于 Swoole 4.5+ 实现的高性能、高灵活性的 PHP 协程框架,内置协程服务器及大量常用的组件,性能较传统基于 PHP-FPM 的框架有质的提升,提供超高性能的同时,也保持着极其灵活的可扩展性,标准组件均基于 PSR 标准 实现,基于强大的依赖注入设计,保证了绝大部分组件或类都是 可替换可复用 的。

框架组件库除了常见的协程版的 MySQL 客户端Redis 客户端,还为您准备了协程版的 Eloquent ORMWebSocket 服务端及客户端JSON RPC 服务端及客户端GRPC 服务端及客户端OpenTracing(Zipkin, Jaeger) 客户端Guzzle HTTP 客户端Elasticsearch 客户端Consul、Nacos 服务中心ETCD 客户端AMQP 组件Nats 组件Apollo、ETCD、Zookeeper、Nacos 和阿里云 ACM 的配置中心基于令牌桶算法的限流器通用连接池熔断器Swagger 文档生成Swoole TrackerBlade、Smarty、Twig、Plates 和 ThinkTemplate 视图引擎Snowflake 全局ID生成器Prometheus 服务监控 等组件,省去了自己实现对应协程版本的麻烦。

Hyperf 还提供了 基于 PSR-11 的依赖注入容器注解AOP 面向切面编程基于 PSR-15 的中间件自定义进程基于 PSR-14 的事件管理器Redis/RabbitMQ 消息队列自动模型缓存基于 PSR-16 的缓存Crontab 秒级定时任务Sessioni18n 国际化Validation 表单验证 等非常便捷的功能,满足丰富的技术场景和业务场景,开箱即用。

框架初衷

尽管现在基于 PHP 语言开发的框架处于一个百花争鸣的时代,但仍旧未能看到一个优雅的设计与超高性能的共存的完美框架,亦没有看到一个真正为 PHP 微服务铺路的框架,此为 Hyperf 及其团队成员的初衷,我们将持续投入并为此付出努力,也欢迎你加入我们参与开源建设。

设计理念

Hyperspeed + Flexibility = Hyperf,从名字上我们就将 超高速灵活性 作为 Hyperf 的基因。

  • 对于超高速,我们基于 Swoole 协程并在框架设计上进行大量的优化以确保超高性能的输出。
  • 对于灵活性,我们基于 Hyperf 强大的依赖注入组件,组件均基于 PSR 标准 的契约和由 Hyperf 定义的契约实现,达到框架内的绝大部分的组件或类都是可替换的。

基于以上的特点,Hyperf 将存在丰富的可能性,如实现 单体 Web 服务,API 服务,网关服务,分布式中间件,微服务架构,游戏服务器,物联网(IOT)等。

文档齐全

我们投入了大量的时间用于文档的建设以提供高质量的文档体验,以解决各种因为文档缺失所带来的问题,文档上也提供了大量的示例,对新手同样友好。
Hyperf 官方开发文档

生产可用

我们为组件进行了大量的单元测试以保证逻辑的正确,目前存在 1602 个单测共 4944 个断言条件,Hyperf 是一款经历过严酷的生产环境考验的一个项目,目前已有很多的大型互联网企业都已将 Hyperf 部署到了自己的生产环境上并稳定运行。

官网及交流

Github 👈👈👈👈👈 点 Star 支持我们
Gitee 码云 👈👈👈👈👈 点 Star 支持我们
Hyperf 官网
Hyperf 文档
Hyperf 交流群(已满): 862099724
Hyperf 交流 2 群: 811414891
钉钉群 一群(已满): 34538367
钉钉群 二群: 34488757

https://segmentfault.com/a/1190000038346119

读者上岸百度经验分享(上)

lei阅读(2)

读者准备面试的时间是 3 个月左右。但是,不是仅仅用 3 个月就能上岸大厂,之前也有计算机基础(网络、数据结构、操作系统、数据库、计组、微机原理等)。

前段时间,贾哥在星球向我询问 offer 选择的问题,我才知道贾哥已经斩获两个还不错的 offer。

贾哥和我一样都是双非本科,学历上面我们和大部分一样都没有任何优势。他的校招经历挺波折的,非常有参考价值。

于是,我就找到贾哥让他写一篇文章分享一下自己秋招的一些准备面试的经历以及经验。

贾哥写的太用心了,整篇文章大概有 1w+字。我将分为两次来发。觉得内容不错的话,大家记得点赞催更。

希望贾哥的分享对小伙伴们有帮助!

01

秋招这一路跌跌撞撞的走来,经历了很多心酸,也成长了很多。

从信心满满的开始,到不断地自我怀疑。从一个一无所知的菜鸡,到现在还是一个菜鸟。

我或许没有很多成功的逆袭经验来分享给大家。但是!我从一个秋招的裸奔男孩到理想上岸,收获的更多是失败的经验、成长的阅历和人生的考验吧!

我对计算机并没有激情满满的热爱,更多的是随着投入的时间和学习而产生的兴趣吧!

我是一个普通的不能在普通的大学生:双非本科,没有任何实习经历、比赛经历。

作为一个计算机学子,我大一大二几乎不知道自己将来会选择编程开发……

听过很多秋招大佬的传奇逆袭经历,向往他们将热爱都投身到刷力扣的成就感中,羡慕他们在秋招时斩获大把 Offer。

社会遵循着 2-8 原则,我或许应该被归到 8 这一类当中。我有时在不断问自己,你真的适合开发这一行吗?你会在这条路上走多远呀?评估自己的实力与大佬们的差距,可能就是我学习的动力吧!

作为一个被秋招毒打的打工人,我想和大家分享我的经历!

02

带着高考的些许遗憾,我来到了我的母校,西安某不知名双非一本,专业为数字媒体技术。

这个专业虽然归类在计算机学院下,但是我们的课程方向是游戏动画,影视建模方向。

导致每次面试官问我专业,我都要解释一遍,我是计算机专业的,计算机的公共基础课(数据结构、计算机网络等)我们都会学。

我们的就业方向貌似更加偏向新媒体方向,虽然编程知识也会学,甚至还学了那本西瓜书的《机器学习》。

大学前两年,自己就是一种浑浑噩噩的状态。我没有很明确的目标和方向,每天都是在宿舍-食堂-教室,上好该上的课。

曾经想拿个综测的专业第一,但是好像光靠成绩还是不够的,后来标准降到了考试尽力考个高分就行。

对于学习数据结构、操作系统等等计算机专业课程,我有一个深深的感触:考试分数高不代表你真的“学会了” 。

这些基础课程,我基本都是上课认真听听,考前复习半个月,拿个不错的分数过了,感觉任务就完成了。

现在熬夜补这些知识的时候,眼里都是悔恨的泪水呀 🥺。

大三,才意识到自己马上要毕业了,考虑了一个月,放弃考研的打算。我想了很久很久,感觉还是做一个打工人吧!

C/C++中的指针让我头晕眼花,于是我选择了 Java。

2019 年 10 月,开始了自己在大学里,真正有目标,有动力的去学习!

在一个失眠焦虑的夜晚,我写下这段话来激励自己:

今年在综测时,拿到了专业第一,可以申请保研(我校保研一般只能保本校)。也动摇过,秋招真的太难了,要不就放弃吧。但是想到自己大三时立下的雄心壮志,既然选择了这条路,就一抹黑的走下去吧,秋招不上岸,春招还能搏一把;这条路实在走不通,那我就考研!

然后,我就开始在 B 站、慕课网、油管、MOOC 上找 Java 的视频学习。

从 JavaSE、JavaWeb、框架的学习。2020 年 2 月份,似乎感觉,把这些内容都过了一遍。

期间一边看网课、博客文章、Guide 哥的专栏总结,一边写博客加深理解。寒假租了房,每天按部就班的输入,过年前几天才回家。过年那天晚上,都是一边看春晚,一边在复习。

03

到 3 月份,认识的几个同学开始投滴滴、百度的实习,我才开始写简历,到牛客看面经,也准备投实习。但是,看到面经的各种提问,我感觉自己像没学一样,全都是知识盲区。

了解的东西不够深入,到不了面试那种深层次提问,还有数据结构、网络、操作系统这些都没怎么复习。自己学过的这些课,脑海里仅仅残留着一点点印象。

更关键的是,我简历写完了技能列表,项目实在没得可写。面对空白乏力的简历,我感觉自己还有好多好多知识要补,完全就是在精卫填海。

本来打算过完年早早去出租屋里学习,年前就定了正月除六的车票打算赶过去。但是,突如其来的疫情,只能让我待在家里,打乱了我安排好的学习计划。

每天,面对面经上满满的知识盲区,自己在家里的效率又比较低,开学又遥遥无期,学习计划一拖再拖。

同时,我的两位伙伴在 5 月都去到了北京实习,我还在家里天天感觉无所事事。

找实习已经是不可能了,只能直接秋招了。然而,项目经历还是空白,做过的课设项目含金量低,单纯的管理系统实在不想往简历上去写。

对比朋友每天大厂的实习日常,再看看自己的狼狈不堪。每天,整个人都有着巨大的心里压力和焦虑。学校在线的网课都是在后台静音放着,天天跑到教育厅下询问开学时间,“又是不开学的一天!哎,到底什么以后才能去学校呀!”。

那段时间,真的过得非常压抑,每天都是忐忑不安、内心焦躁。自己仿佛在一条漆黑的路上跌跌撞撞的走着,这条路没有光亮,没有尽头。

后来,心态渐渐放平,全国都在众志成城的抗击疫情,大家都在努力着。换个角度想想,自己最大的财富,不就是拥有健康吗?

为了赶上既定的任务安排,我只能每天早早起来学习,虽然中途可能被一些其他事情打断,但是用时间来弥补效率,一直复习到深夜。有时莫名感觉,自己 20 多年来,第一次真正的这么努力。

2020 年 6 月,我不顾我妈的劝阻,来到了西安,和好基友小贤租了间房。他也没有找到实习,我们都是共赴秋招的裸奔男孩,两个人开始做秋招的最后冲刺!

04

来到西安后,我便开始集中精力复习基础知识:

  • 把多线程、集合类相关的知识重头复习了一遍,专门针对这一块的面试提问看了很多文章;
  • 在 B 站刷了两遍宋红康老师讲的《JVM 从入门到精通》,真的良心推荐 👍,零零散散看了下《深入理解 Java 虚拟机》这本圣经;
  • 复习了一遍计算机网络,主要是针对 TCP-IP 体系结构、HTTP 协议,看着面经来复习知识点
  • 数据库只做了简单复习,基本的 SQL 能写出来,牛客做了些题

眼看秋招提前批已到来,而且没有笔试,对我来说是个莫大的机会。但是,由于自己项目还没整理,没有可写的内容到简历上。所以只能任之溜走了。

这是对 Guide 哥之前的一次提问,让我很清楚自己接下来的两个月该做什么!

05

7 月份的时候,自己的项目经历还是空白,导致简历一直没法完善。

于是我开始着手开始准备项目。顺带着晚上刷题。

学校稍微有代表性的一点就是老师指导我们组做了个国家级的大创项目,但是我负责前端相关的内容。课设都是很基础的类似新闻管理系统、学生管理系统,还有 Unity 做的两个游戏 Demo,实在没法往简历上写。自己学习的方向是后端,只能找有代表性的项目来做!

Github Star 了些 Java 相关的项目,但当我拉下代码导入,发现自己搞不懂有些地方为什么要这样写,项目的架构是怎么设计的?关键的技术点在哪里?可能出现什么问题?如何去改善?

因为这些问题搞不懂,吃不透,虽然简历上写的是你的项目,但面试官一问就被问住了,所以终究还是不属于你。

由于自己底子薄,框架探究没那么深入,自己虽然学了 SSM、SpringBoot 这些框架,但是也只是能简单上手使用下。当下也没时间来深入探究底层原理学习,只能停留在简单了解和使用上。开源项目我可能没法吃透,我需要找个视频教程跟着做,然后基于自己理解再做拓展。

我把 B 站所有有关 Java 的项目都找了一遍,搜索不同的关键字足足过了三遍进行筛选统计。我发现项目大体可以分为两大类:

  • 【原理性】:就是造轮子,对已有框架或者协议自己来做个实现;如 Guide 哥的 RPC 框架和 HTTP 的轻量级框架,其他的如实现 Tomcat 功能、性能基准测试框架、实现网络协议等
  • 【功能性】:项目实现具体的业务功能;如各种权限管理系统、博客系统、商城、管理系统等。形式有前后端分离的,有基于微信小程序的后台的、还有客户端的

筛选了大概一周,我找到了适合自己的项目。一个是基于自己之前练手的 Demo,跟着视频学习自己做了拓展,一个是前后端分离的项目。

项目没必要功能业务多么复杂,涉及的技术栈有多广,但是一定能够自己吃透,原理性、结构性的层面自己搞懂,还有一定要有亮点!

因为面试官想听的不是你做了什么,而是怎么去做的。就我而言,更多的是考察你发现问题、分析问题、解决问题的能力。即便项目本身简单,但是一些特殊情况要考虑到,为什么这么设计?出现问题了怎么改进?如何去完善?其他技术方式怎么实现?

在百度三面主管面时,全程都在问项目,大概问了 50min 之久。虽然我觉得准备时自己考虑的很周到了,但是毕竟没参加工作,很多问题根本不知道:

因为基于 WebSocket 协议做的聊天室,本身是应用层的协议,直接就用 TCP 来保证消息可靠传输,如果访问量大,为了高效可以改用 UDP。这个项目准备的重心没有放在网络层面,而是考虑到多线程下并发聊天,会存在线程安全的问题,准备了很多多线程相关的针对项目的改善、应对策略,消息存储发送。

但是面试官全程都在针对网络层面做拓展,我只能根据已有的知识和对自己项目的拓展了解做回答。面试结束,我感觉自己被按在地上摩擦,又限了入了深深的自我怀疑中~

06

到了 8 月份的时候,我才开始完善简历以及刷题。

我的简历大概前前后后改了十二版,最初是改简历的布局,内容块;后面就是字字斟酌,细微调整。

经常删删改改,一句话可能要思考好久;我把我掌握的知识点都很详细的列出来,虽然技能列表看起来很基础,但是我有自信对自己写的内容负责

小伙伴们一定要重视简历!多花点精力在完善简历上!

我的刷题大概从 6 月就已经开始,断断续续在 LeetCode 上刷一些题。在 8 月的时候,我开始每天集中抽出很多时间来刷题。

没错,大佬们天天坚持刷个一年半载,我 7、8 月才开始每天集中刷题。

我大三就意识到了刷题得重要性,因为做题能力差,报了蓝桥杯比赛没去。

既然意识到重要性,为什么不早点去每天坚持刷题呢?

我尝试过,最终放弃了。这么做可能更多是临时抱佛脚的心态,对刚做完的题有个印象。

对我来说,复习路上最大的阻碍就是刷题了,因为自己的代码能力实在太差了。

三月份,我大概做了半个月题。《剑指 Offer》上的常规题,我基本上就是半天一道题,因为自己做这些题实在是想不来,想半个小时尝试去解决,但大多时候都是“差一点”,或者思路正确但又不能用代码实现出来。然后看题解,看别人不同的解法,自己再独立写一遍。

因为时间紧任务重,半天能够让我复习好多知识点了,所以想等复习完提纲之后再来刷题。而且,关键是做的题目,当时感觉自己懂了、会了,但是过一段时间又忘了,只能隐约留下个解题思路,还是不能够独立 AC。

七月份,只能是逼着自己来。因为大厂太看重代码能力了,即便是我理论知识掌握的再好,笔试都过不了,根本没得机会去面试。

然后,就开始分类刷题。参考 labuladong 哥的刷题套路,weiwei 哥的刷题分类,小齐姐的刷题经验,剑指 OfferKrahets路飞哥的精彩题解,每天花 8 个小时左右刷题,复习数据结构。

一道单链表反转的题,我整整想了一天半才搞懂。该题下的所有题解全部看了一遍,包括公众号的一些文章。递归的解法,短短几句话,我始终无法理解。

小贤从 4 月份一直开始刷题,在这期间一直和小贤在一起复习。他是 C++方向,算法和代码能力很强,刷题方面我都是请教他的。

单链表递归解法,他画图整整给我解释了一个晚上,从斐波那契的递归,到链表的实现。第二天,我终于搞懂了,在力扣发布了自己写的最认真的一次题解。单链表反转,自己写了不下 20 遍了吧;这次,可能真的是永远记住了吧。

8 月份,小贤由于有事回家了。房间只剩我一个人,我和老板续了房租,继续备战秋招。

期间,刷题有任何问题,我都会立即给小贤打电话过去交流。

【刷题的误区】

开始,我觉得自己不是在刷题,而是不断地重复写,好像在“背代码”。因为有些题说思路,我能够很清晰的表达出来,做的多了发现解题的套路还是比较固定的(虽然也没做多少 🤔),但是到实际的动手写,又写不出来了。

针对这个问题,我也很痛苦。一方面觉得“背代码”很可耻,自己真的就这么差吗,做个简单题都写不出来吗?但是,我真的是没办法,只能用做的少,练得少来安慰自己。

就这样,每天逼着自己,刷了大概 170 题左右,每天将基础的八大排序写一遍

其实,前期的刷题,自己没见过没思路很正常,参考别人的题解,把这种解法引用到类似的题目上。就像写作文一样,针对不同问题有不同的模板,根据具体问题调整边界即可。我自己总结来说,就是两大因素:

  1. 针对不同问题求解的代码模板,要恰当灵活的应用(如双指针、滑窗、列表 DP 等)
  2. 代码熟练度。模板是基于代码的熟练度而存在的,就像写排序算法一样能够很快的写出来

但是,这个量还有我的认知,对秋招来说是远远不够的。这是一项长期的积累和训练,谁也不可能偷懒,达到立竿见影的效果。因此,在后来的秋招笔试中,我重重的摔了跟头 😭,这是可预见的。

听学姐说她们去年是互联网的寒冬,找工作难。今年,因为疫情的原因,仿佛一切都变得更难,竞争更加激烈。

八月,2020 年的秋招已正式开始,但是我还在刷题复习中,准备即将到来的“金九银十”。这份简历,整整迟投出一个月……

微信搜“JavaGuide”回复“计算机基础”即可获取图解计算机基础+个人原创的 Java 面试手册。

https://segmentfault.com/a/1190000038345706

教你如何理解CopyOnWriteArrayList写时复制容器

lei阅读(1)

# 写入时复制(CopyOnWrite)思想

写入时复制(CopyOnWrite,简称COW)思想是计算机程序设计领域中的一种优化策略。其核心思想是,如果有多个调用者(Callers)同时要求相同的资源(如内存或者是磁盘上的数据存储),他们会共同获取相同的指针指向相同的资源,直到某个调用者视图修改资源内容时,系统才会真正复制一份专用副本(private copy)给该调用者,而其他调用者所见到的最初的资源仍然保持不变。

这过程对其他的调用者都是透明的(transparently)。此做法主要的优点是如果调用者没有修改资源,就不会有副本(private copy)被创建,因此多个调用者只是读取操作时可以共享同一份资源。

# CopyOnWriteArrayList的实现原理

在使用CopyOnWriteArrayList之前,我们先阅读其源码了解下它是如何实现的。以下代码是向CopyOnWriteArrayList中add方法的实现(向CopyOnWriteArrayList里添加元素),可以发现在添加的时候是需要加锁的,否则多线程写的时候会Copy出N个副本出来。
《2020最新Java基础精讲视频教程和学习路线!》

/**
 * Appends the specified element to the end of this list. * * @param e element to be appended to this list * @return <tt>true</tt> (as specified by {@link Collection#add}) */
    public boolean add(E e) {
    final ReentrantLock lock = this.lock;
    lock.lock();
    try {
        Object[] elements = getArray();
        int len = elements.length;
        Object[] newElements = Arrays.copyOf(elements, len + 1);
        newElements[len] = e;
        setArray(newElements);
        return true;
    } finally {
        lock.unlock();
    }
    }

读的时候不需要加锁,如果读的时候有多个线程正在向CopyOnWriteArrayList添加数据,读还是会读到旧的数据,因为写的时候不会锁住旧的CopyOnWriteArrayList。

public E get(int index) {
    return get(getArray(), index);
}

JDK中并没有提供CopyOnWriteMap,我们可以参考CopyOnWriteArrayList来实现一个,基本代码如下:

import java.util.Collection;
import java.util.Map;
import java.util.Set;
 
public class CopyOnWriteMap<K, V> implements Map<K, V>, Cloneable {
    private volatile Map<K, V> internalMap;
 
    public CopyOnWriteMap() {
        internalMap = new HashMap<K, V>();
    }
 
    public V put(K key, V value) {
 
        synchronized (this) {
            Map<K, V> newMap = new HashMap<K, V>(internalMap);
            V val = newMap.put(key, value);
            internalMap = newMap;
            return val;
        }
    }
 
    public V get(Object key) {
        return internalMap.get(key);
    }
 
    public void putAll(Map<? extends K, ? extends V> newData) {
        synchronized (this) {
            Map<K, V> newMap = new HashMap<K, V>(internalMap);
            newMap.putAll(newData);
            internalMap = newMap;
        }
    }
}

实现很简单,只要了解了CopyOnWrite机制,我们可以实现各种CopyOnWrite容器,并且在不同的应用场景中使用。

# 几个要点

  • 实现了List接口
  • 内部持有一个ReentrantLock lock = new ReentrantLock();
  • 底层是用volatile transient声明的数组 array
  • 读写分离,写时复制出一个新的数组,完成插入、修改或者移除操作后将新数组赋值给array

「注:」

volatile (挥发物、易变的):变量修饰符,只能用来修饰变量。volatile修饰的成员变量在每次被线程访问时,都强迫从共享内存中重读该成员变量的值。而且,当成员变量发生变 化时,强迫线程将变化值回写到共享内存。这样在任何时刻,两个不同的线程总是看到某个成员变量的同一个值。

# 增删改查

1)增

public boolean add(E e) {
    final ReentrantLock lock = this.lock;
    //获得锁
    lock.lock();
    try {
        Object[] elements = getArray();
        int len = elements.length;
        //复制一个新的数组
        Object[] newElements = Arrays.copyOf(elements, len + 1);
        //插入新值
        newElements[len] = e;
        //将新的数组指向原来的引用
        setArray(newElements);
        return true;
    } finally {
        //释放锁
        lock.unlock();
    }
}

   
public void add(int index, E element) {
    final ReentrantLock lock = this.lock;
    lock.lock();
    try {
        Object[] elements = getArray();
        int len = elements.length;
        if (index > len || index < 0)
            throw new IndexOutOfBoundsException("Index: "+index+
                                                ", Size: "+len);
        Object[] newElements;
        int numMoved = len - index;
        if (numMoved == 0)
            newElements = Arrays.copyOf(elements, len + 1);
        else {
            newElements = new Object[len + 1];
            System.arraycopy(elements, 0, newElements, 0, index);
            System.arraycopy(elements, index, newElements, index + 1,
                             numMoved);
        }
        newElements[index] = element;
        setArray(newElements);
    } finally {
        lock.unlock();
    }
}

2)删

public E remove(int index) {
    final ReentrantLock lock = this.lock;
    //获得锁
    lock.lock();
    try {
        Object[] elements = getArray();
        int len = elements.length;
        E oldValue = get(elements, index);
        int numMoved = len - index - 1;
        if (numMoved == 0)
            //如果删除的元素是最后一个,直接复制该元素前的所有元素到新的数组
            setArray(Arrays.copyOf(elements, len - 1));
        else {
            //创建新的数组
            Object[] newElements = new Object[len - 1];
            //将index+1至最后一个元素向前移动一格
            System.arraycopy(elements, 0, newElements, 0, index);
            System.arraycopy(elements, index + 1, newElements, index,
                             numMoved);
            setArray(newElements);
        }
        return oldValue;
    } finally {
        lock.unlock();
    }
}

3)改

public E set(int index, E element) {
    final ReentrantLock lock = this.lock;
    //获得锁
    lock.lock();
    try {
        Object[] elements = getArray();
        E oldValue = get(elements, index);

        if (oldValue != element) {
            int len = elements.length;
            //创建新数组
            Object[] newElements = Arrays.copyOf(elements, len);
            //替换元素
            newElements[index] = element;
            //将新数组指向原来的引用
            setArray(newElements);
        } else {
            // Not quite a no-op; ensures volatile write semantics
            setArray(elements);
        }
        return oldValue;
    } finally {
        //释放锁
        lock.unlock();
    }
}

4)查

//直接获取index对应的元素
public E get(int index) {return get(getArray(), index);}
private E get(Object[] a, int index) {return (E) a[index];}

# CopyOnWrite的应用场景

CopyOnWrite并发容器用于读多写少的并发场景。比如白名单,黑名单,商品类目的访问和更新场景,假如我们有一个搜索网站,用户在这个网站的搜索框中,输入关键字搜索内容,但是某些关键字不允许被搜索。这些不能被搜索的关键字会被放在一个黑名单当中,黑名单每天晚上更新一次。当用户搜索时,会检查当前关键字在不在黑名单当中,如果在,则提示不能搜索。实现代码如下:

import java.util.Map;
 
import com.ifeve.book.forkjoin.CopyOnWriteMap;
 
/**
 * 黑名单服务
 *
 * @author fangtengfei
 *
 */
public class BlackListServiceImpl {
 
    private static CopyOnWriteMap<String, Boolean> blackListMap = new CopyOnWriteMap<String, Boolean>(
            1000);
 
    public static boolean isBlackList(String id) {
        return blackListMap.get(id) == null ? false : true;
    }
 
    public static void addBlackList(String id) {
        blackListMap.put(id, Boolean.TRUE);
    }
 
    /**
     * 批量添加黑名单
     *
     * @param ids
     */
    public static void addBlackList(Map<String,Boolean> ids) {
        blackListMap.putAll(ids);
    }
 
}

代码很简单,但是使用CopyOnWriteMap需要注意两件事情:

  1. 减少扩容开销。根据实际需要,初始化CopyOnWriteMap的大小,避免写时CopyOnWriteMap扩容的开销。
  2. 使用批量添加。因为每次添加,容器每次都会进行复制,所以减少添加次数,可以减少容器的复制次数。如使用上面代码里的addBlackList方法。

# CopyOnWrite的缺点

CopyOnWrite容器有很多优点,但是同时也存在两个问题,即内存占用问题和数据一致性问题。所以在开发的时候需要注意一下。

「内存占用问题」。因为CopyOnWrite的写时复制机制,所以在进行写操作的时候,内存里会同时驻扎两个对象的内存,旧的对象和新写入的对象(注意:在复制的时候只是复制容器里的引用,只是在写的时候会创建新对象添加到新容器里,而旧容器的对象还在使用,所以有两份对象内存)。如果这些对象占用的内存比较大,比如说200M左右,那么再写入100M数据进去,内存就会占用300M,那么这个时候很有可能造成频繁的Yong GC和Full GC。之前我们系统中使用了一个服务由于每晚使用CopyOnWrite机制更新大对象,造成了每晚15秒的Full GC,应用响应时间也随之变长。

「针对内存占用问题」,可以通过压缩容器中的元素的方法来减少大对象的内存消耗,比如,如果元素全是10进制的数字,可以考虑把它压缩成36进制或64进制。或者不使用CopyOnWrite容器,而使用其他的并发容器,如ConcurrentHashMap。

「数据一致性问题」。CopyOnWrite容器只能保证数据的最终一致性,不能保证数据的实时一致性。所以如果你希望写入的的数据,马上能读到,请不要使用CopyOnWrite容器。

# CopyOnWriteArrayList为什么并发安全且性能比Vector好

我知道Vector是增删改查方法都加了synchronized,保证同步,但是每个方法执行的时候都要去获得锁,性能就会大大下降,而CopyOnWriteArrayList 只是在增删改上加锁,但是读不加锁,在读方面的性能就好于Vector,CopyOnWriteArrayList支持读多写少的并发情况。

链接 :https://www.cnblogs.com/myseries/p/10877420.html

https://segmentfault.com/a/1190000038343207

Mysql普通索引和唯一索引的选择分析

lei阅读(2)

假设一个用户管理系统,每个人注册都有一个唯一的手机号,而且业务代码已经保证了不会写入两个重复的手机号。如果用户管理系统需要按照手机号查姓名,就会执行类似这样的 SQL 语句:

select name from users where mobile = '15202124529';

通常会考虑在 mobile 字段上建索引。由于手机号字段相对较大,通常基本不会把手机号当做主键,那么现在就有两个选择:

1.  给 id_card 字段创建唯一索引
2.  创建一个普通索引

如果业务代码已经保证了不会写入重复的身份证号,那么这两个选择逻辑上都是正确的。

从性能的角度考虑,选择唯一索引还是普通索引?

如图:假设字段 k 上的值都不重复
image

接下来,就从这两种(ID,k)索引对查询语句和更新语句的性能影响来进行分析

查询过程

假设,执行查询的语句是 select id from T where k=5。这个查询语句在索引树上查找的过程,先是通过 B+ 树从树根开始,按层搜索到叶子节点,也就是图中右下角的这个数据页,然后可以认为数据页内部通过二分法来定位记录(数据页内部通过有序数组保存节点。数据页之间通过双向链表串接)。

  • 对于普通索引来说,查找到满足条件的第一个记录 (5,500) 后,需要查找下一个记录,直到碰到第一个不满足 k=5 条件的记录。
  • 对于唯一索引来说,由于索引定义了唯一性,查找到第一个满足条件的记录后,就会停止继续检索。

那么,这个不同带来的性能差距会有多少呢?答案是,微乎其微

原因:除非 Key 的列非常大,有连续多个 Key 占满了一个 page,才会引起一次 page 的 IO,这样才会产生比较明显的性能差异,从均摊上看,差异几乎可以不算。

InnoDB 的数据是按数据页为单位来读写的。也就是说,当需要读一条记录的时候,并不是将这个记录本身从磁盘读出来,而是以页为单位,将其整体读入内存。在 InnoDB 中,每个数据页的大小默认是 16KB。

更新过程

为了说明普通索引和唯一索引对更新语句性能的影响这个问题,需要先介绍一下 change buffer

  • 当需要更新一个数据页时,如果数据页在内存中就直接更新,
  • 而如果这个数据页还没有在内存中的话,在不影响数据一致性的前提下:
  1.  InnoDB 会将这些 更新操作 缓存在 change buffer 中,这样就不需要从磁盘中读入这个数据页了。
  2. 在下次查询需要访问这个数据页的时候,将数据页读入内存,
  3. 然后执行 change buffer 中与这个页有关的操作。

    通过这种方式就能保证这个数据逻辑的正确性

需要说明的是,虽然名字叫作 change buffer,实际上它是可以持久化的数据。也就是说,change buffer 在内存中有拷贝,也会被写入到磁盘上。

把change buffer中的操作,应用到旧的数据页,得到新的数据页的过程,应该称为merge。

Ps.  除了访问这个数据页会触发 merge 外,系统有后台线程会定期 merge。在数据库正常关闭(shutdown)的过程中,也会执行 merge 操作。

(change buffer的merge操作,先把change buffer的操作更新到内存的数据页中,此操作写到redo log中,mysql未宕机,redo log写满后需要移动check point点时,通过判断内存中数据和磁盘是否一致即是否是脏页来刷新到磁盘中,当mysql宕机后没有内存即没有脏页,通过redo log来恢复。)

显然,如果能够将更新操作先记录在 change buffer,减少读磁盘,语句的执行速度会得到明显的提升。

而且,数据读入内存是需要占用 buffer pool 的,所以这种方式还能够避免占用内存,提高内存利用率。

什么条件下可以使用 change buffer 呢?

对于唯一索引来说,所有的更新操作都要先判断这个操作是否违反唯一性约束。

比如,要插入 (4,400) 这个记录,就要先判断现在表中是否已经存在 k=4 的记录,而这必须要将数据页读入内存才能判断。

如果都已经读入到内存了,那直接更新内存会更快,就没必要使用 change buffer 了。

因此,唯一索引的更新就不能使用 change buffer,实际上也只有普通索引可以使用。

change buffer 用的是 buffer pool 里的内存,因此不能无限增大。change buffer 的大小,可以通过参数 innodb_change_buffer_max_size 来动态设置。这个参数设置为 50 的时候,表示 change buffer 的大小最多只能占用 buffer pool 的 50%。

image

Ps. 数据库缓冲池(buffer pool) https://www.jianshu.com/p/f9ab1cb24230

分析:插入一个新记录 InnoDB 的处理流程

理解了 change buffer 的机制,那么如果要在这张表中插入一个新记录 (4,400) 的话,InnoDB 的处理流程是怎样的

1、第一种情况是:这个记录要更新的目标页在内存中。

  •  这时,InnoDB 的处理流程如下:对于唯一索引来说,找到 3 和 5 之间的位置,判断到没有冲突,插入这个值,语句执行结束;
  • 对于普通索引来说,找到 3 和 5 之间的位置,插入这个值,语句执行结束。

这样看来,普通索引和唯一索引对更新语句性能影响的差别,只是一个判断,只会耗费微小的 CPU 时间。但,这不是关注的重点

2、第二种情况是,这个记录要更新的目标页不在内存中。这时,InnoDB 的处理流程如下:

  • 对于唯一索引来说,需要将数据页读入内存,判断到没有冲突,插入这个值,语句执行结束;
  • 对于普通索引来说,则是将更新记录在 change buffer,语句执行就结束了。

将数据从磁盘读入内存涉及随机 IO 的访问,是数据库里面成本最高的操作之一。change buffer 因为减少了随机磁盘访问,所以对更新性能的提升是会很明显的。

change buffer主要是将更新操作缓存起来,异步处理. 这样每次更新过来,直接记下change buffer即可,速度很快,将多次写磁盘变为一次写磁盘

change buffer 的使用场景

通过上面的分析,已经清楚了使用 change buffer 对更新过程的加速作用,也清楚了 change buffer 只限于用在普通索引的场景下,而不适用于唯一索引。

普通索引的所有场景,使用 change buffer 都可以起到加速作用吗?

因为 merge 的时候是真正进行数据更新的时刻,而 change buffer 的主要目的就是将记录的变更动作缓存下来,所以在一个数据页做 merge 之前,change buffer 记录的变更越多(也就是这个页面上要更新的次数越多),收益就越大。

因此,对于写多读少的业务来说,页面在写完以后马上被访问到的概率比较小,此时 change buffer 的使用效果最好。这种业务模型常见的就是账单类、日志类的系统。(适合写多读少的场景,读多写少反倒会增加change buffer的维护代价)

反过来,假设一个业务的更新模式是写入之后马上会做查询,那么即使满足了条件,将更新先记录在 change buffer,但之后由于马上要访问这个数据页,会立即触发 merge 过程。这样随机访问 IO 的次数不会减少,反而增加了 change buffer 的维护代价。所以,对于这种业务模式来说,change buffer 反而起到了副作用。(如果立即对普通索引的更新操作结果执行查询,就会触发merge操作,磁盘中的数据会和change buffer 的操作记录进行合并,产生大量io)

索引选择和实践

综上分析,普通索引和唯一索引应该怎么选择:

其实,这两类索引在查询能力上是没差别的,主要考虑的是对更新性能的影响。所以,建议尽量选择普通索引

如果所有的更新后面,都马上伴随着对这个记录的查询,那么应该关闭 change buffer。

而在其他情况下,change buffer 都能提升更新性能。在实际使用中,普通索引和 change buffer 的配合使用,对于数据量大的表的更新优化还是很明显的。

Ps. 特别地,在使用机械硬盘时,change buffer 这个机制的收效是非常显著的。所以,当有一个类似“历史数据”的库,应该特别关注这些表里的索引,尽量使用普通索引,然后把 change buffer 尽量开大,以确保这个“历史数据”表的数据写入速度。

change buffer 和 redo log

理解了 change buffer 的原理,可能会联想到 redo log 和 WAL(Write-Ahead Logging,它的关键点就是先写日志,再写磁盘)。

WAL 提升性能的核心机制,也的确是尽量减少随机读写

在表上执行这个插入语句:

mysql> insert into t(id,k) values(id1,k1),(id2,k2);

假设当前 k 索引树的状态,查找到位置后,k1 所在的数据页在内存 (InnoDB buffer pool) 中,k2 所在的数据页不在内存中。如图 是带 change buffer 的更新状态图。

image

图3  带 change buffer 的更新过程

分析这条更新语句,你会发现它涉及了四个部分:

内存、redo log(ib_log_fileX)、 数据表空间(t.ibd)、系统表空间(ibdata1)。

数据表空间:就是一个个的表数据文件,对应的磁盘文件就是“表名.ibd”; 系统表空间:用来放系统信息,如数据字典等,对应的磁盘文件是“ibdata1”

数据表空间 和 系统表空间 似乎代表的就是B+树对应的那个复杂的结构

这条更新语句做了如下的操作(按照图中的数字顺序):

  1. Page 1 在内存中,直接更新内存;
  2. Page 2 没有在内存中,就在内存的 change buffer 区域,记录下“我要往 Page 2 插入一行”
  3. 这个信息将上述两个动作记入 redo log 中(图中 3 和 4)。

做完上面这些,事务就可以完成了。所以,你会看到,执行这条更新语句的成本很低,就是写了两处内存,然后写了一处磁盘(两次操作合在一起写了一次磁盘),而且还是顺序写的。

change buffer和redo log颗粒度不一样,因为change buffer只是针对如果更改的数据所在页不在内存中才暂时储存在change buffer中。而redo log会记录一个事务内进行数据更改的所有操作,即使修改的数据已经在内存中了,那也会记录下来

同时,图中的两个虚线箭头,是后台操作,不影响更新的响应时间。

那在这之后的读请求,要怎么处理呢?

比如,我们现在要执行 select * from t where k in (k1, k2)。

如果读语句发生在更新语句后不久,内存中的数据都还在,那么此时的这两个读操作就与系统表空间(ibdata1)和 redo log(ib_log_fileX)无关了。

image

图 4 带 change buffer 的读过程

从图中可以看到:读 Page 1 的时候,直接从内存返回。

WAL 之后如果读数据,是不是一定要读盘,是不是一定要从 redo log 里面把数据更新以后才可以返回?

其实是不用的。虽然磁盘上还是之前的数据,但是这里直接从内存返回结果,结果是正确的。要读 Page 2 的时候,需要把 Page 2 从磁盘读入内存中,然后应用 change buffer 里面的操作日志,生成一个正确的版本并返回结果。可以看到,直到需要读 Page 2 的时候,这个数据页才会被读入内存。

如果要简单地对比这两个机制在提升更新性能上的收益的话,redo log 主要节省的是随机写磁盘的 IO 消耗(转成顺序写),而 change buffer 主要节省的则是随机读磁盘的 IO 消耗。

思考题:

1、通过图 3 可以看到,change buffer 一开始是写内存的,那么如果这个时候机器掉电重启,会不会导致 change buffer 丢失呢?change buffer 丢失可不是小事儿,再从磁盘读入数据可就没有了 merge 过程,就等于是数据丢失了。会不会出现这种情况呢?

答:

1.change buffer有一部分在内存有一部分在ibdata.

做purge操作,应该就会把change buffer里相应的数据持久化到ibdata

2.redo log里记录了数据页的修改以及change buffer新写入的信息

如果掉电,持久化的change buffer数据已经purge,不用恢复。主要分析没有持久化的数据

情况又分为以下几种:

(1)change buffer写入,redo log虽然做了fsync但未commit,binlog未fsync到磁盘,这部分数据丢失

(2)change buffer写入,redo log写入但没有commit,binlog以及fsync到磁盘,先从binlog恢复redo log,再从redo log恢复change buffer

(3)change buffer写入,redo log和binlog都已经fsync.那么直接从redo log里恢复。

https://segmentfault.com/a/1190000038321537

grpc-node 源码阅读笔记[0]

lei阅读(2)

简单介绍 gRPC

贴一张挂在官网的图片:https://grpc.io/docs/what-is-…

image

可以理解 gRPC 是 RPC(远程过程调用)框架的一种实现,关于 RPC 的介绍因为并不是本次的主题,所以放个链接来帮助大家理解:https://www.zhihu.com/questio…

我所理解 RPC 整个执行的过程就是 Client 调用方法 -> 序列化请求参数 -> 传输数据 -> 反序列化请求参数 -> Server 处理请求 -> 序列化返回数据 -> 传输数据 -> Client 接收到方法返回值:

image

其主要逻辑会集中在 数据的序列化/反序列化 以及 数据的传输上,而这两项 gRPC 分别选用了 Protocol BuffersHTTP2 来作为默认选项。

gRPC 在 Node.js 的实现

gRPC 在 Node.js 的实现上一共有两个官方版本,一个是基于 c++ addon 的版本,另一个是纯 JS 实现的版本

gRPC 在 Node.js 中相关的模块

除了上边提到的两个 gRPC 的实现,在 Node.js 中还存在一些其他的模块用来辅助使用 gRPC。

  • grpc-tools 这个是每个语言都会用的,用来根据 proto 文件生成对应,插件提供了 Node.js 语言的实现
  • proto-loader 用来动态加载 proto 文件,不需要使用 grpc_tools 提前生成代码(性能比上边的方式稍差)

这次笔记主要是针对 grpc-node 方式的实现,在 c++ addon 模块的实现下,并不是一个 gRPC 的完整实现,做的事情更多的是一个衔接的工作,通过 JS、c++ 两层封装将 c++ 版本的 gRPC 能力暴露出来供用户使用。

之所以选择它是因为觉得逻辑会较 grpc-js 清晰一些,更适合理解 gRPC 整体的运行逻辑。

在项目仓库中,两个目录下是我们需要关注的:

  • src(JS 代码)
  • ext(c++ 代码)

ext 中的代码主要用于调用 c++ 版本 gRPC 的接口,并通过 NAN 提供 c++ addon 模块。
src 中的代码则是调用了 ext 编译后的模块,并进行一层应用上的封装。
而作为使用 gRPC 的用户就是引用的 src 下的文件了。

我们先通过官方的 hello world 示例来说明我们是如何使用 gRPC 的,因为 gRPC 默认的数据序列化方式采用的 protobuf,所以首先我们需要有一个 proto 文件,然后通过 gRPC 提供的文件来生成对应的代码,生成出来的文件包含了 proto 中所定义的 service、method、message 等各种结构的定义,并能够让我们用比较熟悉的方式去使用。

示例中的 proto 文件:

package helloworld;

// The greeting service definition.
service Greeter {
  // Sends a greeting
  rpc SayHello (HelloRequest) returns (HelloReply) {}
}

// The request message containing the user's name.
message HelloRequest {
  string name = 1;
}

// The response message containing the greetings
message HelloReply {
  string message = 1;
}

grpc_tools 是用来生成 proto 对应代码的,这个命令行工具提供了多种语言的生成版本。
在 Node 中,会生成两个文件,一般命名规则为 xxx_pb.jsxxx_grpc_pb.jsxxx_pb.js 是 proto 中各种 service、method 以及 message 的结构描述及如何使用的接口定义,而 xxx_grpc_pb.js 主要则是针对 xxx_pb.js 的一个整合,按照 proto 文件中定义的结构生成对应的代码,在用户使用的时候,使用前者多半用于构造消息结构,使用后者则是方法的调用。

生成后的关键代码(XXX_grpc_pb.js):

const grpc = require('@grpc/grpc');
const helloworld_pb = require('./helloworld_pb.js');

function serialize_helloworld_HelloReply(arg) {
  if (!(arg instanceof helloworld_pb.HelloReply)) {
    throw new Error('Expected argument of type helloworld.HelloReply');
  }
  return Buffer.from(arg.serializeBinary());
}

function deserialize_helloworld_HelloReply(buffer_arg) {
  return helloworld_pb.HelloReply.deserializeBinary(new Uint8Array(buffer_arg));
}

function serialize_helloworld_HelloRequest(arg) {
  if (!(arg instanceof helloworld_pb.HelloRequest)) {
    throw new Error('Expected argument of type helloworld.HelloRequest');
  }
  return Buffer.from(arg.serializeBinary());
}

function deserialize_helloworld_HelloRequest(buffer_arg) {
  return helloworld_pb.HelloRequest.deserializeBinary(new Uint8Array(buffer_arg));
}


// The greeting service definition.
const GreeterService = exports.GreeterService = {
  // Sends a greeting
sayHello: {
    path: '/helloworld.Greeter/SayHello',
    requestStream: false,
    responseStream: false,
    requestType: helloworld_pb.HelloRequest,
    responseType: helloworld_pb.HelloReply,
    requestSerialize: serialize_helloworld_HelloRequest,
    requestDeserialize: deserialize_helloworld_HelloRequest,
    responseSerialize: serialize_helloworld_HelloReply,
    responseDeserialize: deserialize_helloworld_HelloReply,
  },
};

exports.GreeterClient = grpc.makeGenericClientConstructor(GreeterService);

最终导出的 sayHello 就是我们在 proto 文件中定义的 SayHello 方法,所以我们在作为 Client 的时候使用,就是很简单的调用 sayHello 就行了:

const messages = require('./helloworld_pb');
const services = require('./helloworld_grpc_pb');
const grpc = require('grpc');

const client = new services.GreeterClient(
  target,
  grpc.credentials.createInsecure()
);

const request = new messages.HelloRequest();

request.setName('Niko');

client.sayHello(request, function(err, response) {
  console.log('Greeting:', response.getMessage());
});

其实真实写的代码也就上边的几行,实例化了一个 Client,实例化一个 Message 并构建数据,然后通过 client 调用对应的 method 传入 message,就完成了一个 gRPC 请求的发送。
在这个过程中,我们直接可见的用到了 grpc-nodecredentials 以及 makeGenericClientConstructor,我们就拿这两个作为入口,首先从 makeGenericClientConstructor 来说。

源码分析

makeGenericClientConstructor

在翻看 index.js 文件中可以发现, makeGenericClientConstructor 其实是 client.makeClientConstructor 的一个别名,所以我们需要去查看 src/client.js 中对应函数的定义,就像函数名一样,它是用来生成一个 Client 的构造函数的,这个构造函数就是我们在上边示例中的 GreeterClient
源码所在位置: https://github.com/grpc/grpc-…

当对照着 xxx_grpc_pb.js 与源码来看时,会发现调用函数只传入了一个参数,而函数定义却存在三个参数,这个其实是历史原因导致的,我们可以直接忽略后边的两个参数。

精简后的源码:

exports.makeClientConstructor = function(methods) {
  function ServiceClient(address, credentials, options) {
    Client.call(this, address, credentials, options);
  }

  util.inherits(ServiceClient, Client);
  ServiceClient.prototype.$method_definitions = methods;
  ServiceClient.prototype.$method_names = {};

  Object.keys(methods).forEach(name => {
    const attrs = methods[name];
    if (name.indexOf('$') === 0) {
      throw new Error('Method names cannot start with $');
    }
    var method_type = common.getMethodType(attrs);
    var method_func = function() {
      return requester_funcs[method_type].apply(this,
        [ attrs.path, attrs.requestSerialize, attrs.responseDeserialize ]
        .concat([].slice.call(arguments))
      );
    };
    
    ServiceClient.prototype[name] = method_func;
    ServiceClient.prototype.$method_names[attrs.path] = name;
    // Associate all provided attributes with the method
    Object.assign(ServiceClient.prototype[name], attrs);
    if (attrs.originalName) {
      ServiceClient.prototype[attrs.originalName] =
        ServiceClient.prototype[name];
    }
  });

  ServiceClient.service = methods;

  return ServiceClient;
};

methods 参数就是我们上边文件中生成的对象,包括服务地址、是否使用 stream、以及 请求/返回值 的类型及对应的序列化/反序列化 方式。

大致的逻辑就是创建一个继承自 Client 的子类,然后遍历我们整个 service 来看里边有多少个 method,并根据 method 不同的传输类型来区分使用不同的函数进行数据的传输,最后以 method 为 key 放到 Client 子类的原型链上。

common.getMethodType 就是用来区分 method 究竟是什么类型的请求的,目前 gRPC 一共分了四种类型,双向 Stream、两个单向 Stream,以及 Unary 模式:

exports.getMethodType = function(method_definition) {
  if (method_definition.requestStream) {
    if (method_definition.responseStream) {
      return constants.methodTypes.BIDI_STREAMING;
    } else {
      return constants.methodTypes.CLIENT_STREAMING;
    }
  } else {
    if (method_definition.responseStream) {
      return constants.methodTypes.SERVER_STREAMING;
    } else {
      return constants.methodTypes.UNARY;
    }
  }
};

在最后几行有一处判断 originalName 是否存在的操作,这个是在 proto-loader 中存在的一个逻辑,将 methodName 转换成纯小写放了进去,单纯看注释的话,这并不是一个长期的解决方案: https://github.com/grpc/grpc-…

P.S. proto-loader 是 JS 里边一种动态加载 proto 文件的方式,性能比通过 grpc_tools 预生成代码的方式要低一些。

所有的请求方式,都被放在了一个叫做 requester_funcs 的对象中,源码中的定义是这样的:

var requester_funcs = {
  [methodTypes.UNARY]: Client.prototype.makeUnaryRequest,
  [methodTypes.CLIENT_STREAMING]: Client.prototype.makeClientStreamRequest,
  [methodTypes.SERVER_STREAMING]: Client.prototype.makeServerStreamRequest,
  [methodTypes.BIDI_STREAMING]: Client.prototype.makeBidiStreamRequest
};

从这里就可以看出,其实是和我们 getMethodType 所对应的四种处理方式。

最终,将继承自 Client 的子类返回,完成了整个函数的执行。

Client

首先我们需要看看继承的 Client 构造函数究竟做了什么事情。
抛开参数类型的检查,首先是针对拦截器的处理,我们可以通过两种方式来实现拦截器,一个是提供拦截器的具体函数,这个在所有 method 触发时都会执行,还有一个可以通过传入 interceptor_provider 来实现动态的生成拦截器,函数会在初始化 Client 的时候触发,并要求返回一个新的 interceptor 对象用于执行拦截器的逻辑。

interceptor 的用法

// interceptors 用法
const interceptor = function(options, nextCall) {
  console.log('trigger')
  return new InterceptingCall(nextCall(options));
}
const client = new services.GreeterClient(
  target,
  grpc.credentials.createInsecure(),
  {
    interceptors: [interceptor]
  }
);

// interceptor_providers 用法
const interceptor = function(options, nextCall) {
  console.log('trigger')
  return new InterceptingCall(nextCall(options));
}

const interceptorProvider = (methodDefinition) => {
  console.log('call interceptorProvider', methodDefinition)
  return interceptor
}

const client = new services.GreeterClient(
  target,
  grpc.credentials.createInsecure(),
  {
    interceptor_providers: [interceptorProvider]
  }
);

P.S. 需要注意的是,如果传入 interceptor_providers,则会在两个地方触发调用,一个是实例化 Client 的时候,还有一个是在 method 真实调用的时候,每次调用都会触发,所以如果要复用 interceptor,最好在函数之外构建出函数体

但是这样的拦截器其实是没有太多意义的,我们不能够针对 metadatamessage 来做自己的修改,如果我们观察 InterceptingCall 的具体函数签名,会发现它支持两个参数的传入。

function InterceptingCall(next_call, requester) {
  this.next_call = next_call;
  this.requester = requester;
}

上边示例只介绍了第一个参数,这个参数预期接受一个对象,对象会提供多个方法,我们可以通过console.log(nextCall(options).constructor.prototype)来查看都有哪些,例如 sendMessagestart 之类的。
而观察这些函数的实现,会发现他们都调用了一个 _callNext

InterceptingCall.prototype.sendMessage = function(message) {
  this._callNext('sendMessage', [message]);
};

InterceptingCall.prototype.halfClose = function() {
  this._callNext('halfClose');
};

InterceptingCall.prototype.cancel = function() {
  this._callNext('cancel');
};

InterceptingCall.prototype._callNext = function(method_name, args, next) {
  var args_array = args || [];
  var next_call = next ? next : this._getNextCall(method_name);
  if (this.requester && this.requester[method_name]) {
    // Avoid using expensive `apply` calls
    var num_args = args_array.length;
    switch (num_args) {
      case 0:
        return this.requester[method_name](next_call);
      case 1:
        return this.requester[method_name](args_array[0], next_call);
      case 2:
        return this.requester[method_name](args_array[0], args_array[1],
                                           next_call);
    }
  } else {
    if (next_call === emptyNext) {
      throw new Error('Interceptor call chain terminated unexpectedly');
    }
    return next_call(args_array[0], args_array[1]);
  }
};

_callNext 方法中,我们就可以找到 requester 参数究竟是有什么用了,如果 requester 也有实现对应的 method_name,那么就会先执行 requester 的方法,随后将 next_call 对应的方法作为调用 requester 方法的最后一个参数传入。
在 grpc-node 中,拦截器的执行顺序与传入顺序有关,是一个队列,先传入的拦截器先执行,如果传入了第二个参数,则先执行第二个参数对应的方法,后执行第一个参数对应的方法。

所以如果我们想做一些额外的事情,比如说针对 metadata 添加一个我们想要的字段,那么就可以这么来写拦截器:

var interceptor = function(options, nextCall) {
  return new InterceptingCall(nextCall(options), {
    start: function(metadata, listener, next) {
      next(metadata, {
        onReceiveMetadata: function (metadata, next) {
          metadata.set('xxx', 'xxx')
          next(metadata);
        },
      });
     },
  });
};

稍微特殊的地方是,start函数的next参数被调用时传入的第二个参数并不是一个InterceptingCall的实例,而是一个InterceptingListener的实例,两者都有_callNext的实现,只不过所提供的方法不完全一样罢了。

Channel 的创建

接下来的代码逻辑主要是用于创建 Channel,可以通过传递不同的参数来覆盖 Channel,也可以用默认的 Channel,这个 Channel 对应的 gRPC 中其实就是做数据传输的那一个模块,可以理解为 HTTP2 最终是在这里使用的。
一般很少会去覆盖默认的 Channel,所以我们直接去看 grpc-node 里边的 Channel 是如何实现的。

Channel 是 c++ 代码实现的,代码的位置: https://github.com/grpc/grpc-…

如果有同学尝试过混用 grpc-nodegrpc-js,那么你一定有看到过这个报错:Channel's second argument (credentials) must be a ChannelCredentials
原因就在于 Channel 实例化过程中会进行检查我们创建 Channel 传入的 credential 是否是继承自 grpc 中的 ChannelCredentials 类。
grpc-nodegrpc-js 用的是两个不同的类,所以混用的话可能会出现这个问题。

然后就是根据传入的 credential 的不同来判断是否要使用加密,而一般常用的 grpc.credentials.createInsecure() 其实就是不走加密的意思了,我们可以在 https://github.com/grpc/grpc-…https://github.com/grpc/grpc-… 来看到对应的逻辑。

后边就是调用 c++ 版本的 grpc 来构建对应的 Channel 了,如果有老铁看过 c++ 版本是如何创建 grpc Client 的,那么这些代码就比较熟悉了: https://github.com/grpc/grpc/…
grpc-node 中也是调用的同样的 API 来创建的。

makeUnaryRequest

Client 被创建出来后,我们会调用 Client 上的方法(也就是发请求了),这时候就会触发到上边提到的 requester_funcs 其中的一个,我们先从最简单的 Unary 来说,这种 Client/Server 都是 Unary 请求方式时会触发的函数。
我们通过上边 method_func 中调用方式可以确定传递了什么参数进去,有几个固定的参数 path、request 序列化方式,以及 response 的反序列化方式。
后边的参数就是由调用时传入的动态参数了,这些可以在 makeUnaryRequest 函数定义中看到,分别是 argument(也就是 request body)、metadata(可以理解为 header,一些元数据)、options 是一个可选的参数(自定义的拦截器是放在这里的),可以用于覆盖 method 的一些描述信息,以及最后的 callback 就是我们接收到 response 后应该做的操作了。

整个函数的实现,按长度来说,有一半都是在处理参数,而剩下的部分则做了两件事,一个是实例化了 ClientUnaryCall 对象,另一个则是处理拦截器相关的逻辑,并启动拦截器来发送整个请求。
makeUnaryRequest 函数中涉及到拦截器的部分有这么几块 resolveInterceptorProvidersgetLastListenergetInterceptingCall

ClientUnaryCall

先来看 ClientUnaryCall 做了什么事情,在源码中有这样的一个代码块,是使用该对象的场景:

function ClientUnaryCall(call) {
  EventEmitter.call(this);
  this.call = call;
}

var callProperties = {
  argument: argument,
  metadata: metadata,
  call: new ClientUnaryCall(),
  channel: this.$channel,
  methodDefinition: method_definition,
  callOptions: options,
  callback: callback
};

// 以及后续与拦截器产生了一些关联
var emitter = callProperties.call;
// 这行代码很诡异,看起来是可以在实例化的时候传入的,却选择了在这里覆盖属性值
emitter.call = intercepting_call;

var last_listener = client_interceptors.getLastListener(
  methodDefinition,
  emitter,
  callProperties.callback
);

关于 ClientUnaryCall 的定义也非常简单,其实是一个继承自 EventEmitter 的子类,增加了一个 call 属性的定义,以及两个方法封装调用了 call 属性对应的一些方法。

强烈怀疑 这部分代码是后期有过调整,因为 ClientUnaryCall 构造函数的实现中是可以接受一个参数作为 call 属性的赋值的,然而在代码应用中选择了后续覆盖 call 属性,而非直接在实例化的时候传入进去

resolveInterceptorProviders

resolveInterceptorProviders 是用来处理用户传入的拦截器的,这个函数在 Client 的整个生命周期会有两处调用,一个是在上边 Client 实例化的过程中会触发一次,再有就是每次 method 被调用之前,会重新触发该函数。
resolveInterceptorProviders 的逻辑很简单,就是遍历我们传入的 interceptor_provider 并将对应 method 的信息描述传入并执行,得到 provider 返回的 interceptor 用作拦截器。
Client 实例化过程中是会遍历所有的 method 来执行,而在具体的 method 触发时则只触发当前 method 相关的 provider 逻辑。

getLastListener

getLastListener 按照注释中的描述,是为了获得一个最后会触发的监听者,源码大致是这样的:
https://github.com/grpc/grpc-…

var listenerGenerators = {
  [methodTypes.UNARY]: _getUnaryListener,
  [methodTypes.CLIENT_STREAMING]: _getClientStreamingListener,
  [methodTypes.SERVER_STREAMING]: _getServerStreamingListener,
  [methodTypes.BIDI_STREAMING]: _getBidiStreamingListener
};

function getLastListener(method_definition, emitter, callback) {
  if (emitter instanceof Function) {
    callback = emitter;
    callback = function() {};
  }
  if (!(callback instanceof Function)) {
    callback = function() {};
  }
  if (!((emitter instanceof EventEmitter) &&
       (callback instanceof Function))) {
    throw new Error('Argument mismatch in getLastListener');
  }
  var method_type = common.getMethodType(method_definition);
  var generator = listenerGenerators[method_type];
  return generator(method_definition, emitter, callback);
}

同样也使用了一个枚举来区分不同的方法类型来调用不同的函数来生成对应的 listener。

比如这里用到的 getUnaryListener,是这样的一个逻辑:

function _getUnaryListener(method_definition, emitter, callback) {
  var resultMessage;
  return {
    onReceiveMetadata: function (metadata) {
      emitter.emit('metadata', metadata);
    },
    onReceiveMessage: function (message) {
      resultMessage = message;
    },
    onReceiveStatus: function (status) {
      if (status.code !== constants.status.OK) {
        var error = common.createStatusError(status);
        callback(error);
      } else {
        callback(null, resultMessage);
      }
      emitter.emit('status', status);
    }
  };
}

代码也算比较清晰,在不同的阶段会触发不同的事件,然后再真正返回结果以后,触发 callback 来告知用户请求响应。
也就是我们在示例中调用 sayHello 时传入的 callback 被调用的地方了。

getInterceptingCall

getInterceptingCall 函数的调用会返回一个实例,通过操作该实例我们可以控制请求的开始、数据的发送以及请求的结束。
我们上边 getLastListener 返回的对象触发的时机也是会在这里可以找到的。

从源码上来看会涉及到这么几个函数:

var interceptorGenerators = {
  [methodTypes.UNARY]: _getUnaryInterceptor,
  [methodTypes.CLIENT_STREAMING]: _getClientStreamingInterceptor,
  [methodTypes.SERVER_STREAMING]: _getServerStreamingInterceptor,
  [methodTypes.BIDI_STREAMING]: _getBidiStreamingInterceptor
};

function getInterceptingCall(method_definition, options,
                             interceptors, channel, responder) {
  var last_interceptor = _getLastInterceptor(method_definition, channel,
                                            responder);
  var all_interceptors = interceptors.concat(last_interceptor);
  return _buildChain(all_interceptors, options);
}

function _getLastInterceptor(method_definition, channel, responder) {
  var callback = (responder instanceof Function) ? responder : function() {};
  var emitter = (responder instanceof EventEmitter) ? responder :
                                                      new EventEmitter();
  var method_type = common.getMethodType(method_definition);
  var generator = interceptorGenerators[method_type];
  return generator(method_definition, channel, emitter, callback);
}

function _buildChain(interceptors, options) {
  var next = function(interceptors) {
    if (interceptors.length === 0) {
      return function (options) {};
    }
    var head_interceptor = interceptors[0];
    var rest_interceptors = interceptors.slice(1);
    return function (options) {
      return head_interceptor(options, next(rest_interceptors));
    };
  };
  var chain = next(interceptors)(options);
  return new InterceptingCall(chain);
}

_getUnaryInterceptor 由于篇幅较长,直接贴 GitHub 链接了:https://github.com/grpc/grpc-…

大致的逻辑就是我们通过 method_definitionchannel 等参数来获取到一个 interceptor,并将其拼接到原有的 interceptor 后边,作为最后执行的拦截器, _buildChain 函数比较简单,就是实现了一个链式调用的函数,用来按顺序执行拦截器。

关于 interceptor 如何使用可以看我们介绍 interceptor 用法时写的 demo

主要的逻辑实际上在 _getUnaryInterceptor 中,我们会创建一个功能全面的 interceptor,函数会返回一个匿名函数,就是我们在上边代码中看到的调用 generator 的地方了,而在匿名函数的开头部门,我们就调用了 getCall 来获取一个 call 对象,这个 call 对象就是我们与 gRPC 服务器之间的通道了,请求最终是由 call 对象负责发送的。

getCall 中实际上调用了 channel 对象的 createCall 方法,这部分的逻辑也是在 c++ 中做的了,包含数据的发送之类的逻辑。

这是我们回到 makeUnaryRequest 函数,再看函数结束的地方调用的那三个方法,第一个 start,将我们的 metadata(可以理解为 header) 发送了过去,然后将真实的信息发送了过去,最后调用关闭方法。

我们可以在 _getUnaryInterceptor 中的 startsendMessage 以及 halfClose 函数中都有调用 _startBatchIfReady 函数,而这个方法实际上就是调用的 channel 上的 startBatch 方法,再根据调用链查找,最终会看到处理逻辑在这里:https://github.com/grpc/grpc/…
opType 与 代码中 switch-case 中的对应关系在这里: https://github.com/grpc/grpc-…

首先在 start 里边主要是发送了 metadata,并且尝试接受服务端返回过来的 metadata,并在回调中触发我们传入的 listeneronReceiveMetadata 方法。
然后检查 response 的状态是否正确,并触发 listeneronReceiveStatus 方法。

接下来是调用 sendMessage 方法,在这里我们将消息体进行序列化,并发送,在回调中就会去调用我们传入的 callback。

最后在 halfClose 方法中其实就是发送一个指令来设置请求的结束。

整个的流程细化以后大概是这个样子的:

image

小结

上边整体的记录就是关于 Client 这一侧是如何实现的了。
主要涉及到 Client 的构建、发送请求时做的事情、拦截器的作用。
而更深入的一些逻辑其实是在 c++ 版本的 gRPC 库里所实现,所以本次笔记并没有过多的涉及。

https://segmentfault.com/a/1190000038338788

老板说“把系统升级到https”,我用一个脚本实现了,而且永久免费!​

lei阅读(2)

正文

现在很多站长都会考虑将自己的站点从http升级到https,不仅是基于安全的考虑,有的也是因为第三方平台的限制,如谷歌浏览器会将http站点标记为不安全的站点,微信平台要求接入的微信小程序必须使用https等。

那如何将一个http站点升级为https站点呢?

http与https的区别

为了数据传输的安全,https在http的基础上加入了ssl协议,ssl协议依靠证书来验证服务器的身份,并为浏览器和服务器之间的通信加密。要想将http升级为https,只需要给http站点增加一个CA证书即可。

目前获取CA证书有两种途径:

  1. 购买收费的CA证书
  2. 获取免费的证书

收费的CA证书各大服务提供商都有卖,如阿里云、腾讯云等。

image

【系统架构】如何升级到https?一个脚本帮你搞定,且永久免费

收费的证书不便宜,从阿里云官方网站看,它的价格可以从几千元到上万元不等。

image
《2020最新Java基础精讲视频教程和学习路线!》

【系统架构】如何升级到https?一个脚本帮你搞定,且永久免费

这对于小公司平台,甚至是个人站点来说,是一个不小的开支。

Letsencrypt是一个免费、自动化和开放的证书颁发机构,其颁发的证书一次有效期为三个月,但是只要能持续更新,基本可以永久使用。

今天推荐的这个脚本acme.sh,实现了 acme 协议, 可以帮你持续自动从Letsencrypt更新CA证书。下载地址如下:

github.com/Neilpang/ac…

安装 acme.sh

安装acme.sh很简单,一个命令即可:

curl get.acme.sh | sh

普通用户和 root 用户都可以安装使用。安装过程进行了以下几步:

1、把acme.sh安装到你的home目录下:

~/.acme.sh/

并创建 一个 bash 的 alias,方便你使用:alias acme.sh=~/.acme.sh/acme.sh

2、自动为你创建 cronjob,每天 0:00 点自动检测所有的证书。如果快过期了,需要更新,则会自动更新证书,安装过程不会污染已有的系统任何功能和文件,所有的修改都限制在安装目录中:~/.acme.sh/

生成证书

acme.sh 实现了 acme 协议支持的所有验证协议, 一般有两种方式验证:http 和 dns 验证。

1、http 方式需要在你的网站根目录下放置一个文件, 来验证你的域名所有权,完成验证,然后就可以生成证书了。

acme.sh –issue -d mydomain.com -d www.mydomain.com –webroot /home/wwwroot/mydomain.com/

acme.sh 会全自动的生成验证文件, 并放到网站的根目录,然后自动完成验证。最后会聪明的删除验证文件,整个过程没有任何副作用。

如果你用的是apache服务器,acme.sh 还可以智能的从 apache的配置中自动完成验证,你不需要指定网站根目录:

acme.sh –issue -d mydomain.com –apache

如果你用的是nginx服务器,或者反代,acme.sh还可以智能的从 nginx的配置中自动完成验证,你不需要指定网站根目录:

acme.sh –issue -d mydomain.com –nginx

注意:无论是 apache 还是 nginx 模式,acme.sh在完成验证之后,会恢复到之前的状态,都不会私自更改你本身的配置。好处是你不用担心配置被搞坏,但也有一个缺点,你需要自己配置 ssl 的配置,否则,只能成功生成证书,你的网站还是无法访问https。但是为了安全,你还是自己手动改配置吧。

如果你还没有运行任何 web 服务,80 端口是空闲的, 那么 acme.sh 还能假装自己是一个webserver, 临时听在80 端口,完成验证:

acme.sh –issue -d mydomain.com –standalone

2、dns 方式,在域名上添加一条 txt 解析记录,验证域名所有权。

这种方式的好处是,你不需要任何服务器,不需要任何公网 ip,只需要 dns 的解析记录即可完成验证。不过,坏处是,如果不同时配置 Automatic DNS API,使用这种方式 acme.sh 将无法自动更新证书,每次都需要手动再次重新解析验证域名所有权。

acme.sh –issue –dns -d mydomain.com

然后,acme.sh 会生成相应的解析记录显示出来,你只需要在你的域名管理面板中添加这条 txt 记录即可。

等待解析完成之后, 重新生成证书:

acme.sh –renew -d mydomain.com

注意:第二次这里用的是 –renew

dns 方式的真正强大之处在于可以使用域名解析商提供的 api 自动添加 txt 记录完成验证。

acme.sh 目前支持 cloudflare, dnspod, cloudxns, godaddy 以及 ovh 等数十种解析商的自动集成。

copy/安装 证书

前面证书生成以后,接下来需要把证书 copy 到真正需要用它的地方。

注意:默认生成的证书都放在安装目录下:~/.acme.sh/,请不要直接使用此目录下的文件。例如,不要直接让 nginx/apache 的配置文件使用这下面的文件。这里面的文件都是内部使用,而且目录结构可能会变化。

正确的使用方法是使用 –installcert 命令,并指定目标位置,然后证书文件会被copy到相应的位置,例如:

acme.sh --installcert -d <domain>.com 

--key-file /etc/nginx/ssl/<domain>.key 

--fullchain-file /etc/nginx/ssl/fullchain.cer 

--reloadcmd "service nginx force-reload"
复制代码

一个小提醒,这里用的是 service nginx force-reload,不是 service nginx reload,据测试, reload并不会重新加载证书,所以用的 force-reload。

Nginx 的配置 ssl_certificate 使用 /etc/nginx/ssl/fullchain.cer,而非 /etc/nginx/ssl/.cer ,否则 SSL Labs 的测试会报 Chain issues Incomplete 错误。

–installcert命令可以携带很多参数,来指定目标文件。并且可以指定 reloadcmd, 当证书更新以后,reloadcmd会被自动调用,让服务器生效。

值得注意的是,这里指定的所有参数都会被自动记录下来,并在将来证书自动更新以后,被再次自动调用。

更新证书

目前证书在 60 天以后会自动更新,你无需任何操作。今后有可能会缩短这个时间,不过都是自动的,你不用关心。

更新 acme.sh

目前由于 acme 协议和 Letsencrypt CA 都在频繁的更新,因此 acme.sh 也经常更新以保持同步。

升级 acme.sh 到最新版 :

acme.sh –upgrade

如果你不想手动升级, 可以开启自动升级:

acme.sh –upgrade –auto-upgrade

之后, acme.sh 就会自动保持更新了。

你也可以随时关闭自动更新:

acme.sh –upgrade –auto-upgrade 0

出错怎么办:

如果出错, 请添加 debug log:

acme.sh –issue ….. –debug

或者:

acme.sh –issue ….. –debug 2

链接:https://juejin.cn/post/686583…

https://segmentfault.com/a/1190000038331121

Java高级特性-注解:注解实现Excel导出功能

lei阅读(3)

注解是 Java 的一个高级特性,Spring 更是以注解为基础,发展出一套“注解驱动编程”。

这听起来高大上,但毕竟是框架的事,我们也能用好注解吗?

的确,我们很少有机会自己写注解,导致我们搞不清楚注解是怎么回事,更别提用好注解了。

既然这样,我们就从具体的工作出发,开发一个 Excel 导出功能。我相信,你在搞懂这个例子后,就能明白注解是怎么个用法。

Excel 导出-需求拆解

在后台管理系统中,常常需要把数据导出 Excel 表。

比如,在双十一过后,销售部要把商品订单录入到 Excel 表,财务部要把支付订单录入到 Excel 表,然后各部门汇总分析,最后找个时间讨论怎么改善公司的服务。

你想呀,双十一的订单成千上万,靠人工录入,少说也要花三四天,而且还特别容易出错。所以,你必须开发 Excel 导出功能。

那么,具体怎么做呢?

上次我们提到,注解想发挥作用,有三个要素:定义、使用、读取。这次,我们就利用注解的三个特性,来实现 Excel 导出功能,设计过程是这样的。

第一步,我们要创建不同的 Excel 模型。双十一过后,销售部要订单数据,财务部要支付数据,两个部门要的 Excel 表肯定也不一样,这就得帮每个部门创建不同的 Excel 模型,他们拿到想要的数据。

第二步,我们要根据 Excel 模型,来导出 Excel 表。

看到这,你应该明白 Excel 导出的设计过程了。接下来,我们就来一步步实现这个功能。

创建 Excel 模型

创建 Excel 模型,涉及到注解三要素中的定义、使用。

首先,定义 Excel 注解,我们直接看关键代码。

@Target({ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
public @interface ExcelField {

    /**
     * 导出字段标题
     */
    String title();
    
    /**
     * 导出字段排序(升序)
     */
    int sort() default 0;
    
    /**
     * 对齐方式(0:自动;1:靠左;2:居中;3:靠右)
     */
    int align() default 0;    

}

这里用到了两个元注解@Retention@Target@Target代表这个注解只能放在成员变量上;@Retention代表这个注解要加载到 JVM 内存,我们可以用反射来读取注解。

此外,注解还有 3 个成员变量,分别对应:Excel 的字段标题、字段排序、对齐方式,方便大家微调表格。到了这,定义 Excel 注解就完成了。

接下来,使用注解,我们还是直接看代码。

public class OrderModel {
    @ExcelField(title = "订单号", align = 2, sort = 20)
    private String orderNo;

    @ExcelField(title = "金额", align = 2, sort = 20)
    private String amount;
    
    // 创建时间
    private Date createTime;
    
    // 省略 getter/setter 方法
}

订单模型有 3 个字段:订单号、金额、创建时间,但这里注解只加到订单号、金额上,表示这两个字段会导出 Excel 表,而创建时间会忽略,你可以看看这副图片。

excel模型导出-效果图

至此,我们完成了定义注解、使用注解,得到了一个 Excel 模型。但要想实现导出功能,还必须根据这个模型,生成出 Excel 表。

读取 Excel 模型

读取 Excel 模型,涉及到注解三要素中的读取。 我们需要读取注解,生成 Excel 表,这主要分成 3 个步骤:初始化 Excel 表对象—>写入数据到 Excel 表对象—>输出文件。

第一步,初始化 Excel 表对象。在这一步中,我们要根据 Excel 模型,生成一个 Excel 表对象,要创建这几个东西:标题、表头、样式等等。我们来看代码。

public class ExcelExporter {

    // ...省略无数代码

    /***************************** 初始化 Excel 表对象 ****************************/
    /**
     * 构造函数
     * @param title 表格标题,传“空值”,表示无标题
     * @param cls   excel模型对象
     */
    public ExcelExporter(String title, Class<?> cls) {
        // 获取注解list
        Field[] fs = cls.getDeclaredFields();
        for (Field f : fs) {
            ExcelField ef = f.getAnnotation(ExcelField.class);
            if (ef != null) {
                annotationList.add(new Object[]{ef, f});
            }
        }
        annotationList.sort(comparing(o -> ((ExcelField) o[0]).sort()));
        // 通过注解获取表头
        List<String> headerList = new ArrayList<>();
        for (Object[] os : annotationList) {
            String t = ((ExcelField) os[0]).title();
            headerList.add(t);
        }
        // 初始化excel表:创建excel表、添加表标题、创建表头等等
        initialize(title, headerList);
    }
}

在初始化的时候,我们先从 Excel 模型对象中读取注解,获得一个注解列表;然后,再从注解列表中,读取 title-字段标题;最后,再初始化 Excel 表对象,包括:创建 Excel 表对象、添加表标题、创建表头、添加样式。

第二步,写入数据到 Excel 表对象。在这一步中,我们要把 Java 的列表数据写到 Excel 表对象里,让这些数据能变成 Excel 表的一行行信息。还是来看代码。

public class ExcelExporter {

    /***************************** 初始化 Excel 表对象 ****************************/
    // ...省略无数代码

    /***************************** 写入数据到 Excel 表对象 ****************************/
    /**
     * 写入数据
     * @return list 数据列表
     */
    public <E> ExcelExporter setDataList(List<E> list) {
        for (E dataObj : list) {
            // 添加行
            Row row = this.addRow();

            // 获取数据,并写入单元格
            int cellNo = 0;
            for (Object[] os : annotationList) {
                // 获取成员变量的值
                Object value = null;
                try {
                    value = Reflections.invokeGetter(dataObj, ((Field) os[1]).getName());
                } catch (Exception ex) {
                    log.info(ex.toString());
                    value = "";
                }
                if (value == null) {
                    value = "";
                }

                // 写入单元格
                ExcelField ef = (ExcelField) os[0];
                this.addCell(row, cellNo++, value, ef.align());
            }
        }
        return this;
    }
}

我们先传入一个数据列表 dataList,然后用循环来遍历 dataList,在这个循环中,我们不断把数据写进 Excel 表对象里,具体操作是:创建了一个空白行,利用注解获取成员变量里的值,最后写进 Excel 表的单元格里。

第三步,输出文件。在这一步中,就是 Excel 表对象变成一个文件,来看下最后的代码吧。

public class ExcelExporter {

    /***************************** 初始化 Excel 表对象 ****************************/
    // ...省略无数代码

    /***************************** 写入数据到 Excel 表对象 ****************************/
    // ...省略无数代码

    /***************************** 输出相关 ****************************/
    /**
     * 输出到文件
     * @param fileName 输出文件名,加上绝对路径
     */
    public ExcelExporter writeFile(String fileName) throws IOException {
        FileOutputStream os = new FileOutputStream(fileName);
        this.write(os);
        return this;
    }
}

输出文件就没什么好说的了,就是指定文件名,然后把文件输出到指定的地方。

到了这,读取 Excel 模型就完成了。

当然,读取 Excel 模型涉及到注解的读取,这是最难理解的地方,因为读取注解要用到 Java 另一个高级特性—反射。而且,注解一般是用来简化业务,如果你对业务没有深刻的了解,是很难用好的。

限于篇幅,我只讲了最核心的代码,项目的完整代码放在文末的链接上,大家可以好好看看。

写在最后

注解想发挥作用,有三个要素:定义、使用、读取。这篇文章利用了注解的三要素,实现了 Excel 导出功能。

这分成两步。第一步,创建 Excel 模型,这涉及到注解三要素中的定义、使用;第二步,读取 Excel 模型,这涉及到注解三要素中的读取。

总之,注解一般用来简化业务,你要想用好注解,不但得熟练掌握 Java 的高级用法,还得对业务有深刻的理解。

文章演示代码:点击跳转

https://segmentfault.com/a/1190000038327072