浅谈节流(throttle)与防抖动(debounce)

简介

节流与防抖是性能优化常见的方式,用于限制一些高频操作导致对cpu无意义的负担。
我搜索了网络上很多相关的文章,都没有办法直观表现出两者的区别。因此我自己总结了一下两者的区别,并用几个例子来解释一下两者区别

一下内容均以 lodash 的实现为例

防抖动debounce

防抖动的核心概念是当停止调用函数一段时间后。调用该函数

example:

1
2
3
4
5
6
const d = _.debounce(() => console.log('call func'), 1000, {leading: true, trailing: true});

const dt = setInterval(() => {
console.log('call timer');
d();
}, 200);

输出结果如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
call func # 如果 leading 为true
call timer
call timer
call timer
call timer
call timer
...
call timer
call timer
call timer
(执行clearInterval(dt))
(等待1s后)
call func # 如果 trailing 为true

即如果在限定时间(本例为1s)内不断调用防抖函数,则永远不会触发1s后的trailing的输出

节流throttle

节流的核心概念是一段时间内,函数最多只能被调用一次。

example:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
const t1 = _.throttle(() => console.log('call func'), 1000, {leading: true, trailing: true});
t1(); // 立即输出call func

const t2 = _.throttle(() => console.log('call func'), 1000, {leading: false, trailing: true});
t2(); // 一秒后输出call func

const t3 = _.throttle(() => console.log('call func'), 1000, {leading: false, trailing: false});
t3(); // 无反应

const t4 = _.throttle(() => console.log('call func'), 1000, {leading: true, trailing: true});
const t4t = setInterval(t4, 200); // 立即输出call func, 之后每秒输出一次

const t5 = _.throttle(() => console.log('call func'), 1000, {leading: false, trailing: true});
const t5t = setInterval(t5, 200); // 一秒后输出call func, 之后每秒输出一次

const t6 = _.throttle(() => console.log('call func'), 1000, {leading: false, trailing: false});
const t6t = setInterval(t6, 200); // 一秒后输出call func, 之后每秒输出一次 因为内部是通过 debounce 的 maxWait来实现的

如果在 throttle 规定的时间内有多次触发的话。必定会在时间段结束时触发函数调用
如果只有一次触发的话。如果 leadingtrailing 都为 true 。只会触发开始的那次函数调用

共同点

对于lodash来说
throttle的实现是通过debounce来实现的

lodash 实现

1
2
3
4
5
6
7
8
9
throttle(func, wait, options);

// 等价于

debounce(func, wait, {
options.leading || true,
options.trailing || true,
'maxWait': wait
});

参考文章

https://css-tricks.com/debouncing-throttling-explained-examples/

Docker Swarm 模型配合 traefik 实现集群sticky实践

背景

为什么要用Docker Swarm, 相比 Kubernetes 有什么好处

Docker Swarm可以看作是docker自带的一个简化版的Kubernetes, 拥有Kubernetes的基本功能如:

  • 更新
  • 回滚
  • 动态扩容
  • 分布式部署

Docker Swarm 的优势在于:

  • Docker 自带, 无需另外安装。学习成本低
  • 单机也能很好的使用,也可以很方便的进行实例扩容与设备扩容
  • Swarm 只有2层网络封装,而 Kubernetes 有5层网络封装
  • Swarm 本身占用的内存只有100M左右,而 Kubernetes 的简化版 k3s 也需要512M的内存空间,对小资源机器友好

Traefik 是什么, 为什么要使用它

Traefik 是一个反向代理软件,类似 Nginx但对于微服务有很好的优化。可以搭配各种分布式发现服务而无需另外配置。在本篇文章中我们会使用 Docker Provider 作为服务发现。

虽然 Docker Swarm 自带了http请求的分发,但是无法实现 sticky 功能(即同一用户的请求会分发到同一后端实例),因此需要Traefik 作为请求的中间件来分发请求

搭建 Docker Swarm 环境

