这是一份 mac 折腾配置以及各种改造的精华笔记, 可以帮你从头到尾打造一个极为顺手的 mac.
在Windows端配合sux 可以统一 win & mac 的使用体验.
. . .
Akka/Erlang 的 actor 模型与 Go 语言的协程 Goroutine 与通道 Channel 代表的 CSP(Communicating Sequential Processes) 模型有什么区别呢?
首先这两者都是并发模型的解决方案,我们看看 Actor 和 Channel 这两个方案的不同:
在 Actor 模型中,主角是 Actor,类似一种 worker,Actor 彼此之间直接发送消息,不需要经过什么中介,消息是异步发送和处理的:
Actor 模型描述了一组为了避免并发编程的常见问题的公理:
更多可见 Actor 模型专题
. . .
etcd 是一个 Go 语言编写的分布式、高可用的强一致性键值存储系统,用于提供可靠的分布式键值(key-value)存储、配置共享和服务发现等功能。 etcd可以用于存储关键数据和实现分布式调度,它在现代化的集群运行中能够起到关键性的作用。
Raft用于保证分布式数据的一致性。动画演示Raft
动画演示Raft选主
前提知识:
. . .
整数编码int
简单动态字符串sds
编码压缩表ziplist
(key1|val1|key2|val2|…这样存储), 字典ht
整数集合
编码(一种特殊的编码, 会使用各种规则来利用位空间, 来节省内存), 字典ht
编码(键为Set的元素, 值都为Null)压缩表ziplist
,双端列表LinkedList
编码压缩表ziplist
(member1|score1|member2|score2|…, 按照score从小到大排列), 跳跃表SkipList编码
, 这个编码里包含一个字典结构和一个跳表结构, 但这两种数据结构都会通过指针来共享相同元素的成员和分值, 所以同时使用跳跃表和字典来保存集合元素不会产生任何重复成员或者分值, 也不会因此而浪费额外的内存: ZScore
查询member成员的 score 值, 或者快速确定是否有某个memberzrank
/zrange
等现在我们看看,对于这个问题,Redis的作者 @antirez 是怎么说的:
There are a few reasons:
可参考: 本博客文章跳表
总结:
用zset,拿时间戳作为score,消息内容作为key调用zadd来生产消息,消费者轮询zset用zrangebyscore指令获取N秒之前的数据轮询进行处理。
说了一个将 score 拆成高 32 位和低 32 位,高 32 位存分数,低 32 位存时间的方法。
当以下条件中的任意一个被满足时, 程序会自动开始对哈希表执行扩展操作:
服务器目前正在执行 BGSAVE 命令或者 BGREWRITEAOF 命令, 并且哈希表的负载因子大于等于 5 ;
根据 BGSAVE 命令或 BGREWRITEAOF 命令是否正在执行, 服务器执行扩展操作所需的负载因子并不相同, 这是因为在执行 BGSAVE 命令或 BGREWRITEAOF 命令的过程中, Redis 需要创建当前服务器进程的子进程, 所以在子进程存在期间, 服务器会提高执行扩展操作所需的负载因子, 从而尽可能地避免在子进程存在期间进行哈希表扩展操作, 这可以避免不必要的内存写入操作, 最大限度地节约内存。
以下是哈希表渐进式 rehash 的详细步骤:
ht[1]
分配空间, 让字典同时持有 ht[0]
和 ht[1]
两个哈希表。ht[0]
哈希表在 rehashidx 索引上的所有键值对 rehash 到 ht[1]
, 当 rehash 工作完成之后, 程序将 rehashidx 属性的值增一。ht[0]
的所有键值对都会被 rehash 至 ht[1]
, 这时程序将 rehashidx 属性的值设为 -1 , 表示 rehash 操作已完成。渐进式 rehash 的好处在于它采取分而治之的方式, 将 rehash 键值对所需的计算工作均滩到对字典的每个添加、删除、查找和更新操作上, 从而避免了集中式 rehash 而带来的庞大计算量。
因为在进行渐进式 rehash 的过程中, 字典会同时使用 ht[0]
和 ht[1]
两个哈希表, 所以在渐进式 rehash 进行期间, 字典的删除(delete)、查找(find)、更新(update)等操作会在两个哈希表上进行: 比如说, 要在字典里面查找一个键的话, 程序会先在 ht[0]
里面进行查找, 如果没找到的话, 就会继续到 ht[1]
里面进行查找, 诸如此类。
另外, 在渐进式 rehash 执行期间, 新添加到字典的键值对一律会被保存到 ht[1]
里面, 而 ht[0]
则不再进行任何添加操作: 这一措施保证了 ht[0]
包含的键值对数量会只减不增, 并随着 rehash 操作的执行而最终变成空表。
子进程创建RDB文件, 根据父进程内存生成临时快照文件, 完成后对原有RDB文件进行原子替换. 然后子进程发送信号给父进程表示完成
追加阻塞:
AOF可以更好的保护数据不丢失,一般AOF会每隔1秒,通过一个后台刷盘线程执行一次fsync操作(这种一秒刷盘一次的策略, 可能会造成追加阻塞: 当硬盘资源繁忙时,即主线程发现距离上次fsync时间超过2秒, 为了数据安全性, 主线程会阻塞直到后台刷盘线程执行fsync操作完成),保证最多丢失1秒钟的数据。
["C", "D", "E", "F", "G"]
。要使用尽量少的命令来记录list键的状态,最简单的方式不是去读取和分析现有AOF文件的内容,,而是直接读取list键在数据库中的当前值,然后用一条RPUSH list "C" "D" "E" "F" "G"
代替前面的6条命令AOF 重写程序可以很好地完成创建一个新 AOF 文件的任务, 但是, 在执行这个程序的时候, 调用者线程会被阻塞。很明显, 作为一种辅佐性的维护手段, Redis 不希望 AOF 重写造成服务器无法处理请求, 所以 Redis 决定将 AOF 重写程序放到(后台)子进程里执行, 这样处理的最大好处是:
不过, 使用子进程也有一个问题需要解决: 因为子进程在进行 AOF 重写期间, 主进程还需要继续处理命令, 而新的命令可能对现有的数据进行修改, 这会让当前数据库的数据和重写后的 AOF 文件中的数据不一致。为了解决这个问题, Redis 增加了一个 AOF 重写缓存, 这个缓存在 fork 出子进程之后开始启用, Redis 主进程在接到新的写命令之后, 除了会将这个写命令的协议内容追加到现有的 AOF 文件之外, 还会追加到这个重写缓存中, 换言之, 当子进程在执行 AOF 重写时, 主进程需要执行以下三个工作:
当子进程完成 AOF 重写之后, 它会向父进程发送一个完成信号, 父进程在接到完成信号之后, 会调用一个信号处理函数, 并完成以下工作:
rename
,覆盖原有的 AOF 文件。这就是aof的原子替换.在整个 AOF 后台重写过程中, 只有最后的写入缓存和改名操作会造成主进程阻塞, 在其他时候, AOF 后台重写都不会对主进程造成阻塞, 这将 AOF 重写对性能造成的影响降到了最低。以上就是 AOF 后台重写, 也即是 BGREWRITEAOF 命令的工作原理。
比如想要将temp文件原子替换origin文件, 则直接rename
tmp文件到origin文件即可实现.rename
通过来说, 直接修改 file system metadata, 如inode信息. 在posix标准里, rename
实现是原子的, 即:
rename
成功, 原文件名 指向 temp 文件; 原文件内容被删除.rename
失败, 原文件名 仍指向原来的文件内容.cmd
)cmd
发送给从redis总结:
主从刚刚连接的时候,进行全量同步;全量同步结束后,进行增量同步。当然,如果有需要,slave 在任何时候都可以发起全量同步。redis 策略是,无论如何,首先会尝试进行增量同步,如不成功,要求从机进行全量同步。
Redis键的过期策略,是有定期删除+惰性删除两种。
当新增数据发现内存达到限制时,Redis触发内存淘汰机制。
用文字描述一下故障切换(failover)的过程:
redis集群是一个由多个主从节点群组成的分布式服务器群,它具有复制、高可用和分片特性。Redis集群不需要sentinel哨兵也能完成节点移除和故障转移的功能。需要将每个节点设置成集群模式,这种集群模式没有中心节点,可水平扩展,据官方文档称可以线性扩展到上万个节点(官方推荐不超过1000个节点)。redis集群的性能和高可用性均优于之前版本的哨兵模式,且集群配置非常简单。
集群模式有以下几个特点:
在哨兵模式中,仍然只有一个Master节点。当并发写请求较大时,哨兵模式并不能缓解写压力。 我们知道只有主节点才具有写能力,那如果在一个集群中,能够配置多个主节点,缓解写压力,redis-cluster集群模式能达到此类要求。
在Redis-Cluster集群中,可以给每一个主节点添加从节点,主节点和从节点直接遵循主从模型的特性。
当用户需要处理更多读请求的时候,添加从节点开启read-only来读写分离可以扩展系统的读性能。
Redis集群的主节点内置了类似Redis Sentinel的节点故障检测和自动故障转移功能,当集群中的某个主节点下线时,集群中的其他在线主节点会注意到这一点,并对已下线的主节点进行故障转移。
集群进行故障转移的方法和Redis Sentinel进行故障转移的方法基本一样(也有主观下线
和客观下线
),不同的是,在集群里面,故障转移的过程是:
所以集群不必另外使用Redis Sentinel。
常见的集群分片算法有:
Redis采用的是Hash Slot
计算方式:hash(key)%N
缺点:如果增加一个redis,映射公式变成了 hash(key)%(N+1)
如果一个redis宕机了,映射公式变成了 hash(key)%(N-1)
在以上两种情况下,几乎所有的缓存都失效了。
先构造出一个长度为2^32整数环,根据节点名称的hash值(分布在[0,2^32-1])放到这个环上。现在要存放资源,根据资源的Key的Hash值(也是分布在[0,2^32-1]),在环上顺时针的找到离它最近的一个节点,就建立了资源和节点的映射关系。
Redis采用的是Hash Slot分片算法,用来计算key存储位置的。集群将整个数据库分为16384个槽位slot,所有key-value数据都存储在这些slot中的某一个上。一个slot槽位可以存放多个数据,key的槽位计算公式为:slot_number=CRC16(key)%16384,其中CRC16为16位的循环冗余校验和函数。
客户端可能会挑选任意一个redis实例去发送命令,每个redis实例接收到命令,都会计算key对应的hash slot,如果在本地就在本地处理,否则返回moved给客户端,让客户端进行重定向到对应的节点执行命令(实现得好一点的smart客户端会缓存键-slot-节点的映射关系来获得性能提升).
那为什么是16384个槽呢?
ps:CRC16算法产生的hash值有16bit,该算法可以产生2^16-=65536个值。换句话说,值是分布在0~65535之间。那作者在做mod运算的时候,为什么不mod65536,而选择mod16384?作者解答
在redis节点发送心跳包时需要把所有的槽放到这个心跳包里,以便让节点知道当前集群信息,16384=16k,在发送心跳包时使用char进行bitmap压缩后是2k(2 * 8 (8 bit) * 1024(1k) = 2K)
,也就是说使用2k的空间创建了16k的槽数。
虽然使用CRC16算法最多可以分配65535(2^16-1)个槽位,65535=65k,压缩后就是8k(8 * 8 (8 bit) * 1024(1k) = 8K)
,也就是说需要需要8k的心跳包,作者认为这样做不太值得;并且一般情况下一个redis集群不会有超过1000个master节点,所以16k的槽位是个比较合适的选择。
详细的请参考: https://segmentfault.com/a/1190000015804406
本博客也有一份: Cache和DB一致性
总结:
cache aside pattern
是指大面积的缓存失效,打崩了DB.
如果缓存挂掉,所有的请求会压到数据库,如果未提前做容量预估,可能会把数据库压垮(在缓存恢复之前,数据库可能一直都起不来),导致系统整体不可服务。
又或者打个比方, 如果所有首页的Key失效时间都是12小时,中午12点刷新的,我零点有个秒杀活动大量用户涌入,假设当时每秒 6000 个请求,本来缓存在可以扛住每秒 5000 个请求,但是缓存当时所有的Key都失效了。此时 1 秒 6000 个请求全部落数据库,数据库必然扛不住.
处理方案:
是指缓存和数据库中都没有的数据,而用户不断发起请求,严重会击垮数据库
我们数据库的 id 都是1开始自增上去的,如发起为id值为 -1 的数据或 id 为特别大不存在的数据。这时的用户很可能是攻击者,攻击会导致数据库压力过大,严重会击垮数据库。
处理方案:
是指持续的大并发的访问一个热点数据, 当这个Key在失效的瞬间,持续的大并发就穿破缓存,直接请求数据库
这个跟缓存雪崩有点像,但是又有一点不一样,缓存雪崩是因为大面积的缓存失效,打崩了DB,而缓存击穿不同的是缓存击穿是指一个Key非常热点,在不停的扛着大并发,大并发集中对这一个点进行访问,当这个Key在失效的瞬间,持续的大并发就穿破缓存,直接请求数据库,就像在一个完好无损的桶上凿开了一个洞。
处理方案:
结论:
大了. 因为Base64 编码本质上是一种将二进制数据转成文本数据的方案。对于非二进制数据,是先将其转换成二进制形式,然后每连续 6 比特(2 的 6 次方 = 64)计算其十进制值,根据该值在上面的索引表中找到对应的字符,最终得到一个文本字符串。也就是说, 每 3 个原始字符编码成 4 个字符,如果原始字符串长度不能被 3 整除,那怎么办?使用 0 值来补充原始字符串。
Base64 编码之所以称为 Base64,是因为其使用 64 个字符来对任意数据进行编码,同理有 Base32、Base16 编码。标准 Base64 编码使用的 64 个字符为:
这 64 个字符是各种字符编码(比如 ASCII 编码)所使用字符的子集,基本,并且可打印。唯一有点特殊的是最后两个字符,因对最后两个字符的选择不同,Base64 编码又有很多变种,比如 Base64 URL 编码。
Base64 编码本质上是一种将二进制数据转成文本数据的方案。对于非二进制数据,是先将其转换成二进制形式,然后每连续 6 比特(2 的 6 次方 = 64)计算其十进制值,根据该值在上面的索引表中找到对应的字符,最终得到一个文本字符串。
假设我们要对 Hello!
进行 Base64 编码,按照 ASCII 表,其转换过程如下图所示:
可知 Hello!
的 Base64 编码结果为 SGVsbG8h
,原始字符串长度为 6 个字符,编码后长度为 8 个字符,每 3 个原始字符经 Base64 编码成 4 个字符,编码前后长度比 4/3,这个长度比很重要 - 比原始字符串长度短,则需要使用更大的编码字符集,这并不我们想要的;长度比越大,则需要传输越多的字符,传输时间越长。Base64 应用广泛的原因是在字符集大小与长度比之间取得一个较好的平衡,适用于各种场景。
是不是觉得 Base64 编码原理很简单?
但这里需要注意一个点:Base64 编码是每 3 个原始字符编码成 4 个字符,如果原始字符串长度不能被 3 整除,那怎么办?使用 0 值来补充原始字符串。
以 Hello!!
为例,其转换过程为:
注:图表中蓝色背景的二进制 0 值是额外补充的。
Hello!!
Base64 编码的结果为 SGVsbG8hIQAA
。最后 2 个零值只是为了 Base64 编码而补充的,在原始字符中并没有对应的字符,那么 Base64 编码结果中的最后两个字符 AA
实际不带有效信息,所以需要特殊处理,以免解码错误。
标准 Base64 编码通常用 =
字符来替换最后的 A
,即编码结果为 SGVsbG8hIQ==
。因为 =
字符并不在 Base64 编码索引表中,其意义在于结束符号,在 Base64 解码时遇到 =
时即可知道一个 Base64 编码字符串结束。
如果 Base64 编码字符串不会相互拼接再传输,那么最后的 =
也可以省略,解码时如果发现 Base64 编码字符串长度不能被 4 整除,则先补充 =
字符,再解码即可。
解码是对编码的逆向操作,但注意一点:对于最后的两个 =
字符,转换成两个 A
字符,再转成对应的两个 6 比特二进制 0 值,接着转成原始字符之前,需要将最后的两个 6 比特二进制 0 值丢弃,因为它们实际上不携带有效信息。
总结:
说到utf8,就不得不说一下unicode了。 Unicode是一个很大的集合,每一个unicode对应一个符号,不管是中文的汉字,英文字符,日文,韩文等等。现在的规模可以容纳100多万个符号。每个符号的编码都不一样,比如,U+0639表示阿拉伯字母 Ain,U+0041表示英语的大写字母A,U+4E25表示汉字“严”。具体的符号对应表,可以查询unicode.org,或者专门的汉字对应表。
需要注意的是,Unicode只是一个符号集,它只规定了符号的二进制代码,却没有规定这个二进制代码应该如何存储。
比如,汉字“严”的unicode是十六进制数4E25,转换成二进制数足足有15位(100111000100101),也就是说这个符号的表示至少需要2个字节。表示其他更大的符号,可能需要3个字节或者4个字节,甚至更多。
这里就有两个严重的问题,第一个问题是:如何才能区别unicode和ascii?计算机怎么知道三个字节表示一个符号,而不是分别表示三个符号呢?第二个问题是:我们已经知道,英文字母只用一个字节表示就够了,如果unicode统一规定,每个符号用三个或四个字节表示,那么每个英文字母前都必然有二到三个字节是0,这对于存储来说是极大的浪费,文本文件的大小会因此大出二三倍,这是无法接受的。
它们造成的结果是:
1)出现了unicode的多种存储方式,也就是说有许多种不同的二进制格式,可以用来表示unicode。
2)unicode在很长一段时间内无法推广,直到互联网的出现。
互联网的普及,强烈要求出现一种统一的编码方式。UTF-8就是在互联网上使用最广的一种unicode的实现方式。其他实现方式还包括UTF-16和UTF-32,不过在互联网上基本不用。重复一遍,这里的关系是,UTF-8是Unicode的实现方式之一。
UTF-8最大的一个特点,就是它是一种变长的编码方式。它可以使用1~4个字节表示一个符号,根据不同的符号而变化字节长度。
UTF-8的编码规则很简单,只有二条:
1)对于单字节的符号,字节的第一位(字节的最高位)设为0,后面7位为这个符号的unicode码。因此对于英语字母,UTF-8编码和ASCII码是相同的。
2)对于n字节的符号(n>1),第一个字节的前n位都设为1,第n+1位设为0,后面字节的前两位一律设为10。剩下的没有提及的二进制位,全部为这个符号的unicode码。
下表总结了编码规则,字母x表示可用编码的位。
Unicode符号范围 UTF-8编码方式(十六进制) | (二进制)—————+———————————————————————
0000 0000-0000 007F | 0xxxxxxx
0000 0080-0000 07FF | 110xxxxx 10xxxxxx
0000 0800-0000 FFFF | 1110xxxx 10xxxxxx 10xxxxxx
0001 0000-0010 FFFF | 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx
下面,还是以汉字“严”为例,演示如何实现UTF-8编码:
已知“严”的unicode是4E25(100111000100101),根据上表,可以发现4E25处在第三行的范围内(0000 0800-0000 FFFF),因此“严”的UTF-8编码需要三个字节,即格式是“1110xxxx 10xxxxxx 10xxxxxx”。然后,从“严”的最后一个二进制位开始,依次从后向前填入格式中的x,多出的位补0。这样就得到了,“严”的UTF-8编码是“11100100 10111000 10100101”,转换成十六进制就是E4B8A5。
大家好,我是光城,欢迎关注公众号:guangcity。在 STL 编程中,容器和算法是独立设计的,即数据结构和算法是独立设计的,连接容器和算法的桥梁就是迭代器了,迭代器使其独立设计成为可能。如下图所示:
上图给出了 STL 的目标就是要把数据和算法分开,分别对其进行设计,之后通过一种名为 iterator 的东西,把这二者再粘接到一起。
设计模式中,关于 iterator 的描述为:一种能够顺序访问容器中每个元素的方法,使用该方法不能暴露容器内部的表达方式。而类型萃取技术就是为了要解决和 iterator 有关的问题的。
它将范型算法 (find, count, find_if) 用于某个容器中, 最重要的是要给算法提供一个访问容器元素的工具,iterator 就扮演着这个重要的角色。
而在算法中我们可能会定义简单的中间变量或者设定算法的返回变量类型,这时候需要知道迭代器所指元素的类型是什么,但是由于没有 typeof 这类判断类型的函数, 我们无法直接获取,那该如何是好?本文就来具体阐述。
对于迭代器来说就是一种智能指针,因此,它也就拥有了一般指针的所有特点——能够对其进行 * 和 -> 操作。但是在遍历容器的时候,不可避免的要对遍历的容器内部有所了解,所以,干脆把迭代器的开发工作交给容器的设计者好了,如此以来,所有实现细节反而得以封装起来不被使用者看到,这正是为什么每一种 STL 容器都提供有专属迭代器的缘故。
而 Traits 在bits/stl_iterator_base_types.h
中:
template<class _Tp> |
看的一脸懵逼吧,没事,看完本节,入门 STL,哈哈~
首先,在算法中运用迭代器时,很可能会用到其相应型别(associated type)(迭代器所指之物的型别)。假设算法中有必要声明一个变量,以 “迭代器所指对象的型别” 为型别,该怎么办呢?
解决方法是:利用 function template 的参数推导机制。
例如:
如果 T 是某个指向特定对象的指针,那么在 func 中需要指针所指向对象的型别的时候,怎么办呢?这个还比较容易,模板的参数推导机制可以完成任务,
template <class I> |
通过模板的推导机制,我们轻而易举的或得了指针所指向的对象的类型。
template <class I, class T> |
但是,函数的 “template 参数推导机制” 推导的只是参数,无法推导函数的返回值类型。万一需要推导函数的传回值,就无能为力了。因此,我们引出下面的方法。
迭代器所指对象的型别,称之为迭代器的 value type。
尽管在 func_impl 中我们可以把 T 作为函数的返回值,但是问题是用户需要调用的是 func。
template <class I, class T> |
如果去编译上述代码,编译失败!
这个问题解决起来也不难,声明内嵌型别似乎是个好主意,这样我们就可以直接获取。只要做一个 iterator,然后在定义的时候为其指向的对象类型制定一个别名,就好了,像下面这样:
template <class T> |
很漂亮的解决方案,看上去一切都很完美。但是,实际上还是有问题,因为 func 如果是一个泛型算法,那么它也绝对要接受一个原生指针作为迭代器,但是显然,你无法让下面的代码编译通过:
int *p = new int(5); |
我们的 func 无法支持原生指针,这显然是不能接受的。此时,template partial specialization 就派上了用场。
前面也提到了,如果直接使用typename I::value_type
,算法就无法接收原生指针,因为原生指针根本就没有 value_type 这个内嵌类型。
因此,我们还需要加入一个中间层对其进行判断,看它是不是原生指针,注意,这就是 traits 技法的妙处所在。
如果我们只使用上面的做法,也就是内嵌 value_type,那么对于没有 value_type 的指针,我们只能对其进行偏特化,这种偏特化是针对可调用函数 func 的偏特化,假如 func 有 100 万行行代码,那么就会造成极大的视觉污染。
(1)函数偏特化
函数偏特化:
template <class T> |
输出:
class version |
(2)加入中间层
在 STL 中 Traits 是什么呢?看下图:
利用一个中间层iterator_traits
固定了func
的形式,使得重复的代码大量减少,唯一要做的就是稍稍特化一下 iterator_tartis 使其支持 pointer 和 const pointer:)
#include <iostream> |
上述的过程是首先询问iterator_traits<I>::value_type
,如果传递的 I 为指针, 则进入特化版本,iterator_traits
直接回答T
; 如果传递进来的I
为class type
, 就去询问T::value_type
.
上述的通俗解释为算法 (func) 问 iterator_traits(我),但是 iterator_traits(我)发现手上是指针的时候,就由我来替它回答。如果是 class type,iterator_traits(我)就继续问(他 —T::value_type)。
总结:通过定义内嵌类型,我们获得了知晓 iterator 所指元素类型的方法,通过 traits 技法,我们将函数模板对于原生指针和自定义 iterator 的定义都统一起来,我们使用 traits 技法主要是为了解决原生指针和自定义 iterator 之间的不同所造成的代码冗余,这就是 traits 技法的妙处所在。
学习书籍:
侯捷《 STL 源码剖析》
学习文章:
https://juejin.im/post/5b1a43fb51882513bf1795c6
https://www.cnblogs.com/mangoyuan/p/6446046.html
http://www.cppblog.com/nacci/archive/2005/11/03/911.aspx