0%

视频链接地址:https://www.bilibili.com/video/BV19r4y1w7Nx

一、Golang“调度器”的由来?

(1) 单进程时代不需要调度器

我们知道,一切的软件都是跑在操作系统上,真正用来干活(计算)的是CPU。早期的操作系统每个程序就是一个进程,直到一个程序运行完,才能进行下一个进程,就是“单进程时代”

一切的程序只能串行发生。

早期的单进程操作系统,面临2个问题:

1.单一的执行流程,计算机只能一个任务一个任务处理。

2.进程阻塞所带来的CPU时间浪费。

那么能不能有多个进程来宏观一起来执行多个任务呢?

后来操作系统就具有了最早的并发能力:多进程并发,当一个进程阻塞的时候,切换到另外等待执行的进程,这样就能尽量把CPU利用起来,CPU就不浪费了。

阅读全文 »

通道和阻塞

通道(channel)和协程(goroutine)是实现Go并发程序的两种机制。其中,通道又分为无缓冲通道和有缓冲通道两种,在编写实际的并发程序时,基本都使用异步模式的有缓冲通道。通道又可细分为支持读和写的双向通道,只读的通道,只写的通道三种。

通道阻塞场景

无论是有缓存通道、还是无缓冲通道都存在阻塞的情况。阻塞场景共4个,有缓存和无缓冲各2个。

无缓冲通道的特点是,发送的数据需要被读取后发送才会完成(同步),它阻塞场景是:

  • 通道中无数据,但执行读通道。
  • 通道中无数据,向通道写数据,但无协程读取。

有缓存通道的特点是,有缓存时可以向通道中写入数据后直接返回(异步),它阻塞场景是:

  • 通道缓存无数据,但执行读通道(接收数据)。
  • 通道缓存已经占满,向通道写数据(发送数据),但无协程读。

使用协程、通道和select

Go的select关键字可以让我们操作多个通道,将协程(goroutine),通道(channel)和select结合起来构成了Go的一个强大特性。

阅读全文 »

说明:理解本文的前提是你对Go语言已有一定的基础了解。

什么是指针:即一个指针变量指向一个值的内存地址。

使用值类型和指针类型的区别
首先,我们来看一个计算面积的代码,如下所示。

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
package main

import (
"fmt"
)

type Rect struct { //定义一个结构体
width float64
length float64
}

func (rect Rect) area() float64 { //定义一个方法,按值传递
return rect.width * rect.length
}

func (rect *Rect) area1() float64 { //定义一个方法,按指针传递
rect.width *= 2
rect.length *= 2
return rect.width * rect.length
}

func main() {
var rect = new(Rect) //使用new函数创建一个结构体指针rect,也就是说rect的类型是*Rect
rect.width = 100
rect.length = 200
fmt.Println("Width:", rect.width, "Length:", rect.length,"Area:", rect.area()) //通过结构体指针类型的变量调用area()方法
fmt.Println("Width:", rect.width, "Length:", rect.length,"Area:", rect.area1())
}

在Go语言中,默认是按值传递。当一个变量当作参数传递的时候,会创建一个变量的副本,然后传递给函数或者方法,你可以看到这个副本的地址和变量的地址是不一样的。当变量当做指针被传递的时候,一个新的指针被创建,它指向变量同样的内存地址,所以你可以将这个指针看成原始变量指针的副本。

阅读全文 »

一、最早的Web服务器

最早的Web服务器简单地响应浏览器发来的HTTP静态文件请求,并将存储在服务器上的静态文件(例如: jpg、htm、html)返回给浏览器。如图是处理流程

比如我访问:http://www.example.com/index.html,那么网络服务器就会去对应目录中找到 index.html这个文件,并返回给浏览器。

二、CGI的出现

首先说明:CGI是一种协议

事物总是不断发展,网站也越来越复杂,所以出现动态技术。但是Web服务器并不能直接运行 php/asp 这样的文件,自己不能做,外包给别人吧,但是要与第三做个约定,我给你什么,然后你给我什么,就是我把请求参数发送给你,然后我接收你的处理结果再给客户端。这个约定就是 CGI协议(Common Gateway Interface),协议只是一个“规定、规则”,理论上用什么语言都能实现,比如用 vb/c/perl/php/python 来实现。

在2000年或更早的时候,CGI 比较盛行。那时,Perl 是编写 CGI 的主流语言,以至于一般的 CGI 程序(遵循 CGI 协议的程序)就是 Perl 程序(例如世界上80%的网站所采用的编程语言 php 语言刚开始的版本就是用Perl语言写的)。