Docker Swarm 环境非常好搭建,因为已经集成到Docker中了。我们安装好Docker以后就可以直接使用了

依赖:

  • Docker 1.12+
1
2
# 初始化swarm, 并将当前节点作为manager
$ docker swarm init
1
2
3
# 或者使用--advertise-addr 和 --listen-addr参数来指定使用哪个IP作为沟通IP
# 两个参数一般保持一致即可
$ docker swarm init --advertise-addr 10.0.0.1:2377 --listen-addr 10.0.0.1:2377

使用Docker快速搭建Traefik

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
version: '3'

services:
reverse-proxy:
# The official v2.0 Traefik docker image
image: traefik:v2.0
# Enables the web UI and tells Traefik to listen to docker
command: --api.insecure=true --providers.docker.endpoint=tcp://127.0.0.1:2377 --providers.docker.swarmMode=true
ports:
# The HTTP port
- "80:80"
# The Web UI (enabled by --api.insecure=true)
- "8080:8080"
volumes:
# So that Traefik can listen to the Docker events
- /var/run/docker.sock:/var/run/docker.sock
  • api.insecure参数用于打开WEB UI. 可以访问127.0.0.1:8080/api/rawdata获取当前可以连接到的服务的相关信息
  • providers.docker.endpoint指向docker的沟通端口。默认端口为2377
  • providers.docker.swarmMode 表示为swarm模式

创建测试服务

此处使用 whoami 镜像提供的HTTP服务用于打印出集群连接相关信息

将以下文件保存为docker-compose.yml

1
2
3
4
5
6
7
8
9
10
11
12
13
version: '3'

services:
whoami:
# A container that exposes an API to show its IP address
image: containous/whoami
deploy:
replicas: 3
labels:
- "traefik.http.routers.whoami.rule=Host(`whoami.docker.localhost`)"
- "traefik.http.services.whoami.loadbalancer.server.port=80"
- "traefik.http.services.whoami.loadbalancer.sticky=true"
- "traefik.http.services.whoami.loadbalancer.sticky.cookie.name=foosession"

启动集群:

1
docker stack deploy -c ./docker-compose.yml whoami

测试命令:

1
$ curl -vs -c cookie.txt -b cookie.txt -H "Host: whoami.docker.localhost" http://127.0.0.1

注意需要带上 header Host 这样才能成功反向代理

Vim 操作笔记

起因

听说Vim对开发者开发效率有比较大的提升,正好最近在寻求突破,就开始尝试一下一直想尝试使用的vim

此处主要记录了配置vim的一些关键

系统环境:

  • Mac
  • zsh

替换系统自带的vim

1
2
3
4
5
# 使用 brew 安装 vim
$ brew install vim

$ echo "vim=/usr/local/bin/vim" >> ~/.zshrc

安装包管理器

使用vim-plug 作为包管理器

划分vim配置(可选)

1
2
3
4
5
6
7
~
├── .vimrc
└── .vim
├── autoload
├── general.vim
├── mappings.vim
└── plugins.vim

.vimrc中使用如下命令进行子配置运行

1
2
3
runtime! mappings.vim
runtime! general.vim
runtime! plugins.vim

插件、主题、配置

快捷键

不常见但实用

  • Ctrl + o: 跳到上一个位置

  • Ctrl + i: 跳到下一个位置

  • Ctrl + u: 上翻半页

  • Ctrl + d: 下翻半页

  • Shift + *: 在当前文件中搜索光标指定位置的文本

  • Ctrl + r: 在命令模式如插入寄存器中的文本。如Ctrl+r "

  • cw: 替换从光标所在位置后到一个单词结尾的字符

技巧

  • 智能大小写匹配

    如果搜索字符串有大写时则对大小写敏感

    1
    2
    set ignorecase
    set smartcase

    必须设置了ignorecase后smartcase才生效

  • 让vim支持系统剪切板

    首先需要vim支持剪切板功能,如果没有的话就下载完整的包

    1
    $ vim --version | grep clipboard

    如果clipboard属性前为+则说明vim已支持剪切板

    然后设置.vimrc

    1
    set clipboard=unnamed
  • 退出时保存会话并在下次打开时恢复

    1
    2
    3
    4
    5
    6
    au VimLeave * mks! ~/Session.vim
    if expand("%")==""
    if(expand("~/Session.vim")==findfile("~/Session.vim"))
    silent :source ~/Session.vim
    endif
    endif

