Map是各个语言中都有一种数据结构,在原生支持并发的Go这里也例外。但是在高并发场景下使用原生的Map恰恰是存在一些坑的。
Go官方博客中对有一些说明:
Maps are not safe for concurrent use: it’s not defined what happens when you read and write to them simultaneously. If you need to read from and write to a map from concurrently executing goroutines, the accesses must be mediated by some kind of synchronization mechanism. One common way to protect maps is with sync.RWMutex.
意思是说,并发访问map是不安全的,并发读写时会出现未定义行为。如果希望多协程访问读写map,必须提供某种同步机制,最常用的是sync.RWMutex
。
使用sync.RWMutex
加锁是最直接的一种方式,但是存在两个问题:
示例: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
60package main
import (
"fmt"
"sync"
"time"
)
const CycleNum = 100000
type Map interface {
Set(key, value string)
Get(key string) (string, bool)
}
type MutexMap struct {
m sync.RWMutex
c map[string]string
}
func NewMutexMap() *MutexMap {
return &MutexMap{
c: make(map[string]string),
}
}
func (m *MutexMap) Set(key, value string) {
m.m.Lock()
defer m.m.Unlock()
m.c[key] = value
}
func (m *MutexMap) Get(key string) (string, bool) {
m.m.RLock()
defer m.m.RUnlock()
res, ok := m.c[key]
return res, ok
}
func main() {
beginTime := time.Now()
mutexMap := NewMutexMap()
wg := sync.WaitGroup{}
wg.Add(CycleNum)
for i := 0; i < CycleNum; i++ {
s := fmt.Sprintf("%d", i)
go func() {
mutexMap.Set(s, s)
}()
go func() {
mutexMap.Get(s)
}()
wg.Done()
}
wg.Wait()
spanTime := time.Now().Sub(beginTime)
fmt.Printf("spend time: %v", spanTime)
}
sync.Map
是Go 1.9 以后标准库中提供的并发Map。其内部实现是引入两个map将读写进行分离。
其中read map只提供读,而dirty map则负责写。这样read map就可以在不加锁的情况下进行并发读取,当read map中没有读取到值时,再加锁进行后续读取,并累加未命中数,当未命中数到达dirty map的长度时,用dirty map替换read map。虽然引入了两个map,但是底层数据存储的是指针,指向的是同一份值
源码:
数据结构1
2
3
4
5
6
7
8
9type Map struct {
// 互斥锁
mu Mutex
// 读map
read atomic.Value // readOnly
// 写map
dirty map[interface{}]*entry
misses int
}
Load方法:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21func (m *Map) Load(key interface{}) (value interface{}, ok bool) {
read, _ := m.read.Load().(readOnly)
e, ok := read.m[key]
if !ok && read.amended {
m.mu.Lock()
// 避免在我们加锁的时候,key写入了读map中
// amended 标记dirty map中含有read map中不存在的数据
read, _ = m.read.Load().(readOnly)
e, ok = read.m[key]
if !ok && read.amended {
e, ok = m.dirty[key]
// 记录miss数,到达dirty长度的时候进行替换
m.missLocked()
}
m.mu.Unlock()
}
if !ok {
return nil, false
}
return e.load()
}
Store方法: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
27func (m *Map) Store(key, value interface{}) {
read, _ := m.read.Load().(readOnly)
if e, ok := read.m[key]; ok && e.tryStore(&value) {
return
}
// 没有在read map中读取到值,或者读取到值但是更新失败
m.mu.Lock()
read, _ = m.read.Load().(readOnly)
if e, ok := read.m[key]; ok {
if e.unexpungeLocked() {
// 读到的值为删除状态,写入dirty map中
m.dirty[key] = e
}
e.storeLocked(&value)
} else if e, ok := m.dirty[key]; ok {
// dirty map中有这个值,进行更新
e.storeLocked(&value)
} else {
//
if !read.amended {
m.dirtyLocked()
m.read.Store(readOnly{m: read.m, amended: true})
}
m.dirty[key] = newEntry(value)
}
m.mu.Unlock()
}
Kafka是一个分布式的消息系统,作为消息系统,他具备系统解耦,冗余存储,流量削峰,缓冲、异步通信等功能。同时他还是一个存储系统和流式处理平台,作为存储系统他能够把消息持久化到磁盘,降低数据丢失的风险;作为流式处理平台,不仅能够为各个流式处理框架提供稳定的数据来源,还提供了一些流式处理的库
Kafka中的ISR、AR又代表什么?ISR的伸缩又指什么?
分区中的所有副本统称为AR(Assigned Replicas)。所有与 leader副本保持一定程度同步的副本(包括 leader副本在内)组成ISR(In-Sync Replicas),ISR集合是AR集合中的一个子集。消息会先发送到 leader副本,然后 follower副本才能从 leader副本中拉取消息进行同步,同步期间内 follower副本相对于 leader副本而言会有一定程度的滞后。前面所说的“一定程度的同步”是指可忍受的滞后范围,这个范围可以通过参数进行配置。与 leader副本同步滞后过多的副本(不包括 leader副本)组成OSR(Out-of-Sync Replicas),由此可见,AR=ISR+OSR。在正常情况下,所有的 follower副本都应该与 leader副本保持一定程度的同步,即AR=ISR,OSR集合为空。
ISR的伸缩是指leader副本负责跟踪ISR集合中所有follower副本的滞后状态,有follower副本滞后太多的时候将他从ISR中剔除,OSR集合中有follower副本”追上“了leader副本将其加入ISR集合中
Kafka中的HW、LEO、LSO、LW等分别代表什么?
HW是 High Watermark的缩写,俗称高水位,它标识了一个特定的消息偏移量(offset),消费者只能拉取到这个offset之前的消息。
LEO是 Log End Offset的缩写,它标识当前日志文件中下一条待写入消息的 offset。LEO的大小相当于当前日志分区中最后一条消息的 offset值加1。分区ISR集合中的每个副本都会维护自身的LEO,而ISR集合中最小的LEO即为分区的HW,对消费者而言只能消费HW之前的消息。
LSO(Last Stable Offset) 对未完成的事务而言,LSO 的值等于事务中第一条消息的位置(firstUnstableOffset),对已完成的事务而言,它的值同 HW 相同
LW:Low Watermark 低水位, 代表 AR 集合中最小的 logStartOffset 值
Kafka中是怎么体现消息顺序性的?
通过Topic和Partition来提现,Topic是消息归类的主题是逻辑概念,Topic下有多个Partition,其在存储层面可以看成是一个可追加的日志文件,消息在被追加到Partition日志文件中的时候会分配一个特定的偏移量(offset)。offset是消息在分区中的唯一标识,Kafka通过offset来保证消息在分区内的顺序性
Kafka中的分区器、序列化器、拦截器是否了解?它们之间的处理顺序是什么?
分区器、序列化器、拦截器都是生产者客户端中的东西。
分区器是将消息发送给指定的分区的,如果在发送的消息中指定了Partition,就不需要分区器了,默认的分区器中对key进行哈希(采用MurmurHash2算法,具备高运算性能及低碰撞率),同时也可以自定义分区器
序列化器把消息对象转换成字节数组,这样才能够通过网络发送给Kafka。
生产者拦截器既可以用来在消息发送前做一些准备工作,比如按照某个规则过滤不符合要求的消息、修改消息的内容等,也可以用来在发送回调逻辑前做一些定制化的需求,比如统计类工作。
他们之间工作的顺序是 拦截器 -> 序列化器 -> 分区器
Kafka生产者客户端的整体结构是什么样子的?
Kafka生产者客户端中使用了几个线程来处理?分别是什么?
使用了两个线程来进行处理:主线程和Sender线程。
主线程负责由KafkaProducer创建消息,通过拦截器、序列化器和分区器作用以后缓存到消息累加器;Sender线程负责从消息累加器中获取消息并将其发送到Kafka中
“消费组中的消费者个数如果超过topic的分区,那么就会有消费者消费不到数据”这句话是否正确?如果正确,那么有没有什么hack的手段?
是正确的,可以让多个消费线程消费同一个分区来hack,通过assign()、seek()等方法实现,这样可以打破原有的消费线程的个数不能超过分区的限制。
消费者提交消费位移时提交的是当前消费到的最新消息的offset还是offset+1?
提交的是offset + 1,表示下一条需要拉取的消息的位置
有哪些情形会造成重复消费?
位移提交动作在消费完所有拉取到的消息后才执行会造成重复消费,因为批量拉取一批消息以后,中间消费处理的过程中可能会出现异常,这样的会前面消费的消息就会再消费一次
Rebalance 的时候也会出现,一个分区在原有消费者的位移没有上传的时候分配给另外一个消费者,另外一个消费会从上一次位移的地方继续拉取消息进行消费,这样就造成了消息的重复消费,后续可以通过添加Rebalance的监听器来做一些Rebalance的工作来解决位移未提交的问题
那些情景下会造成消息漏消费?
位移提交动作在消费消息之前会造成消息漏消费,因为提交新的位移以后,可能拉取到的那一批消息中间出现异常,那么那一批消息后面的那一部分消息就不会被消费到,这样就导致了消息的漏消费
KafkaConsumer是非线程安全的,那么怎么样实现多线程消费?
第一种方式:线程封闭,每个线程实例化一个KafkaConsumer对象,一个消费线程可以消费一个或者多个分区中的消息,所有的消费线程都隶属于同一个消费者组。
第二种方式:多个消费线程同时消费同一个分区,通过assign()、seek()等方法实现,这样可以打破原有的消费线程的个数不能超过分区的限制,进一步提高消费能力。但是这种方式会导致位移提交和顺序控制的处理变得更加复杂。
简述消费者与消费组之间的关系
每一个消费者都属于一个消费者组中,消费者组能够消费到一个主题中的所有消息(实现多播),然后把这些消息通过Partition负载均衡分配给具体的消费者进行消费。
Redis Cluster 作为一个分布式的解决方案,首先要解决的就是把整个数据集按照分区规则映射到多个节点的问题,即把数据集划分到多个节点上,每个节点负责整体数据的一个子集。
常见的分区规则有哈希分区和顺序分区,这两种分区各有各的特点,Redis采用的哈希分区的方法。
对应哈希分区常见有如下几种规则
将key哈希之后对节点数量取余,决定数据最终的映射点。
优点:
问题:
为系统中的每一个节点赋一个token值,这些token构成一个环。读写key的时候,计算key的hash值,然后再环中顺时针寻找离这个key最近的节点。
优点:
问题:
Redis Clust采用的就是虚拟槽分区的规则,首先用一个分散度良好的哈希函数把所有的数据映射到一个固定范围的整数集中,整数定义为槽(slot)。槽是集群内数据 管理和迁移的基本单位。每个节点负责处理指定数量的槽的数据。
Redis Clust采用的哈希函数是CRC16方法,总共分了16383个槽。
优点:
Redis Clust集群相比单机Redis功能上存在一些限制:
Redis集群一般由多个节点组成,节点数量至少为6个才能保证组成完整高可用的集群。建议为集群内所有节点统一目录,一般划分三个目录:conf、 data、log,分别存放配置、数据和日志相关文件。
相比单机模式需要改变的配置如下:1
2
3
4port 6379 //端口
cluster-enabled yes //开启集群模式
cluster-config-file nodes-6379.conf //集群内部的配置文件
cluster-node-timeout 15000 //节点超时时间,单位毫秒
注意到上面配置了集群内部的配置文件,集群配置文件的作用:当集群内节点发生信息变化时,如添加节点、节点下线、故障转移等。节点会自动保存集群的状态到配置文件中。该配置文件由Redis自行维护,不要手动修改,防止节点重启时产生集群信息错乱。
启动所有节点以后可以通过 CLUSTER NODES
查看集群状态
1 | 127.0.0.1:6379> CLUSTER NODES |
节点握手是指一批运行在集群模式下的节点通过Gossip协议彼此通信, 达到感知对方的过程。节点握手是集群彼此通信的第一步,由客户端发起命令:CLUSTER MEET [ip] [port]
Redis Clust的节点中是用clusterNode
表示一个节点的状态的,握手实际上就是两个节点交换clusterNode
的过程,交换的不仅仅是自己的clusterNode
还有之间已经通过握手获取的clusterNode
信息。当集群中所有的节点都互通以后,每一个节点都会拥有整个集群所有节点的clusterNode
信息。
Redis集群把所有的数据映射到16384个槽中。每个key会映射为一个固 定的槽,只有当节点分配了槽,才能响应和这些槽关联的键命令。通过 CLUSTER ADDSLOTS
命令为节点分配槽。
1 | redis-cli -h 127.0.0.1 -p 6379 cluster addslots {0..5461} |
当数据库中的16384个槽都有节点在处理时,集群处于上线状态(ok);相反地,如果数据库中有任何一个槽没有得到处理,那么集群处于下线状态(fail),可以通过CLUSTER INFO
获取到的集群状态查看
Redis集群提供了灵活的节点扩容和收缩方案。在不影响集群对外服务 的情况下,可以为集群添加节点进行扩容也可以下线部分节点进行缩容。
伸缩的重点是槽位的重新分配,和数据的转移。
Redis集群的伸缩操作是由Redis的集群管理软件redis-trib
负责执行的,Redis提供了进行重新分片所需的所有命令,而redis-trib
则通过向源节点和目标节点发送命令来进行重新分片操作。
redis-trib
对集群的单个槽s1ot进行重新分片的步骤如下
redis-trib
对目标节点发送 CLUSTER SETSLOT<sot> IMPORTING<source_id>
命令,让目标节点准备好从源节点导人(Import)属于槽s1ot的键值对。redis-trib
对源节点发送 CLUSTER SETSLOT<s1ot> MIGRATING<target_id>
命令,让源节点准备好将属于槽s1ot的键值对迁移(migrate)至目标节点。redis-trib
向源节点发送 CLUSTER GETKEYSINSLOT<s1ot> <count>
命令,获得最多count个属于槽s1ot的键值对的键名(key name)redis-trib
都向源节点发送一个 MIGRATE <target_ip> <target_port> <key_name> <timeout>
命令,将被选中的键原子地从源节点迁移至目标节点。redis-trib
向集群中的任意一个节点发送 `CLUSTER SETSLOT 在进行集群伸缩期间,源节点向目标节点迁移一个槽的过程中,可能会出现这样一种情况:属于被迁移槽的一部分键值对保存在源节点里面,而另一部分键值对则保存在目标节点里面。
当客户端向源节点发送个与数据库键有关的命令,并且命令要处理的数据库键恰好就属于正在被迁移的槽时:
扩容:
缩容:
CLUST FORGET [NodeId]
参考:
题意非常简单,给出一堆乱序的数,让你找出这堆数中第k大的数
直接对这一堆乱序的数进行排序,然后取出第k大的数。这样做的时间复杂度是O(nlogn)
首先对这些数建一个最大堆,然后再对最大堆堆顶的数pop k次,因为建堆的时间复杂度是O(4n),所以整体的时间复杂度也是O(4n + klogn)
利用快排算法的思想进行变形,每次在数组中随机找一个拆分数,类似快排的思想把这个拆分数放到数组中应该存在的位置,判断拆分数排列第几,如果在k的后面表示第k个数需要在前半部分找;如果在k的前面表示需要后面半部分找。从而使规模一步一步缩小。
最后可以算出时间复杂度期望为O(n),因为这取决于你拆分数的选取。
前面说了快排法能够使得其时间复杂度到达期望为O(n),那能不能使得时间复杂度严格为O(n)呢?可以的,这里就引出了BFPRT算法。
首先我们可以来看一下BFPRT算法的步骤:
原理:
通过这样选到的划分数一定在0.3N-0.7N之间,如图所示
每次选取到的划分数一定都比红色的数要大,比蓝色的数要小
这样时间复杂度的迭代公式就是:
通过推导不难算出其时间复杂度严格为O(n)
求前K大的数就无法做到时间复杂度为O(n)了,但是方法是相似的
依然可以用快排法
只不过在找到第k大的数之后需要对后面比这个数大的数进行一遍排序
参考:
]]>在Go语言中,一个interface{}类型的变量包含两个指针,一个指向其类型,另一个指向真正的值。所以对于一个interface{}类型的nil变量来说,它的两个指针都是0。这是符合Go语言对nil的标准定义的。
当我们将一个具体类型的值赋值给一个interface类型的变量的时候,就同时把类型和值都赋值给了interface里的两个指针。如果这个具体类型的值是nil的话,interface变量依然会存储对应的类型指针和值指针。这个时候拿这个interface变量去和nil常量进行比较的话就会返回false
1 | func main() { |
这个坑可以采取避免将一个有可能为nil的具体类型的值赋值给interface变量尽量避免
在json的规范中,对于数字类型是不区分整形和浮点型的。在使用json.Unmarshal
进行json的反序列化的时候,如果没有指定数据类型(单纯使用interface{}作为接收变量)可能存在丢失精度的问题。因为如果没有指定具体的数据类型,其默认采用的float64作为其数字的接受类型,当数字的精度超过float能够表示的精度范围时就会造成精度丢失的问题。
1 | func main() { |
解决方法是使用func (*Decoder) UseNumber
方法告诉反序列化JSON的数字类型
1 | func main() { |
使用range迭代String的时候要注意,迭代的索引是你迭代的字符的第一个byte(这个字符可能是unicode字符/rune),所以这个索引值不一定是一个自然数的序列。
1 | func main() { |
在struct中小写字母开头的字段将无法encoded,此时你进行encode的时候这些字段将会是零值
1 | type MyData struct { |
参考:
]]>缓存中存储的是数据库中命中的数据,如果没有命中,则会去数据库中去取然后回溯到缓存中。这个时候如果一直访问一个数据库中也没有的数据,就可能造成大量请求直接访问数据库。
缓存中设置了失效时间,如果大量缓存在短时间内一起失效,这样访问的压力就都移到数据库中去了。就像发生雪崩一样。
业务层可能因为一些新闻或者热点事件对一个相同的key在短时间内发出大量请求。如果此时没有命中缓存,就需要构建缓存,但是构建缓存需要时间,在这个时间里面所有对key的请求还是会打到下面的数据库上,造成后端和数据库的压力过大。同时热key本身对缓存系统也会带来非常大的压力。
Redis作为缓存通常是以接口为单位进行存储,也就是说Redis中存储的数据就是接口中返回的东西。
这样能够使得每次请求只有一个原子操作,但是带来的问题就是key会非常大,造成了大key存储的问题。这会导致用这个Redis作为缓存的服务非常不稳定,因为单次请求这个key的读写时间过长数据过多导致后面redis的原子操作可能发生I/O超时,从而导致redis实例命中率波动较大。当缓存命中率比较低的时候,瞬间大量数据请求就会打到数据库,导致带宽不足、数据库压力过大、数据库慢查询过多和平均查询时间变长。
因为我们服务的时间=redis超时时间+数据库慢查询时间+接口逻辑时间,所以大key可能大致整个服务的恶性循环。
可以采用将存储单位从接口数据变为原子数据。每个接口数据都是由原子数据拼接而成,这个拼接可以在业务层完成。从而减小Redis请求超时的情况。
接着上面的问题,当把存储单位从接口数据变成了原子数据以后,势必会造成每个接口的请求数据变多。如果这些请求都通过单个连接去处理,就会造成总体的处理事件过长的问题
这个时候可以使用到批量请求的相关接口:批量存取字符串类型的方式分别为mget、mset、pipeline。三者都会将多次操作合并到一次,也就是进行以此连接返回所有数据。但是之间的差异还是比较明显的,比如:
参考:
]]>githug 是用Ruby开发为了帮助学习使用git的一个闯关游戏。和我之前推荐的学习Linux命令的Bandit游戏有点像。
其Github地址:https://github.com/Gazler/githug
安装过程也比较简单,首先安装好基本的Ruby环境(1.8.7)以上
因为国内可能会遇到用gem安装相关包因为https网络问题报错的情况,如果出现Unable to download data from https://rubygems.org/
这样的报错信息
可以通过如下方式解决1
2sudo gem sources -r https://rubygems.org
sudo gem sources -a http://rubygems.org
当然安装完以后也别忘了设置回来1
2sudo gem sources -r http://rubygems.org
sudo gem sources -a https://rubygems.org
然后进行安装:1
sudo gem install githug
安装完成以后运行githug
会提示创建相关的文件夹,回复y
就行了。
githug有4个基本命令:1
2
3
4play - 默认命令,查看当前状态和题目
hint - 给你当前level的一些提示
reset - 重新开始当前关卡
levels - 列出所有关卡
有一些关卡比较简单,选取一些我觉得有点意思的关卡给出题解吧。
使用一个未来的时间作为提交,提交的时候指定–date选项,能够自定义提交的时间
1 | $ git commit -m "future commit" --date=12.12.2018T22:00:00 |
git reset 常用来进行版本回推。要了解reset
命令首先要了解git中的三棵树:HEAD
:是当前分支引用的指针,它总是指向该分支上的最后一次提交。 这表示 HEAD 将是下一次提交的父结点。暂存区(Index)
:预期的下一次提交工作区
:git 实际管理的文件的目录
reset
命令以一种简单可预见的方式直接操纵三棵树,它做了三个基本操作:移动 HAED,重置索引,重置工作目录。实际上 reset 是以特定的顺序来重写三棵树,并在指定相应选项时停止:
1 | git reset to_commit_second.rb |
使用 reset 命令的 –soft 选项
1 | git reset --soft HEAD~ |
checkout 作用于文件的时候,会将文件从当前HEAD中检出,即让当前工作区的文件变成HEAD中的样子,命令格式为git checkout [-q] [<commit>] [--] <paths>...
1 | git checkout config.rb |
可以通过 git blame
命令查看项目中具体哪一行代码是谁写的,什么时候引入的。
1 | git blame config.rb |
检出到指定标签,通过checkout
命令实现,能够创建一个临时分支指向tag所在的提交
1 | git checkout v1.2 |
有的时候会遇到标签和分支的名字一样的情况,通过在前面添加前缀tags/
来指定是tag的名字
1 | git checkout tags/v1.2 |
检出指定提交到一个新的命名分支
1 | git checkout -b test_branch HEAD~ |
rebase命令可以用来整合来自不同分支
如上图所示,提取在 C4 中引入的补丁和修改,然后在 C3 的基础上再应用一次。在 Git 中,这种操作就叫做 变基
git rebase [<upstream> [<branch>]
命令将upstream的修改在branch上再应用一次
1 | git checkout feature |
在对两个分支进行变基时,所生成的“重演”并不一定要在目标分支上应用,你也可以指定另外的一个分支进行应用。就像 从一个特性分支里再分出一个特性分支的提交历史 中的例子这样。
$ git rebase --onto master server client
以上命令的意思是:“取出 client 分支,找出处于 client 分支和 server 分支的共同祖先之后的修改,然后把它们在 master 分支上重演一遍”。
当master 和server 一致的时候,只需要写一个
1 | git rebase --onto master wrong_branch |
git repack 对松散对象进行打包,凡是有引用关联的对象都被打在包里,未被关联的对象仍旧以松散对象的形式保存
1 | git repack |
应用某一个提交的修改到当前分支
1 | git checkout new-feature |
当涉及提交修改时,应该想到 git rebase -i
命令,它接受可以一个参数(提交的哈希值),它将罗列出此提交之后的所有提交,将此提交作为一个root commit。用途为修改提交历史,其后跟一个某一条提交日志的哈希值,表示要修改这条日志之前的提交历史。
输入命令以后,会将提交从前到后显示。每一行的前面有一个命令词,表示对此次更新执行什么操作。
类似下面这种:
1 | pick 06973a3 First coommit |
前面的命令有以下几种:
这一关是需要把第2次提交错误额comment修改过来,所以对第二次提交执行reword并修改备注就行了
把多次修改合并成一次,这里用到的还是上面的git rebase -i
的命令,只不过是将后面动作命令都变成squash
为了把分支的多次提交合并为主干上的一次提交,可以在merge命令后面加一个 squash 参数
git merge branch-name --squash
过关命令:1
2git merge long-feature-branch --squash
git commit -m "comment"
你提交了几次但是提交顺序多了,想把顺序换一下,这里用到的还是git rebase -i
命令。需要把几次提交的顺序更换一下即可。
在程序持续迭代的过程中不免会引入 bug,除了定位 bug 的代码片断,我们还想知道 bug 是在什么时间被引入的,这时就可以借助 Git 提供的 bisect 工具来查找是哪次提交引入了 bug。bisect 是用二分法来查找的,就像用二分查找法查找数组元素那样。
其查找流程也比较简单,首先确定查找的commit范围,然后在每一次二分查找时给出程序执行是否正确的判断。这个时候bisect就会自动进行二分查找。
1 | $ git bisect start |
用 git add
命令可以把文件添加到暂存区,但如果你不想把文件中的全部修改都提交到暂存区,或者说你只想把文件中的部分修改提交到缓存区,那么你需要加上edit
参数
这时 Git 会自动打开文本编辑器,编辑的内容就是 git diff
命令的结果,这时你就可以编辑2个文件之间的差异,只保留要提交到暂存区的差异,而删除不需要提交到暂存区的差异,然后保存退出,Git 就会按你编辑过的差异把相应的内容提交到暂存区。
1 | git add file-name --edit |
使用git reflog
可以查看历史所在的分支和提交在哪里
git revert
命令能够对某一次提交执行逆操作,这适用于某次提交出现问题的情况,添加--no-edit
能够让系统自动生成备注
1 | git log |
如果你想把别人的仓库代码作为自己项目一个库来使用,可以采用模块化的思路,把这个库作为模块进行管理。Git 专门提供了相应的工具,用如下命令把第三方仓库作为模块引入:
git submodule add module-url
1 | git submodule add https://github.com/jackmaney/githug-include-me |
二分法是一个非常常用的算法技巧,用于在多条排序记录中快速找到待查找的记录。相比遍历需要O(n)将时间复杂度优化到了O(logn),但是需要一组序列本身是有序的。
写二分代码的关键在于处理好其边界情况
下面是一个标准的二分代码: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
28int search(int array[], int n, int v)
{
int left, right, middle;
// 赋初值,判断是左闭右闭还是左闭右开
left = 0, right = n - 1;
// 退出条件,有小于和小于等于两种写法
while (left <= right)
{
// 中值计算,有向下取整和向上取整
middle = (left + right) / 2;
if (array[middle] > v)
{
// 边界值更新,判断是否需要加一
right = middle;
}
else if (array[middle] < v)
{
// 边界值更新,判断是否需要加一
left = middle;
}
else
{
return middle;
}
}
return -1;
}
正如上面的注释中所示,写好一个二分算法只需要注意这4个地方:
左闭右闭还是左闭右开应该对应不同的写法,主要的区别在于最后一个值的舍弃问题
1 | // 左闭右闭 [0, n-1] 写法 |
一般如果向下取整和左边界更新不加一组合就可能造成死循环,同理向上取整和右边界不减一组合也可能造成死循环
在一些比较特殊的情况下middle = (left + right) / 2
这种写法可能造成溢出问题,更加保险的写法是middle = left + (right - left) / 2;
《编程珠玑》中提供了一个给出了比较完成的二分法代码写法:
1 | int search4(int array[], int n, int v) |
小Q的父母要出差N天,走之前给小Q留下了M块巧克力。小Q决定每天吃的巧克力数量不少于前一天吃的一半,但是他又不想在父母回来之前的某一天没有巧克力吃,请问他第一天最多能吃多少块巧克力
可以构造出理想的情况应该是:前面是一个数开始,然后不断除2(向上取整),直到变为1,最后一直持续1。这样把这个序列分为两段:前面类似等比序列,后面都是1
这样我们知道序列的第一个数就知道了以这个数开头的序列总和最少是多少(按照上面的规则计算),这样我们可以从m-n+1
开始一直往1
当做第一个数遍历,找到第一个满足最小总和少于m的数。便能够得到答案。
这种情况和二分法的常见题型是不是很像,找到第一个满足某种情况的数,而且这个序列还是一个单调序列。用一个二分法加速
1 |
|
给出一条河对岸中n个石子的坐标(加上起点和终点),现在移走m个石子,要求两个石子间的最小值最大,这个最大的最小值
这个一道经典的二分题目,最大化最小值问题。在查找过程中判断一个数是否满足条件的时候,我们可以转化成判断满足这个最小值需要移走的石子数,通过跟给定的石子数进行比较来判断这个数是否满足条件,最后通过二分找到这个最大的值。
1 | using namespace std; |
给出包含n个元素的数组,将这n个元素分成最多m段,问各种分法中每段和的最大值得最小值是多少
最小化最大值问题,使用二分进行求解,需要注意的是,在不断二分的时候边界更新的时候,当中间值不满足条件的时候,新的区间应该是[mid+1,r],满足条件的时候新的区间应该是[l,mic].即要排除掉不满足条件的数
1 | using namespace std; |
单调栈是这样的一种数据结构:栈中从栈底到栈顶的数都是递减的,为了维护这种结构在插入比当前栈顶大的数的时候都需要先将栈顶的数弹出,这样我们就能够知道弹出的这个数两边比它大的数了。在某些题目中,单调栈的这种特定能够给我们提供很大的帮助。
Largest Rectangle in Histogram
题意:
给出一个直方图,求直方图中所能够围成矩形的最大面积
题解:
维护一个递增的单调栈,如果需要将栈顶数据弹出则表示形成了山峰的形状,这样我们就可以计算出这个山峰里能够围成的矩形的面积,因为弹出的这一个数能够知道其左右比它小的数。当遍历完以后还有一些矩形需要计算,再将这个栈依次弹出,这个时候右边没有比它小的数了,他左边比它小的数就是栈顶的数。每弹出一个数就能够计算出一个矩形的面积。
代码: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
29class Solution {
public:
// 维护单调栈
int largestRectangleArea(vector<int>& heights) {
int len = heights.size();
stack<int> s;
int res = 0;
for(int i = 0; i < len; i++)
{
while(!s.empty() && heights[s.top()] > heights[i])
{
int tem = s.top();
s.pop();
// 计算面积
int curArea = s.empty() ? i*heights[tem] : (i - s.top() - 1)*heights[tem];
res = max(res, curArea);
}
s.push(i);
}
while(!s.empty())
{
int tem = s.top();
s.pop();
int curArea = s.empty() ? len*heights[tem] : (len - s.top() - 1) * heights[tem];
res = max(res, curArea);
}
return res;
}
};
题意:
给出一个只包含0和1的二维矩阵,求出其中1围成的矩形的最大面积
题解:
这道题可以说是上面那道题的变形,因为把这个n*m的矩阵转换成m个直方图就是上面的那道题,计算m次即可
代码: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
59class Solution {
public:
vector<int> getSlice(vector<vector<char>>& matrix, int h)
{
vector<int> ret;
int len = matrix[0].size();
for(int i = 0; i < len; i++)
{
int tem = h, count = 0;
while(tem >= 0 && matrix[tem][i] == '1')
{
tem --;
count ++;
}
ret.push_back(count);
}
return ret;
}
int maximalRectangle(vector<vector<char>>& matrix) {
int len1 = matrix.size();
if(len1 == 0) return 0;
int len2 = matrix[0].size();
if(len2 == 0) return 0;
int res = 0;
for(int t = 0; t < len1; t++)
{
vector<int> heights = getSlice(matrix, t);
int len3 = heights.size();
// 使用栈来保存一个递增的序列
stack<int> s;
int tem_res = 0;
for(int i = 0; i < len3; i++)
{
while(!s.empty() && heights[s.top()] > heights[i])
{
int tem = s.top();
s.pop();
// 计算不在序列内的区块的面积
int curArea = s.empty() ? i*heights[tem] : (i - s.top() - 1)*heights[tem];
tem_res = max(tem_res, curArea);
// cout<<i<<" "<<tem_res<<endl;
}
s.push(i);
}
// 对递增序列中的序列面积进行计算
while(!s.empty())
{
int tem = s.top();
s.pop();
int curArea = s.empty() ? len3*heights[tem] : (len3 - s.top() - 1) * heights[tem];
tem_res = max(tem_res, curArea);
// cout<<tem<<" "<<tem_res<<endl;
}
res = max(res, tem_res);
}
return res;
}
};
题意:
给出一个数组,判断是否存在这样的三个数:i < j < k,同时a[i] < a[k] < a[j] 类似于132的组合
题解:
从后往前去维护一个递减的单调栈,同时记录淘汰掉的数的最大值s3(这里相当于a[k]),从后往前遍历的过程中如果有数比s3要小,则表明存在上述的结构
代码:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21class Solution {
public:
// 单调栈结构巧妙解法
bool find132pattern(vector<int>& nums) {
stack<int> s;
int len = nums.size();
if (len < 3) return false;
int s3 = INT_MIN;
for(int i = len - 1; i >= 0; i--)
{
if(nums[i] < s3) return true;
while(!s.empty() && nums[i] > s.top())
{
s3 = max(s3, s.top());
s.pop();
}
s.push(nums[i]);
}
return false;
}
};
单调队列能够维护一个滑动窗口的最大值或者最小值
一般队列中存放的是数组中的index。以最大值为例,队列加数时:判断最后一个数是不是小于等于当前数,如果如果是就弹出最后这个数,不断重复直到无法弹出最后那个数后加当前数加入队尾,使得这个队列中是单调的。队列减数:通过index的差值判断在不在窗口内,通过差值计算是否值是否过期。
典型的滑动窗口内的最大值问题,这里要求每一个滑动窗口的最大值并加入到一个动态数组中
1 | class Solution { |
Redis现在使用得非常广泛,但是Redis单例比较大的局限——单例使用到的内存一般最多10~20GB。这无法支撑大型线上业务系统的需求。而且也会造成资源的利用率过低——现在企业使用的内存肯定是Redis单例使用到内存的好几倍。
为了解决单机承载能力不足的问题,就必然会使用到多个Redis构成的集群。
客户端分片将分片工作放到了业务层,程序代码根据预先设置的路由规则,直接对多个Redis实例进行分布式访问。
这样的好处是,不依赖于第三方分布式中间件,实现方法和代码都自己掌控,可随时调整,不用担心踩到坑;同时,这种分片机制的性能比代理式更好(少了一个中间分发环节)。
但是缺点也比较多:首先这是一种静态分片技术。Redis实例的增减,都得手工调整分片程序,其次,虽然少了中间分发环节,但是导致升级麻烦,对研发人员的个人依赖性强——需要有较强的程序开发能力做后盾。
所以,这种方式下,可运维性较差。出现故障,定位和解决都得研发和运维配合着解决,故障时间变长。
代理分片将分片工作交给专门的代理程序来做。代理程序接收到来自业务程序的数据请求,根据路由规则,将这些请求分发给正确的Redis实例并返回给业务程序。因此整个代理对业务层是透明的,业务层只需要把这个一个单纯的Redis实例使用即可。同时因为Redis请求都打到了代理上,我们很容易在代理的基础上进行进一步的分析工作。
虽然会因此带来些性能损耗,但对于Redis这种内存读写型应用,相对而言是能容忍的。
基于该机制的开源产品Twemproxy,便是其中代表之一,应用非常广泛。
在这种机制下,没有中心节点(和代理模式的重要不同之处)。
这样的好处是,Redis Cluster将所有Key映射到16384个Slot中,集群中每个Redis实例负责一部分,业务程序通过集成的Redis Cluster客户端进行操作。客户端可以向任一实例发出请求,如果所需数据不在该实例中,则该实例引导客户端自动去对应实例读写数据。
但是缺点也是同样存在,这是一个非常重的方案,Redis Cluster的成员管理(节点名称、IP、端口、状态、角色)等,都通过节点之间两两通讯,定期交换并更新。缺少了Redis单例的“简单、可依赖”的特点
Twemproxy是上面代理分片构造Redis集群的一个开源解决方案。
TwemProxy采用中间层代理的方式,在不改动服务器端程序的情况下,使得集群管理更简单、轻量和有效。Twemproxy 通过引入一个代理层,将其后端的多台 Redis实例进行统一管理与分配,使应用程序只需要在Twemproxy 上进行操作,而不用关心后面具体有多少个真实的 Redis实例。
因为Twemproxy本身是单点,所以需要用Keepalived做高可用方案。
Keepalived是一种实现高可用的方案,它的功能主要包括两方面:
1)通过IP漂移,实现服务的高可用:服务器集群共享一个虚拟IP,同一时间只有一个服务器占有虚拟IP并对外提供服务,若该服务器不可用,则虚拟IP漂移至另一台服务器并对外提供服务;
2)对LVS应用服务层的应用服务器集群进行状态监控:若应用服务器不可用,则keepalived将其从集群中摘除,若应用服务器恢复,则keepalived将其重新加入集群中。
在数组中碰到数组和为定值的大致可以分为这两类,一类是这些数不连续,从两个数和为定值到多个数和为定值,最后升级到动态规划的多重部分和问题;另一类是数必须是连续的子数组问题
这类题应该是最常见的题型了,常见的有两种方法:
a[i] + a[j]
与target的大小情况,大于target则j–,小于target则i++,如果数组是有序时间复杂度为O(n),如果数组不是有序的时间复杂度为O(nlogn)题意
给定有序的一个数组,求其中两个数的和刚好为定值target,返回这两个数的索引值
代码:1
2
3
4
5
6
7
8
9
10
11
12
13class Solution {
public:
vector<int> twoSum(vector<int>& nums, int target) {
unordered_map<int, int> m;
for (int i = 0; i < nums.size(); ++i) {
if (m.count(target - nums[i])) {
return {i, m[target - nums[i]]};
}
m[nums[i]] = i;
}
return {};
}
};
对于求解数组中m个数的和为定值的问题,枚举最开始的一个数都可以转换为m-1个数和为定值的问题,其最优的时间复杂度为O(n^m)。因为m如果大于2,排序的开销就不算在里面了,所以采用双指针的方法更加简单
题意:
求数组中3个数的和为定值的这个3个数的索引值
代码: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
36class Solution {
public:
vector<vector<int>> threeSum(vector<int>& nums) {
vector<vector<int>> res;
sort(nums.begin(),nums.end());
int len = nums.size();
for(int i=0;i<len;i++)
{
if(i==0 || nums[i]!=nums[i-1]) // 去重
{
for(int j=i+1,k=len-1;j<k;)
{
if(nums[i]+nums[j]+nums[k] == 0)
{
vector<int> tmp = {nums[i],nums[j],nums[k]};
res.push_back(tmp);
j++;
while(j<k && nums[j]==nums[j-1]) j++;
k=len-1;
}
else if(nums[j]+nums[k]+nums[i]>0)
{
k--;
}
else
j++;
}
}
}
return res;
}
};
进阶可以转换成一个DP的题:有 n 种大小不同的数字 a[i],每种 m[i] 个,判断是否可以从这些数字中选出若干个使他们的和恰好为 K。
设 dp[i+1][j] 为前 i 种数加和为 j 时第 i 种数最多能剩余多少个。(不能得到为-1)
这样状态转移方程为:
模板代码:1
2
3
4
5
6
7
8
9
10
11
12
13
14int a[maxn],m[maxn],dp[maxm]; //a表示数,m表示数的个数,dp范围是所有数的和的范围
bool fun(int n,int K) //n表示数字种类,K表示组成的和
{
memset(dp, -1, sizeof(dp));
dp[0] = 0;
for(int i=0; i<n; ++i) { //根据存储方式作出改变
for(int j=0; j<=K; ++j) {
if(dp[j] >= 0) dp[j] = m[i]; // 前i-1个数已经能凑成j了
else if(j < a[i] || dp[j-a[i]] <= 0) dp[j] = -1; // 否则,凑不成j或者a[i]已经用完,则无法满足
else dp[j] = dp[j-a[i]] - 1; // 否则可以凑成
}
}
return dp[K]>=0;
}
这个题应该是比较基础的一道题:因为数组和一定递增的,所以采用滑动窗口的思想,维护滑动窗口的两个指针i和j,如果当前窗口和小于target时j++,如果当前窗口和大于target时i++
还是遍历一遍数组,使得总体的时间复杂度为O(n),同时记录从第一个数到当前位置数的和为一张hash表,这个表对应的映射项可以是最早出现这个sum的index(以此来求最长子数组的长度),也可以是对应这个sum出现的次数(对应求满足条件的子数组个数)
给定一个整数数组和一个整数 k,你需要找到该数组中和为 k 的连续的子数组的个数
代码:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17class Solution {
public:
int subarraySum(vector<int>& nums, int k) {
int len = nums.size();
unordered_map<int, int> m;
int sum = 0, res = 0;
m[0] = 1;
for(int i = 0; i < len; i++)
{
sum += nums[i];
// 加上剩余的值出现的次数
res += m[sum - k];
m[sum] ++;
}
return res;
}
};
在上面那道题的基础上改为求满足条件的最长子数组的长度
1 | class Solution { |
这里用到了两个辅助数组:min_value
、min_index
:min_value[i]
表示以i位置开始往后加的最小累加和;min_index
表示min_value
对应的最小累加和的右边界,举个例子:
1 | arr 5 4 -3 -1 |
这两个辅助数组是能够在O(n)时间复杂内计算出来的:倒序遍历,min_value[i] 只需要判断min_value[i+1]的值是不是负数,如果是负数就加上,不是就到本身这里结尾。得到这样一个数组以后我们就可能轻易得到从某一个位置开始和最小的子数组。
有了这两个辅助数组以后,就可以采用滑动窗口的思想,左右两个指针都不回退,右指针以上面辅助数组进行累加,左指针正常遍历,使得总体的时间复杂度为O(n)
1 | class Solution { |
NSQ主要包含3个组件:
单个nsqd可以有多个Topic,每个Topic又可以有多个Channel。Channel能够接收Topic所有消息的副本,从而实现了消息多播分发;而Channel上的每个消息被分发给它的订阅者,从而实现负载均衡,所有这些就组成了一个可以表示各种简单和复杂拓扑结构的强大框架。
Topic & Partition
Topic在逻辑上可以被认为是一个queue,每条消费都必须指定它的Topic,可以简单理解为必须指明把这条消息放进哪个queue里。为了使得Kafka的吞吐率可以线性提高,物理上把Topic分成一个或多个Partition,每个Partition在物理上对应一个文件夹,该文件夹下存储这个Partition的所有消息和索引文件。
Producer消息路由
Producer发送消息到broker时,会根据Paritition机制选择将其存储到哪一个Partition。如果Partition机制设置合理,所有消息可以均匀分布到不同的Partition里,这样就实现了负载均衡。
在发送一条消息时,可以指定这条消息的key,Producer根据这个key和Partition机制来判断应该将这条消息发送到哪个Parition。消息在Partition中是有序的,同时一个Partition短时间内会提供给特定下游消费的Consumer 消费,这样可以提供业务中某些场景的有序保证。
Consumer Group
使用Consumer high level API时,同一Topic的一条消息只能被同一个Consumer Group内的一个Consumer消费,但多个Consumer Group可同时消费这一消息。
这是Kafka用来实现一个Topic消息的广播(发给所有的Consumer)和单播(发给某一个Consumer)的手段。一个Topic可以对应多个Consumer Group。如果需要实现广播,只要每个Consumer有一个独立的Group就可以了。要实现单播只要所有的Consumer在同一个Group里。用Consumer Group还可以将Consumer进行自由的分组而不需要多次发送消息到不同的Topic。
Python代码的执行由Python虚拟机(解释器)来控制。Python在设计之初就考虑要在主循环中,同时只有一个线程在执行,就像单CPU的系统中运行多个进程那样,内存中可以存放多个程序,但任意时刻,只有一个程序在CPU中运行。同样地,虽然Python解释器可以运行多个线程,只有一个线程在解释器中运行。对Python虚拟机的访问由全局解释器锁(GIL)来控制,正是这个锁能保证同时只有一个线程在运行。
在多线程环境中,Python虚拟机按照以下方式执行。
对所有面向I/O的(会调用内建的操作系统C代码的)程序来说,GIL会在这个I/O调用之前被释放,以允许其他线程在这个线程等待I/O的时候运行。如果某线程并未使用很多I/O操作,它会在自己的时间片内一直占用处理器和GIL。也就是说,I/O密集型的Python程序比计算密集型的Python程序更能充分利用多线程的好处。
我们都知道,比方我有一个4核的CPU,那么这样一来,在单位时间内每个核只能跑一个线程,然后时间片轮转切换。但是Python不一样,它不管你有几个核,单位时间多个核只能跑一个线程,然后时间片轮转。看起来很不可思议?但是这就是GIL搞的鬼。任何Python线程执行前,必须先获得GIL锁,然后,每执行100条字节码,解释器就自动释放GIL锁,让别的线程有机会执行。这个GIL全局锁实际上把所有线程的执行代码都给上了锁,所以,多线程在Python中只能交替执行,即使100个线程跑在100核CPU上,也只能用到1个核。通常我们用的解释器是官方实现的CPython,要真正利用多核,除非重写一个不带GIL的解释器。
Python中有三种模式实现多线程:继承Thread类、Thread对象和multiprocessing.dummy线程池
继承Thread类,通过重写它的run方法实现多线程:
1 | #!/usr/bin/python |
需要注意的是:
Thread.__init__(self)
这句话1 | #!/usr/bin/python |
需要注意的是:
1 | #!/usr/bin/python |
注意:
Python 多进程的实现也有三种:继承自multiprocessing.Process类、multiprocessing.process对象和multiprocessing pool进程池
这里和多线程第一种实现方式一样
1 | import multiprocessing |
1 | from multiprocessing import Process |
1 | from multiprocessing import Pool |
什么是Minimax,它是用在决策轮、博弈论和概率论中的一条决策规则。它被用来最小化最坏情况下的可能损失。“最坏”情况是对手带来的最坏情况,“最小”是我要执行的一个最优策略的目标。
实际使用中一般,DFS来遍历当前局势以后所有可能的结果,通过『最大化』自己和『最小化』对手的方法获取下一步的动作。
给定一个数组,双方轮流从数组的两边取出一个数,判断最后谁取的数多。
这是一个博弈问题,站在我的角度一定是要使自己的收益最大,但是站在对方的角度一定是要使我的收益最小。此时我们可以用f[i][j]表示我方在i~j这个数组下的收益,s[i][j]表示对方从两边拿了一个数以后我方的收益。此时不难得出状态转移方程:f[i][j] = max(nums[i] + s[i+1][j], nums[j] + s[i][j-1])
和 min(f[i+1][j], f[i][j-1])
代码:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21class Solution {
public:
// Minimax 算法
bool PredictTheWinner(vector<int>& nums) {
int len = nums.size();
if(len == 0) return true;
vector<vector<int> > f(len, vector<int>(len, 0)), s(len, vector<int>(len, 0));
int sum = 0;
for(int i : nums) sum += i;
for(int j = 0; j < len; j++)
{
f[j][j] = nums[j];
for(int i = j-1; i >= 0; i--)
{
f[i][j] = max(nums[i] + s[i+1][j], nums[j] + s[i][j-1]);
s[i][j] = min(f[i+1][j], f[i][j-1]);
}
}
return f[0][len-1] >= (sum+1)/2;
}
};
题意:某人从1~n中选一个数k,你每次给出一个数x,他会告诉你x与n的关系(大于,小于或等于),每次询问你都需要花费x的代价,问你至少需要花费多少钱才能保证查找到k是多少。
一道比较典型的Minimax题目,最小化最大值,当确定中间的一个数x的时候,为了保证找到k一定是选取两边的代价中最大的。但是你可以选取这个x时,你可以选取一个代价最小的x。dp[i][j]表示从i到j猜出值所需要的代价,这时我们可以得到状态转移方程:dp[i][j] = min(x + max(dp[i][k-1], dp[k+1][j]) ) {i <= k <= j}
代码: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
29class Solution {
public:
// Minimax算法,dp思路
int getMoneyAmount(int n) {
vector<vector<int> > dp(n + 1, vector<int>(n + 1, INT_MAX));
for(int i = 0; i <= n; i++)
for(int j = 0; j <= n; j++)
{
if(i == j) dp[i][j] = 0;
else if (i > j) dp[i][j] = 0;
}
for(int i = 1; i <= n; i++)
for(int j = 0; j <= n-i; j++)
{
int tem = INT_MAX;
for(int k = j; k <= j+i; k++)
{
if(k == 0)
tem = min(tem, k + dp[k+1][j+i]);
else if(k == n)
tem = min(tem, k + dp[j][k-1]);
else
tem = min(tem, k + max(dp[j][k-1], dp[k+1][j+i]));
}
dp[j][j+i] = tem;
}
return dp[0][n];
}
};
题意:给定两个数m和target,两人依次从1到m的m个数中取出一个数,当轮到的人取出一个数以后使得所有取出的数不小于target这个人就获胜了,判断第一个取的能不能取得游戏的胜利
按照Minimax的思路,当我方作出决策的时候一定作出的是最我方损失最小的决策。当我方所在一个状态数组(1到m中各个数的取出状态数组)和一个target的时候,这个时候我们要做的是从这个状态数组中标记一个数为拿出状态,使得其大于target或者使得轮到对方决策后一定是输。
这里比较棘手的就是这个状态数组了,但是题目给了一个条件,m的值不会超过20个,这个时候我们就可以做一个状态压缩——用status一个数表示整个状态数组:那么这个时候我们就可以得到状态转移方程:dp[n][status] = ((1 << x) & status) == 0 && (x >= n || dp[n-x][status | (1 << x)])
,其中((1 << x) & status)
表示当前选择的数x是否被选择过;status | (1 << x)
表示选择了x以后的状态数组的状态
虽然得到状态转移方程,但是我们不好通过遍历求解,这个时候就可以将动态规划“退化”成递归加上状态记录。这里dp按理说是一个二维数组,但是status和n是有关系的,n表示的是总数减去其取出的数。这里使用map来表示这个dp数组,因为能够表示出三种状态:没有访问过、true、false
代码: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
30class Solution {
private:
int n;
map<int, bool> dp;
public:
// Minimax算法思想,最小化对手的收益
bool canIWin(int maxChoosableInteger, int desiredTotal) {
n = maxChoosableInteger;
if(n >= desiredTotal) return true;
if((1 + n) * n / 2 < desiredTotal) return false;
return solute(desiredTotal, 0);
}
// 因为status和nowNum是有关联关系的,所以map中需要一个
bool solute(int nowNum, int status) {
if(dp.find(status) != dp.end()) return dp[status];
for(int i = 1; i <= n; i++)
{
int tem = (1 << i);
if((tem & status) == 0 && (i >= nowNum || !solute(nowNum - i, tem | status)))
{
dp[status] = true;
return true;
}
}
dp[status] = false;
return false;
}
};
ProxyChains的功能就是Hook 了 sockets 相关的操作,让普通程序的 sockets 数据走 SOCKS/HTTP 代理。其在实现部分主要是重写了部分socket函数。
其能够在同一条代理链中整合不同类型的代理:1
your_host <-->socks5 <--> http <--> socks4 <--> target_host
源码编译安装
1 | // 下载源码 |
Mac 安装
因为macOS 10.11 后开启了 SIP(System Integrity Protection) 会导致命令行下 proxychains-ng 代理的模式失效。所以要安装ProxyChains首先需要关闭SIP功能
重启Mac,按住Option键进入启动盘选择模式,再按⌘ + R进入Recovery模式。
实用工具(Utilities)-> 终端(Terminal)。
输入命令csrutil enable --without debug
运行。
重启进入系统后,终端里输入 csrutil status,结果中如果有 Debugging Restrictions: disabled 则说明关闭成功。
重启Mac,按住Option键进入启动盘选择模式,再按⌘ + R进入Recovery模式。
实用工具(Utilities)-> 终端(Terminal)。
输入命令csrutil disable
运行。
重启进入系统后,终端里输入 csrutil status,结果中如果有 System Integrity Protection status:disabled. 则说明关闭成功。
关闭以后通过brew
进行安装就行了
1 | $ brew install proxychains-ng |
proxychains-ng默认配置文件名为proxychains.conf
Homebrew
安装的默认为/usr/local/etc/proxychains.conf配置只需要将代理加入[ProxyList]中:
1 | [ProxyList] |
在你需要进行代理的页面前面加上proxychains4
即可
1 | $ proxhchains4 curl www.google.com |
Mac用户可能会觉得关闭SIP会造成一些安全隐患,这个时候可以使用Mac下的一个工具:Proxifier
Proxifier可以设定Mac上不同的应用走不同的代理,我们把我们平常需要的一些终端应用设置走指定的代理就行了
打开Proxifier,打开Proxies->Add
,输入地址和端口号添加对应的sock5代理
在Rules
模块中,我们可以设置指定应用、目标主机、目标端口走我们刚才添加的代理
需要注意的是,给我们提供的代理的Shadowsocks要设置成直接连接不能加入代理中,否则会造成整个代理链成了一条环,最后上不了网。
设置以后就可能在终端中享受代理服务了~
]]>Go是Google开发的一种静态强类型、编译型、并发型,并具有垃圾回收功能的编程语言。Go语言号称集多数编程语言的优势于一身,具有较高的生产效率、先进的依赖管理和类型系统,以及原生的并发计算支持。
Go语言的语法接近C语言,但对于变量的声明有所不同。Go语言支持垃圾回收功能。Go语言的并行模型是以东尼·霍尔的通信顺序进程(CSP)为基础,采取类似模型的其他语言包括Occam和Limbo,但它也具有Pi运算的特征,比如通道传输。
与C++相比,Go语言并不包括如异常处理、继承、泛型、断言、虚函数等功能,但增加了 Slice 型、并发、管道、垃圾回收、接口(Interface)等特性的语言级支持
部署简单。Go 是一个编译型语言,Go 编译生成的是一个静态可执行文件,除了 glibc 外没有其他外部依赖。
并发性好。Goroutine 和 channel 使得编写高并发的服务端软件变得相当容易,很多情况下完全不需要考虑锁机制以及由此带来的问题。
代码风格强制统一
Go语言语法趋于脚本化,比较简洁,但Go是编译型语言而非解释型语言。
Go语言使用垃圾自动回收机制(GC),GC是定时自动启动,人工可做稍微的干预。
在Go语言中处理错误的基本模式是:函数通常返回多个值,其中最后一个值是error类型,用于表示错误类型极其描述;调用者每次调用完一个函数,都需要检查这个error并进行相应的错误处理:if err != nil { /*这种代码写多了不想吐么*/ }。此模式跟C语言那种很原始的错误处理相比如出一辙,并无实质性改进。
Go 语言的软件包管理绝对不是完美的。默认情况下,它没有办法制定特定版本的依赖库,也无法创建可复写的 builds。相比之下 Python、Node 和 Ruby 都有更好的软件包管理系统。然而通过正确的工具,Go 语言的软件包管理也可以表现得不错。
找到两个口碑比较好的入门Go语言的教程:
Go在Linux上的配置比较简单,无非就是下载一个二进制文件,然后添加一下环境变量。
Go在mac上推荐使用brew进行安装,使用官网的安装包进行安装因为苹果对一些目录的保护,后面在安装其他库的时候可能会存在问题
具体可以参考官方配置文档
在Redis这种一对多的服务模式下,每个客户端可以向服务器发送命令请求,而服务器则接收并处理客户端发送的命令请求,并向客户端返回命令回复。通过使用由I/O多路复用技术实现的文件事件处理器,Redis服务器使用单线程单进程的方式来处理命令请求,并与多个客户端进行网络通信。
客户端底层的数据结构如下:
1 | typedef struct redisClient { |
在客户端的各个属性中:
fd表示套接字描述符,伪客户端的fd属性的值为-1:伪客户端处理的命令请求来源于AOF文件或者Lua脚本,而不是网络,所以这种客户端不需要套接字连接;普通客户端的fd属性的值为大于-1的整数
命令和命令参数是对输入缓冲的命令进行解析以后获得命令和参数。
cmd
是命令的实现函数的数组,命令实现函数的结构如下:
1 | struct redisCommand { |
当客户端向服务器发出connect请求的时候,服务器的事件处理器就会对这个事件进行处理,创建相应的客户端状态,并将这个新的客户端状态添加到服务器状态结构clients链表的末尾
1 | /* |
对于客户端的启动程序,其大致的逻辑是:读取本地配置,连接服务器获取服务器的配置,获取本地输入的命令并发送到服务器
一个普通客户端可以因为多种原因而被关闭:
关闭客户端的底层实现:
1 | /* |
从客户端输入一条指令到服务端完成命令的内容并返回要经历以下这些步骤:
Redis服务器中的 serverCron函数默认每隔100毫秒执行一次,这个函数负责管理服务器的资源,并保持服务器自身的良好运转
因为serverCron的实现代码太过冗长,所以这里就简单说一些serverCron函数都干了哪些事情
Redis服务器中许多的操作都需要用到当前的系统时间属性unixtime
,serverCron会更新这个时间属性
Reids服务器中实现过期键的删除需要计算其空转时间,计算空转时间需要用LRU时钟,serverCron会更新这个时钟保证Redis过期键删除功能的正常使用
Redis中使用了一个属性stat_peak_memory
记录了使用内存的峰值,这个属性需要serverCron进行更新
在启动服务器时, Redis会为服务器进程的 SIGTERM信号关联处理器 sigtermhandier函数,这个信号处理器负责在服务器接到 SIGTERM信号时,打开服务器状态的 shutdown_asap标识。
]]>KMP算法解决的是两个字符串的匹配问题(一个字符串是不是另外一个字符串的子串)
暴力法所需要的时间复杂度是O(n*m),KMP算法能够优化到O(n)。KMP算法的核心是使用一个next数组实现匹配的加速
最长前缀后缀:一个字符串中所有的前缀和其所有的后缀中最长的相等的长度,比如说“abcabc”的最长前缀后缀为”abc”
给定一个串s的next数组next[],其每一位next[i]表示串s[0…i-1]中最长前缀后缀
使用next数据对字符串匹配进行加速:s1中查找是否有子串s2,如果s1[i]匹配到s2[j]的时候不相等,并且此时next[j]=k,此时不需要重新回到s1[1]继续进行匹配,而是用s2[k]继续和s1[i]进行匹配,依次类推
因为next数组是找到了最长前缀后缀的,所以其能够从最长前缀的匹配跳到最长后缀的匹配,因为中间不可能出现匹配的情况,如果出现匹配那么表示当前next计算的不可能是”最长“前缀后缀。
规定next[0] = -1
, 因为其前面没有字符串;next[1] = 0
,因为其前面的字符串中只有一个字符,后面的next值的计算取决于前面的next值:
判断当前位置的字符和最长前缀的后一个字符是否相等,如果相等则next[i] = next[i-1] + 1
,如果不等再判断最长前缀的最长前缀和其相等不相等,如果还不相等就继续找最长前缀,直到最长前缀长度为0的时候表示没有找到,这个时候next[i] = 0
1 | class Solution { |
Manacher 算法解决的是字符串中最长的回文子串的问题
暴力法(技巧:中间添加辅助字符)解决这个问题的时间复杂度为O(n^2),Manacher算法能够时间复杂度优化为O(n)
和暴力解法一样从左向右扩充判断,有以下几种情况:
1 | class Solution { |
第一篇概述主要从大型网站的架构演化、架构模式和核心构架要素三个方面对大型网站技术架构进行了一个综合性的概述。
大型网站架构演化发展历程主要经过以下几个阶段:
架构模式中讲了9种解决大型网站一系列问题的解决方案:
核心架构要素中讲了5个核心架构要素:
后面的内容比较多,我就整理成思维导图的形式吧
展开的高清大图可以看这里
]]>Redis服务器是一个事件驱动程序,服务器需要处理以下两类事件
Redis基于Reactor模式开发了自己的网络事件处理器:这个处理器被称为文件事件处理器( fle event handler)
虽然文件事件处理器以单线程方式运行,但通过使用I/O多路复用程序来监听多个套接字,文件事件处理器既实现了高性能的网络通信模型,又可以很好地与 Redis服务器中其他同样以单线程方式运行的模块进行对接,这保持了 Redis内部单线程设计的简单性。
在Redis中事件结构体的定义如下:1
2
3
4
5
6typedef struct aeFileEvent {
int mask; // 读or写标记
aeFileProc *rfileProc; // 读处理函数
aeFileProc *wfileProc; // 写处理函数
void *clientData; // 私有数据
} aeFileEvent;
针对事件的创建和删除的API有:1
2
3
4
5
6
7// 创建文件事件
int aeCreateFileEvent(aeEventLoop *eventLoop, int fd, int mask,
aeFileProc *proc, void *clientData);
// 删除文件事件
void aeDeleteFileEvent(aeEventLoop *eventLoop, int fd, int mask);
// 根据文件描述符获取文件事件
int aeGetFileEvents(aeEventLoop *eventLoop, int fd);
这些接口的实现都比较简单,就是在eventLoop
这个事件池中创建(删除)指定属性的事件
需要使用到事件的创建的地方有两个:
在Linux/Unix中实现I/O多路复用的方法有非常多,大致有select、 epoll、 export和 kqueue这些IO多路复用函数库来实现的
各种实现的性能也是不一样的,之前我写了一篇博客对比了三种I/O多路复用
在Redis中,其会根据具体底层操作系统的不同自动选择系统中性能最高的I/O多路复用函数库来作为 Redis的I/O多路复用程序的底层实现(从程序中看,其性能的排行应该是evport > epoll > kqueue > select ):
1 | /* Include the best multiplexing layer supported by this system. |
各种不同的I/O多路复用库的使用方式是不一样的,所以Redis对功能进行了统一的封装,方便在不同的环境下的使用:
1 | // 创建,初始化 |
下面以我比较熟悉的epoll为例查看封装的实现:
首先定义一个ae状态结构体,事实上就是epoll的文件描述符和一个获取监听事件中就绪文件描述符的文件表1
2
3
4typedef struct aeApiState {
int epfd;
struct epoll_event *events;
} aeApiState;
创建的过程:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21static int aeApiCreate(aeEventLoop *eventLoop) {
aeApiState *state = zmalloc(sizeof(aeApiState));
if (!state) return -1;
// 监听指定大小的事件数量
state->events = zmalloc(sizeof(struct epoll_event)*eventLoop->setsize);
if (!state->events) {
zfree(state);
return -1;
}
// 创建epoll
state->epfd = epoll_create(1024); /* 1024 is just a hint for the kernel */
if (state->epfd == -1) {
zfree(state->events);
zfree(state);
return -1;
}
// 指定数据
eventLoop->apidata = state;
return 0;
}
添加监听事件过程:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19static int aeApiAddEvent(aeEventLoop *eventLoop, int fd, int mask) {
aeApiState *state = eventLoop->apidata;
struct epoll_event ee;
/* If the fd was already monitored for some event, we need a MOD
* operation. Otherwise we need an ADD operation. */
int op = eventLoop->events[fd].mask == AE_NONE ?
EPOLL_CTL_ADD : EPOLL_CTL_MOD;
ee.events = 0;
mask |= eventLoop->events[fd].mask; /* Merge old events */
// 根据时间的mask来决定监听读or写就绪
if (mask & AE_READABLE) ee.events |= EPOLLIN;
if (mask & AE_WRITABLE) ee.events |= EPOLLOUT;
ee.data.u64 = 0; /* avoid valgrind warning */
ee.data.fd = fd;
// 添加监听事件到内核中
if (epoll_ctl(state->epfd,op,fd,&ee) == -1) return -1;
return 0;
}
I/O多路复用接收到了就绪的事件的时候,就需要对事件进行处理,通过文件事件分派器来分派给不同的文件事件处理器,具体需要处理的文件事件类型如下:
值得注意的是连接应答处理时,需要新添加一个监听事件
连接应答处理
1 | void acceptTcpHandler(aeEventLoop *el, int fd, void *privdata, int mask) { |
通过Redis对上面几种事件的应答处理,我们可以得出客户端和服务端的通信模型如下:
Redis的时间事件分为以下两类:
1 | typedef struct aeTimeEvent { |
服务器将所有时间事件都放在一个无序链表中,每当时间事件执行器运行时,它就遍历整个链表,查找所有已到达的时间事件,并调用相应的事件处理器。
1 | // 已就绪事件 |
时间事件相关API如下:
1 | // 创建时间事件 |
创建和删除时间事件的实现都比较简单,相当于构造和析构函数,我们先看看时间事件执行器的实现:
1 | static int processTimeEvents(aeEventLoop *eventLoop) { |
其总体的思想是:遍历所有已到达的时间事件并调用这些事件的处理器。已到达指的是,时间事件的when属性记录的UNIX时间截等于或小于当前时间的UNIX时间戳。
aeSearchNearestTimer
返回目前时间最近的时间事件
1 | // 寻找里目前时间最近的时间事件 |
时间事件的主要处理应用在serverCron
中,其函数的主要工作有:
时间事件和文件事件都在一个事件循环结构体中
1 | typedef struct aeEventLoop { |
在加入事件到进行处理事件中间的环节就是事件循环了,其调用的是aeMain
函数
1 | void aeMain(aeEventLoop *eventLoop) { |
可以看到,当服务器开始运行的时候,事件循环就不停运行,其事件处理函数aeProcessEvents
实现如下:
1 | int aeProcessEvents(aeEventLoop *eventLoop, int flags) |
其主题的逻辑如下:
通过对Redis的时间事件和文件事件的解析,能够了解Redis客户端和服务端交互的基本过程,同时也能够了解到Redis是单线程的,整个事件循环是串行的
]]>