CGI 是 “Common Gateway Interface” 的缩写,翻成中文叫“公共网关接口”,它是 web 服务器与外部应用程序(CGI 程序)之间传递信息的接口标准。通过 CGI 接口,web 服务器就能够获取客户端提交的信息,并转交给服务器端的 CGI 程序处理,最后返回结果给客户端。也就是说,CGI 实际上是一个接口标准。我们通常所说的 CGI 是指 CGI 程序,即实现了 CGI 接口标准的程序。只要某种语言具有标准输入、输出和环境变量,如 perl/PHP/C 等,就可以用来编写 CGI 程序。CGI 只是接口协议,根本不是什么语言。

阅读全文 »

日志是mysql数据库的重要组成部分,记录着数据库运行期间各种状态信息。mysql日志主要包括错误日志、查询日志、慢查询日志、事务日志、二进制日志几大类。作为开发,我们重点需要关注的是二进制日志(binlog)和事务日志(包括redo logundo log),本文接下来会详细介绍这三种日志。

binlog

binlog用于记录数据库执行的写入性操作(不包括查询)信息,以二进制的形式保存在磁盘中。binlogmysql的逻辑日志,并且由Server层进行记录,使用任何存储引擎的mysql数据库都会记录binlog日志。

逻辑日志:可以简单理解为记录的就是sql语句
物理日志:因为mysql数据最终是保存在数据页中的,物理日志记录的就是数据页变更

binlog是通过追加的方式进行写入的,可以通过max_binlog_size参数设置每个binlog文件的大小,当文件大小达到给定值之后,会生成新的文件来保存日志。

binlog使用场景

在实际应用中,binlog的主要使用场景有两个,分别是主从复制数据恢复

  1. 主从复制:在Master端开启binlog,然后将binlog发送到各个Slave端,Slave端重放binlog从而达到主从数据一致。
  2. 数据恢复:通过使用mysqlbinlog工具来恢复数据。

binlog刷盘时机

对于InnoDB存储引擎而言,只有在事务提交时才会记录biglog,此时记录还在内存中,那么biglog是什么时候刷到磁盘中的呢?mysql通过sync_binlog参数控制biglog的刷盘时机,取值范围是0-N

  • 0:不去强制要求,由系统自行判断何时写入磁盘;
  • 1:每次commit的时候都要将binlog写入磁盘;
  • N:每N个事务,才会将binlog写入磁盘。

从上面可以看出,sync_binlog最安全的是设置是1,这也是MySQL 5.7.7之后版本的默认值。但是设置一个大一些的值可以提升数据库性能,因此实际情况下也可以将值适当调大,牺牲一定的一致性来获取更好的性能。

阅读全文 »

Version

PHP version 7.4

问题呈现

最小化分析代码:

1
2
3
4
5
6
7
8
9
$data = ['foo', 'bar'];

foreach ($data as &$item) {
}

foreach ($data as $item) {
}

print_r($data);

输出结果:

1
2
3
4
5
Array
(
[0] => 'foo'
[1] => 'foo'
)

我们可以发现,$data的值莫名奇妙变了,而它只是经过了两个空循环而已,发生了什么?!

先总结一下 PHP 中两条关于引用的两个规则:

  • 给引用变量赋值,实际上是给引用所指向的变量赋值
  • 一个引用变量可以被修改为对另外一个变量的引用

下面我来一行行代码分析产生这个问题的原因:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
$data = ['foo', 'bar'];

// 循环开始,$item 变量不存在,新建一个$item 变量,且是一个引用变量,它不指向任何变量地址
foreach ($data as &$item) {
// loop 1: 执行了 $item = &$data[0];$item 指向 $data[0] 的地址
// loop 2: 执行了 $item = &$data[1];$item 指向 $data[1] 的地址
}
// 提示:这个循环没有改变 $data 的数据,只是 $item 依然指向第二个元素 的地址

// 循环开始,$item 变量存在,不会新建变量
foreach ($data as $item) {
// loop 1: 执行了 $item = $data[0];$item 所指向的变量(即 第二个元素)的值被修改为$data[0](即'foo'),这里已经导致了$data 两个元素都等于 'foo'
// loop 2: 执行了 $item = $data[1];由于$item 指向的是$data[1],实际上相当于执行$data[1] = $data[1],没有任何意义
}
// 最后$data 中的两个元素都是 'foo'

如何避免这个问题1:

1
2
3
4
5
foreach ($data as &$item) {

// 每次 loop 销毁$item (实际上只要在最后一次 loop 销毁即可,因此你可以把 unset 写到 foreach 后面,就是不是很好看)
unset($item);
}

如何避免这个问题2:

不使用引用

Version

mysql version 8.0.13

事务隔离级别概述

mysql 中,InnoDB 所提供的事务符合 ACID 的要求,而事务通过事务日志中的 redo log 和 undo log 满足了原子性、一致性、持久性,事务还会通过锁机制满足隔离性,在 InnoDB 存储引擎中,有不同的隔离级别,它们有着不同的隔离性。