参考文章

Unicode编码解明

简介

本文简述了Unicode编码解码相关知识。假设读者已经对此有一定的基本了解

从Base64开始

为了更好说明变长字符编码,我们从Base64编码开始。因为Base64是每个字节进行编码

编码如下字符串:

1
你好, 世界!

得到如下编码:

1
5L2g5aW9LCDkuJbnlYwh

将这段编码放入浏览器控制台中进行不进行任何处理的解密

1
atob('5L2g5aW9LCDkuJbnlYwh');

得到如下结果:

1
你好, 世界!

可以阴影约约看到逗号和感叹号已经被解释出来,而中文没有被正确处理。因为英文的逗号和感叹号在128位ascii表中。而其他的中文字符就无法正确翻译了。

为了能正确翻译中文。我们需要对数据进行一些特殊处理。即实现utf8的编码规范来将二进制转换成一个unicode码,使其能对应上一个具体的字符。(unicode表是一个巨大的map, 每个数字都能对应一个具体的字符, 至于字符的具体渲染由系统提供的字体显示)

atob
atob 是一个很基础的base64转二进制的方法。他只会单独的去处理每个字节而不管其具体的编码实现

UTF8解码

我们先处理第一个字符ä
很明显。这是因为浏览器错误的处理了这个字节的翻译。我们需要将其转换成二进制

1
2
3
4
'ä'.charCodeAt(0); // 228

// 将其转换成二进制
(228).toString(2); // 11100100

我们查看一下 utf8的编码规范 。首字符前n个1表示由n个字节组成.以0表示收尾与分割。因此我们提取出其描述的二进制位为1110表示这个字由3个字节组成。那么我们继续提取接下来的2个字符

1
2
'½'.charCodeAt(0).toString(2); // 10111101
' '.charCodeAt(0).toString(2); // 10100000

其中最前的二位10是描述位,是无效的。我们将这三个字节的有效的二进制位提取出来可得: 0100 111101 100000。将其转换成16进制

1
(0b0100111101100000).toString(16); // 4f60

简单验证一下结果。直接在控制台输入:

1
'\u4f60' // 你

我们成功提取出了第一个中文字符。那么剩下的中文字符也很简单了

而编码就是其逆过程

UTF8编码

1
2
3
4
5
6
7
8
// 将 你好, 世界! 编码成16进制字符
const str = '你好, 世界!'

str
.split("")
.map(char => char.charCodeAt(0).toString(16))
.map(hex => "\\u" + hex.padStart(4, "0"))
.join(""); // \u4f60\u597d\u002c\u0020\u4e16\u754c\u0021

直接将输出的字符串复制到控制台可以看到自动转换出的中文

\u表示的是只后面跟的是一个unicode。长度为4字节

简单实现一个callback回调的包装器

要求

  • 不使用await
  • 不使用Promise
  • 要求并行指定一系列有callback的函数,并取到返回值

代码

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
function fetch1(cb) {
cb(1)
}

function fetch2(cb) {
cb(2)
}

function fetch3(cb) {
setTimeout(() => {
cb(3)
}, 10000);
}

function wrapFn(fns) {
return function(onCompleted) {
let flag1 = false;
let flag2 = false;

const flags = fns.map(() => false);
const res = fns.map(() => null);
const check = () => {
if(!flags.includes(false)) {
onCompleted(res);
}
}

fns.forEach((fn, i) => {
fn((ret) => {
flags[i] = true;
res[i] = ret;
check();
})
})
}
}


function main() {
// 测试代码
wrapFn([fetch1, fetch2, fetch3])((res) => {
console.log('全部执行完毕');
console.log('结果:', res);
})
}
main();

Git骚操作之从一个分支中批量将离散的commit 迁移到另一个分支

背景

因为某些原因。分支A与分支B在某个点分叉了。且分叉出来的分支B拥有很多乱七八糟的commit。现在希望将分支B中的代码迁移到分支A中。但因为分支B中有很多其他的commit。因此希望把分支B舍弃,只保留想要的一些commit。

本例是指仅作者为我自己的commit

方案

使用git log将自己的commit选出来。然后切换到分支A上。将选出来的commit cherry-pick。

首先根据某些条件选出想要迁移的commit。最终输出成空格分割的hash号

bash语句如下:

1
2
3
# 本例假设起始commit为4524cb34ea4
# 当前所在分支: 分支B
git log --author moonrailgun --oneline 4524cb34ea4^1...HEAD | awk '{print $1}' | sed '1!G;h;$!d' | xargs echo

语句解释:

  • git log --author moonrailgun --oneline 4524cb34ea4^1...HEAD 获取范围4524cb34ea4(包含该commit)~当前commit的commit中作者是(包含)moonrailgun的列并用单行显示
  • awk '{print $1}' 获取每行中以空格分割的第一列
  • sed '1!G;h;$!d' 将输入按行倒序输出(因为git log输出的最后一行在最上面)
  • xargs echo 将输入的行变成一行

由此可以得到一串用空格分割的hash字符串

然后git checkout A切换到分支A。执行git cherry-pick <此处输入刚刚得到的字符串>
如有冲突,解决冲突后git cherry-pick --continue即可

结论

  • 不会丢失代码
  • 若有冲突能马上解决
  • 保留commit细节
  • 解放生产力

Let's Encrypt免费通配符证书申请

依赖

1
git clone https://github.com/certbot/certbot.git

命令

示例:

1
./certbot-auto certonly  -d *.moonrailgun.com --manual --preferred-challenges dns --server https://acme-v02.api.letsencrypt.org/directory

参数说明:

  • certonly 表示安装模式,Certbot 有安装模式和验证模式两种类型的插件。
  • --manual 表示手动安装插件,Certbot 有很多插件,不同的插件都可以申请证书,用户可以根据需要自行选择
  • -d 为那些主机申请证书,如果是通配符,输入 *.yourdomain.com
  • --preferred-challenges dns 使用 DNS 方式校验域名所有权
  • --server Let’s Encrypt ACME v2 版本使用的服务器不同于 v1 版本,需要显示指定。

参考文章

MySQL 查询优化笔记

概述

正确的查询方式与正确的索引可以极大增加数据的查询效率,能极大提升服务器响应时间。对于较大的并发服务来说一点点提升也有比较大的收益。

sql调优

一条sql语句的执行过程:

mysql分为server层和存储引擎层两个部分;
Server 层包括连接器、查询缓存、分析器、优化器、执行器等,涵盖 MySQL 的大多数核心服务功能,以及所有的内置函数(如日期、时间、数学和加密函数等),所有跨存储引擎的功能都在这一层实现,比如存储过程、触发器、视图等。
而存储引擎层负责数据的存储和提取。其架构模式是插件式的,支持 InnoDB、MyISAM、Memory 等多个存储引擎。
现在最常用的存储引擎是 InnoDB,它从 MySQL 5.5.5 版本开始成为了默认存储引擎。

使用show create table 语句查看一张表的DDL

show create table user

使用desc或explain调优sql语句

e.g. : desc select * from user

即在要执行的语句前可以加上desc或explain查看该语句的查询操作。(两者操作是等价的)

该命令会列出以下项:

  • id
  • select_type
  • table
  • partitions
  • type
  • possible_key
  • key
  • key_len
  • ref
  • rows
  • filterd
  • Extra

其中我们主要需要关心的有三列: key, rowsfiltered

其中:
key主要是表示当前查询用到的键
rows表示查询后返回的行数
filtered表示当前查询过滤了多少。 100表示完全没有进行过滤,是最佳的情况。因为where操作是很消耗性能的