什么是事务的隔离级别?如果只是从概念上理解的话可能比较模糊,咱们直接看看不同隔离级别下的实际表现是什么样子的,再结合理论去理解,就会明了很多。首先,打开两个终端,同时连接到当前数据库,如下图所示,我们对两个回话进行编号,并且以颜色区分,1号会话使用黄色进行标识,2号会话使用红色进行标识。

两个会话使用相同的数据库。

两个会话中同时各自开启一个事务。

由于下面的所有操作会在两个会话中来回切换,所以,为了方便描述,我们为每个操作的顺序进行编号,例如下图中,我们先在会话1的事务1中执行了更新操作,然后在事务1中执行了查询操作,最后又在会话2中的事务2中执行了查询操作,按照操作顺序,为各个操作进行了顺序编号。

阅读全文 »

系统环境介绍

Win 10 2004
WSL2(Ubuntu 20.04)
Docker 19.03.13

启用 Linux 子系统

打开 控制面板 -> 程序 -> 启用或关闭Windows功能。找到适用于Linux的Windows子系统虚拟机平台,勾选这两项之后确定,并重新启动计算机。

切换系统 WSL 默认版本到 2

使用管理员用户打开 Windows PowerShell,运行:

1
wsl --set-default-version 2

注意,有可能会出现如下提示:

WSL 2 需要更新其内核组件。有关信息,请访问 https://aka.ms/wsl2kernel

出现这个提示,说明需要更新 WSL2 的内核。按照提示打开链接。点击下图中的下载链接,下载并安装 WSL2 内核。

阅读全文 »

背景

为了更好的说明几种算法,我们举个例子,下文就这个例子分别几种算法实现。

  • 如何限制每分钟访问 /api/books 接口不能超过 120 次 ?

我们先定义一个接口:

1
2
3
4
interface RateLimiter
{
public function access();
}

固定时间窗口算法

固定时间窗口算法又叫计数器算法,逻辑就是对固定时间段内的访问次数进行计数,如果计数结果超过次数限制,则拒绝访问。

固定时间窗口算法的劣势就在于其只关心时间段内的总访问次数,而忽略了瞬间集中请求的问题,换而言之,这种统计方法的粒度太粗了,然而我们无法保证请求在时间段内的分布是平均的。

举个例子,如果 A 用户访问接口的时间分布如下:

时间段 访问次数
00:00 ~ 00:30 20
00:30 ~ 01:00 100
01:00 ~ 01:30 100
01:30 ~ 02:00 20

显然在第一分钟,我们有 120 次请求,第二分钟也是 120 次请求,但是 00:30 ~ 01:30 这一分钟时间内,显然是请求书超过 120 次的,所以,这种情况虽然实现了需求,但是很勉强,粒度不够细。

阅读全文 »

简介

在命令行中是没有 redis pipeline 功能的,但 redis 是支持 pipeline 的,而且在各个语言版的 client 中都有相应的实现。

简单概括 redis pipeline 的功能就是将单次请求变为批量请求。

由于网络开销延迟,就算 redis server 端有很强的处理能力,也会由于收到的 client 消息少,而造成吞吐量小。当 client 使用 pipelining 发送命令时,redis server 会将请求放到队列中(使用内存),执行完毕后一次性发送结果。

Pipeline 在某些场景下非常有用,比如有多个 command 需要被“及时的”提交,而且他们对相应结果没有互相依赖,对结果响应也无需立即获得,那么 pipeline 就可以充当这种“批处理”的工具;而且在一定程度上,可以较大的提升性能,性能提升的原因主要是 TCP 连接中减少了“交互往返”的时间。

管道(pipeline)可以一次性发送多条命令并在执行完后一次性将结果返回,pipeline 通过减少客户端与 redis 的通信次数来实现降低往返延时时间,而且 Pipeline 实现的原理是队列,而队列的原理是时先进先出,这样就保证数据的顺序性。 Pipeline 的默认的同步的个数为 53 个,也就是说 arges 中累加到 53 条数据时会把数据提交。

需要注意到是用 pipeline 方式打包命令发送,redis 必须在处理完所有命令前先缓存起所有命令的处理结果。打包的命令越多,缓存消耗内存也越多。所以并不是打包的命令越多越好。具体多少合适需要根据具体情况测试。

适用场景

有些系统可能对可靠性要求很高,每次操作都需要立马知道这次操作是否成功,是否数据已经写进 redis 了,那这种场景就不适合。

还有的系统,可能是批量的将数据写入 redis,允许一定比例的写入失败,那么这种场景就可以使用了,比如 10000 条一下进入 redis,可能失败了 2 条无所谓,后期有补偿机制就行了,比如短信群发这种场景,如果一下群发 10000 条,按照第一种模式去实现,那这个请求过来,要很久才能给客户端响应,这个延迟就太长了,如果客户端请求设置了超时时间 5 秒,那肯定就抛出异常了,而且本身群发短信要求实时性也没那么高,这时候用 pipeline 最好了。