简单的,如select count(1) from employees where gender = 'M'
我们可以通过简单的增加一个索引来实现优化: alter table employees add index(gender)
select count(1) 表示仅返回数据量而不关心数据值 减少其他变量影响结果

这样当我们使用desc语句时可以看到key变为了gender, rows数量为返回的数量,filtered变为了100。可以注意到的是,Extra的值从Using where变为Using index。说明我们这条语句使用了索引

而对于多条件的查询。我们可以通过联合索引来进行优化
如以下查询:
select count(1) from employees where gender = 'F' and birth_date > '1964-01-01'
则可以通过增加联合索引来进行优化
alter table employees add key (gender, birth_date)
注意: 联合索引的顺序很重要,如果顺序不对则无法进行优化

如果我们想在select时统计别的数据,如以下查询:
select count(distinct birth_date) from employees where gender = 'F'
我们也可以使用联合索引进行调优
alter table employees add key (birth_date, gender)
同样的。需要注意顺序, 不过要注意比如是(birth_date, gender), 虽然都是filtered: 100但是执行效率仍有区别
区别如下:

  • (birth_date, gender): key_len为4, 即用到了两个键(gender是tiny,长度1,birth_date是日期,长度4)。rows为4788,Extra为Using where; Using index for group-by
  • (gender, birth_date): key_len为1, 只用到了gender键。rows为149734,Extra为Using index

而对于一些无法优化的。如双向like操作,也可以通过联合索引来应用一部分索引增加部分速度(虽然只会应用一部分)
select count(1) from employees where gender = 'F' and first_name like '%a%' and last_name like '%b%';
可以增加联合索引实现索引下推功能:
alter table employees drop key gender, add key (gender, first_name, last_name);

优化总结

  • 每个索引都是一个BTree(MySQL一般是B+Tree)
  • 什么时候加索引:搜索条件固定,数据分布不均(即需要全表搜索)
  • 双向like是没法优化的
  • 联合索引受顺序影响。而且如果最左索引是范围的话无法使用后面的
  • 使用索引下推,可以减少从主键树上取数据的时间
  • 大多数复杂情况or查询是无法优化的, 但是一些简单查询可以优化
  • 尽量使用join查询而不是子查询,因为join查询能被优化而子查询不行

参考文档

Linux 小资源服务器使用经验总结

善用交换内存

有些时候。作为个人用的服务器我们往往不会去购买一些性能很好的服务器。但是在某些情况下我们却要临时去使用一个比较高的内存去执行某个程序,但是我们当前服务器的资源却无法执行。因为linux为了防止自身系统崩溃,引入了一个OOM Killer机制。即当一个进程占用过多内存时,系统会直接kill掉这个进程并抛出Out of memory错误。
我们可以通过grep "Out of memory" /var/log/messages来查看相关日志。
这个时候我们就可以使用交换内存来用磁盘空间换内存了。

第一步: 创建一个空白文件

切换到root权限

使用命令dd if=/dev/zero of=/opt/swapfile bs=1M count=1024来创建一个1GB的空白文件到/opt/swapfile。同样的可以按照这个方法创建其他大小的空白文件。我一般会创建和内存一样大小的交换空间

设置一下权限chmod 600 /opt/swapfile

第二步: 创建交换空间

使用命令mkswap /opt/swapfile来将这个空白文件变成一个swap文件。只有该文件才能将对应的磁盘空间作为一个临时内存。

第三部: 应用交换文件

使用命令swapon /opt/swapfile将该交换空间应用到系统中。此时执行free -h可以看到swap一行多出了1GB空间

交换空间应该设定多大?

实际内存 推荐交换空间 推荐交换空间(开启休眠模式)
⩽ 2GB 2倍 3倍
2GB - 8GB 1倍 2倍
8GB - 64GB 至少4GB 1.5倍
> 64GB 至少4GB 不推荐

参考文章:

其他

  • swapoff -a 移除所有的swap内存空间