Tow point about Heap struct
- Heap property (比如: 父节点总比子节点小)
- Shape property (树形)
http://www.icourse163.org/learn/NTHU-451013?tid=522006#/learn/content?type=detail&id=947058&sm=1
Heap 的insert
,先插入一个新的节点到最后的子节点的位置,这样保证了 Shape property的合理性,然后再逐一和父节点比较,进行交换,直到不满足交换条件的位置停止,这时满足 Heap property。
Heap (小顶堆)的找自小的数,并取出。O(log(n))
Heap、Stack内存模型
-
Heap: 程序运行的时候,操作系统会给它分配一段内存,用来储存程序和运行产生的数据.程序运行过程中,对于动态的内存占用请求(比如新建对象,或者使用malloc命令),系统就会从预先分配好的那段内存之中,划出一部分给用户,具体规则是从起始地址开始划分(实际上,起始地址会有一段静态数据,这里忽略)。举例来说,用户要求得到10个字节内存,那么从起始地址0x1000开始给他分配,一直分配到地址0x100A,如果再要求得到22个字节,那么就分配到0x1020。这种因为用户主动请求而划分出来的内存区域,叫做 Heap(堆)。它由起始地址开始,从低位(地址)向高位(地址)增长。Heap 的一个重要特点就是不会自动消失,必须手动释放,或者由垃圾回收机制来回收。
-
Stack: Stack 是由于函数运行而临时占用的内存区域。系统开始执行main函数时,会为它在内存里面建立一个帧(frame),所有main的内部变量(比如a和b)都保存在这个帧里面。main函数执行结束后,该帧就会被回收,释放所有的内部变量,不再占用空间.
int main() {
int a = 2;
int b = 3;
return add_a_and_b(a, b);
}
上面代码中,main函数内部调用了add_a_and_b函数。执行到这一行的时候,系统也会为add_a_and_b新建一个帧,用来储存它的内部变量。也就是说,此时同时存在两个帧:main和add_a_and_b。一般来说,调用栈有多少层,就有多少帧.等到add_a_and_b运行结束,它的帧就会被回收,系统会回到函数main刚才中断执行的地方,继续往下执行。通过这种机制,就实现了函数的层层调用,并且每一层都能使用自己的本地变量。
所有的帧都存放在 Stack,由于帧是一层层叠加的,所以 Stack 叫做栈。生成新的帧,叫做"入栈",英文是 push;栈的回收叫做"出栈",英文是 pop。Stack 的特点就是,最晚入栈的帧最早出栈(因为最内层的函数调用,最先结束运行),这就叫做"后进先出"的数据结构。每一次函数执行结束,就自动释放一个帧,所有函数执行结束,整个 Stack 就都释放了。
Algorithms Behind Modern Storage Systems
Different uses for read-optimized B-trees and write-optimized LSM-trees
https://queue.acm.org/detail.cfm?id=3220266
quick find in Linux
- sudo find ~/ -name 完整名称 在home 目录寻找文件
- sudo find / -name 完整名称 在整个根目录寻找文件
Golang 交叉编译 在Mac下编译Linux二进制文件
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build main.go
#写到Makefile中
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build ${LDFLAGS} -o bin/my_ivr my/ivr
一致性hash算法总结
一致性hash算法主要用在分布式缓存中。普通的hash算法使用方式是:key%N(N为服务器数目), 当服务器数目发送增加或减少时, 分配方式则变为key%(N+1)或key%(N-1).将会有大量的key失效迁移,如果后端key对应的是有状态的存储数据,这种做法将导致服务器间大量的数据迁移,从而照成服务的不稳定
一致性hash算法尽可能的减少了key的失效迁移,只是导致失效的那台节点服务器的key的迁移,这是合理的
一致性hash的核心思想为将key作hash运算, 并按一定规律取整得出0-2^32-1之间的值, 环的大小为2^32,key计算出来的整数值则为key在hash环上的位置,如何将一个key,映射到一个节点, 这里分为两步. 第一步, 将服务的key按该hash算法计算,得到在服务在一致性hash环上的位置. 第二步, 将缓存的key,用同样的方法计算出hash环上的位置,按顺时针方向,找到第一个大于等于该hash环位置的服务key,从而得到该key需要分配的服务器。
虚拟节点提高均衡性
由于节点只有3个,存在某些节点所在位置周围有大量的hash点从而导致分配到这些节点到key要比其他节点多的多,这样会导致集群中各节点负载不均衡,为解决这个问题,引入虚拟节点, 即一个实节点对应多个虚拟节点。缓存的key作映射时,先找到对应的虚拟节点,再对应到实节点。每个节点虚拟出多个虚拟节点,从而提高均衡性
对于集群中缓存类数据key的节点分配问题,有这几种解决方法,简单的hash取模,槽映射,一致性hash。
hash取模 对于hash取模,均衡性没有什么问题,但是如果集群中新增一个节点时,将会有N/(N+1)的数据实效,当N值越大,失效率越高。这显然是不可接受的。
槽映射 redis采用的就是这种算法, 其思想是将key值做一定运算(如crc16, crc32,hash), 获得一个整数值,再将该值与固定的槽数取模(slots), 每个节点处理固定的slots。获取key所在的节点时,先要计算出key与槽的对应关系,再通过槽与节点的对应关系找到节点,这里每次新增节点时,只需要迁移一定槽对应的key即可,而不迁移的槽点key值则不会实效,这种方式将实效率降低到了 N/(N+1)。不过这种方式有个缺点就是所有节点都需要知道槽与节点对应关系,如果client端不保存槽与节点的对应关系的话,它需要实现重定向的逻辑。
一致性hash 一致性hash如上文所言,其新增一个节点的实效率仅为N/(N+1),通过一致性hash最大程度的降低了实效率。同时相比于槽映射的方式,不需要引人槽来做中间对应,最大限度的简化了实现
hashRing 用[]uint32表示,可以知道hashRing的最大值就是 2^64-1。理论上2^32-1就够了。构造虚拟节点,并不表示需要构造2^32-1个。虚拟节点的key尽量随机分配在hashRing中,利于平衡性。每个虚拟节点对应一个真实节点,这里用了map结构存虚拟节点和真实节点的对应关系,key就是0-2^32-1的hash数值,value就是真实节点struct。
不可或缺的hash算法:crc32.ChecksumIEEE([]byte(key)) 会得到一个0-2^32-1的hash值。用于产生虚拟节点的key,再把这个key值存到hashRing中,排序。这样,虚拟节点就在hashRing hash环上了
查询key,对其也做hash算法,得到0-2^32-1的hash值。利用二叉查找算法sort.Search,在hashRing中,找到第一个大于等于该hash环位置的虚拟节点的key值,得到这个虚拟节点,从而得到虚拟节点对应的真实节点值。
Linux fork and exec
在传统的UNIX环境下,有两个基本的操作用于创建和修改进程:函数fork()用来创建一个新的进程,该进程几乎是当前进程的一个完全拷贝,使用新的PID;函数exec()用来启动另外的进程以取代当前运行的进程,保留原有的PID。Linux的进程控制和传统的Unix进程控制基本一致,只在一些细节的地方有些区别。
fork
函数启动一个新的进程,前面我们说过,这个进程几乎是当前进程的一个拷贝:子进程和父进程使用相同的代码段;子进程复制父进程的堆栈段和数据段。这样,父进程的所有数据都可以留给子进程,但是,子进程一旦开始运行,虽然它继承了父进程的一切数据,但实际上数据却已经分开,相互之间不再有影响了,也就是说,它们之间不再共享任何数据了。它们再要交互信息时,只有通过进程间通信来实现,这将是我们下面的内容。既然它们如此相象,系统如何来区分它们呢?这是由函数的返回值来决定的。对于父进程, fork函数返回了子程序的进程号,而对于子程序,fork函数则返回零。在操作系统中,我们用ps函数就可以看到不同的进程号,对父进程而言,它的进程号是由比它更低层的系统调用赋予的,而对于子进程而言,它的进程号即是fork函数对父进程的返回值。
如果一个大程序在运行中,它的数据段和堆栈都很大,一次fork就要复制一次,那么fork的系统开销不是很大吗?其实UNIX自有其解决的办法,大家知道,一般CPU都是以”页”为单位来分配内存空间的,每一个页都是实际物理内存的一个映像,象INTEL的CPU,其一页在通常情况下是 4096(4KB)字节大小,而无论是数据段还是堆栈段都是由许多”页”构成的,fork函数复制这两个段,只是”逻辑”上的,并非”物理”上的,也就是说,实际执行fork时,物理空间上两个进程的数据段和堆栈段都还是共享着的,当有一个进程写了某个数据时,这时两个进程之间的数据才有了区别,系统就将有区别的” 页”从物理上也分开。系统在空间上的开销就可以达到最小。Copy On Write
一个进程调用exec
类函数,它本身就“死亡”了,系统把代码段替换成新的程序的代码,废弃原有的数据段和堆栈段,并为新程序分配新的数据段与堆栈段,唯一留下的,就是进程号,进程号不变(平滑重启)。也就是说,对系统而言,还是同一个进程,不过已经是另一个程序了。(不过exec类函数中有的还允许继承环境变量之类的信息。)
exec会创建一个新的进程,结果是:
这个进程会复用当前这个子进程的pid(一个数字),fork的话会创建一个新的 新进程会覆盖这个子进程的所有数据,也就是它马上就死掉,被新进程替换
int32 int64 and byte
一个字节byte = 8位bit(1 byte = 8bit)
int32 是32bit,占4个字节,最大数值是:2^32 = 4294967296, uint32 = 2*4294967296
int64 是64bit,占8个字节,最大数值是:2^64 = 18446744073709551616, uint64 = 2*18446744073709551616
服务器安全建议
https://mp.weixin.qq.com/s/kPc-0HVmYtNaGDoJOwxzAg
一、服务器
- 禁用ROOT
- 用户名和密码尽量复杂
- 修改ssh的默认22端口,这样用户较难使用ssh登入进行攻击
- 安装DenyHosts防暴力破解软件
- 禁用密码登入,使用RSA公钥登入
二、Redis
- 禁用公网IP地址,包括0.0.0.0
- 使用密码限制访问redis
- 使用较低权限账号运行redis
Elasticsearch 某个字段搜索无效
对一个字段进行搜索,没有起到搜索效果。查看查询语句,查询语句没有问题。查看这个记录的document,这个字段的值也正常。这时候,原因很有可能是: 这个字段没有成功加入到索引的mapping中,这样的字段没有被analysis过,只是一个值,没有进行过倒排索引分词,而不会被全文搜索或查询。
理解传输的二进制协议和文本协议
Go语言内置的gob格式就是一种二进制协议,而JSON、XML等则是文本协议
如果我们用文本协议发送123这个数值,则需要至少三个字节,因为123这个数字需要转换成字符’1’、’2’、’3’这三个ASCII字符,存入三个字节中。
所以同样一个数据,用二进制协议表达的体积通常会小于用文本协议表达的体积。这个特性体现到网络应用中,可能就是网络带宽需求的差异。
换个角度看,当我们用二进制协议把123这个数值写入一个文件以后,我们用文本编辑器打开它,看到的会’{‘这个字符,因为这个字符的ASCII值正好是123。而当我们用文本协议存储数据时,我们可以用文本编辑器直接读到123这个数值。
所以通常二进制协议比较不利于阅读,而文本协议方便阅读。这个特性体现到开发中的时候,可能就是调试难易度的差异。
二进制数据和文本数据还有个差异是执行效率差异。以123这个值为例,二进制序列化时候只需要直接对一个字节进行赋值,而用用文本格式的时候,则需要计算出‘个’、‘十’、‘百’位上的值,并转成ASCII码,再赋值给三个字节,反序列化的时候也是如此。
以上分析了二进制协议和文本协议的一些特性,并没有说哪个是最优方案,因为不同的应用场景会需要不同的技术方案。比如TCP/IP协议是二进制协议,在TCP/IP之上构建的HTTP协议则是文本协议,它们各有各的应用场景,所以会出现技术上的差异
二进制传输协议提交更小,占用网络带宽更小,序列化的时候性能比文本协议快,适合需要更高性能的传输场景,比如微服务场景
文本传输协议利于阅读识别和调试,更适合浏览器等客户端渲染场景
Systemd 目录(详细)
- /lib/systemd/system/vsftpd.service:官方释出的默认配置文件;将xxx.service文件放到这个目录下
- Systemd 默认从目录/etc/systemd/system/读取配置文件。但是,里面存放的大部分文件都是符号链接,指向目录/usr/lib/systemd/system/或/lib/systemd/system,真正的配置文件存放在那个目录
- /etc/systemd/system/vsftpd.service.d/custom.conf:在 /etc/systemd/system 下面创建与配置文件相同文件名的目录,但是要加上 .d 的扩展名。然后在该目录下创建配置文件即可。另外,配置文件最好附文件名取名为 .conf 较佳! 在这个目录下的文件会“累加其他设置”进入 /usr/lib/systemd/system/vsftpd.service 内喔!
- /etc/systemd/system/vsftpd.service.wants/*:此目录内的文件为链接文件,设置相依服务的链接。意思是启动了 vsftpd.service 之后,最好再加上这目录下面建议的服务。
- /etc/systemd/system/vsftpd.service.requires/*:此目录内的文件为链接文件,设置相依服务的链接。意思是在启动 vsftpd.service 之前,需要事先启动哪些服务的意思
B+树和B-树
B+Tree相对于B-Tree有几点不同:
-
非叶子节点只存储键值信息。这样一个节点(比如4K)能存储很多键值,能够减少数的高度H,一次I/O读取到节点数据,能够包含更多的键值,这样在查数据时一定程度上减少了I/O操作
-
所有叶子节点之间都有一个链指针。之间是一种链式环结构,因此可以对B+Tree进行两种查找运算:一种是对于主键的范围查找和分页查找,另一种是从根节点开始,进行随机查找。
-
数据记录都存放在叶子节点中。
局部性原理
当一个数据被用到时,其附近的数据也通常会马上被使用。
由于磁盘顺序读取的效率很高(不需要寻道时间,只需很少的旋转时间),因此对于具有局部性的程序来说,预读可以提高I/O效率。
聚簇索引和非聚簇索引
聚簇索引就是指主索引文件和数据文件为同一份文件,聚簇索引主要在InnoDB存储引擎中,在该索引实现方式中B+ tree的叶子节点上的data就是数据本身,key为主键,如果是一般索引的话,data便会指向对应的主索引。通过聚簇索引查找数据只要一次I/O就可以
非聚簇索引就是指B+Tree的叶子节点上的data,并不是数据本身,而是数据存放的地址。主索引和辅助索引没啥区别,只是主索引中的key一定得是唯一的。非聚簇索引查找数据需要两次I/O
辅助索引。辅助索引是相对于主码索引而言的。在MyISAM中,辅助索引的结构和主码索引的结构是一样的,都是采用的是B+树结构,且叶子节点存储的都是数据记录的地址。而InnoDB中虽然也采用的是B+树存储,但是辅助索引的叶子节点存储的是对应于主码索引的主键。也就是说如果你通过辅助索引查找数据,要先在B+树中查找到主键,然后根据主索引查找到对应的记录,查找两次。
InnoDB按照主键进行聚集,如果没有定义主键,InnoDB会试着使用唯一的非空索引来代替。如果没有这种索引,InnoDB就会定义隐藏的主键然后在上面进行聚集。
http://www.cnblogs.com/xiaoxi/p/6894610.html
缓存的三种模式
- Cache Aside 更新模式
这是最常用的设计模式
- 失效: 应用程序先从cache取数据,没有得到,则从数据库中取数据,成功后,放到缓存中
- 命中: 应用从cache中取数据,取到后返回
- 更新: 先把数据存到数据库中,成功后,
再让缓存失效
,让之后的失效步骤的读操作,来触发更新缓存
在更新步骤,为什么不是写完数据库后更新缓存?主要是怕两个并发的写操作导致脏数据。Cache Aside也是有并发问题的。比如,一个是读操作,但是没有命中缓存,就会到数据库中取数据。而此时来了一个写操作,写完数据库后,让缓存失效,然后之前的那个读操作再把老的数据放进去,就造成了脏数据。这个理论上会出现,但实际上出现的概率非常低,因为这个条件需要发送在读缓存时失效,而且有一个并发的写操作。实际上数据库的写操作比简单的读操作慢得多(写操作要加锁,而简单的读操作是快照读,并且在有索引下,不是取大量数据记录,速度比写操作快),读操作必须在写操作前进入数据库操作,又要晚于写操作更新缓存,所有这些条件具备的概率并不大
。Facebook就是用这个策略,降低脏数据发生的概率
读操作 - 写操作 - 缓存失效 - 读操作更新缓存(老的数据缓存): 这种情况基本不可能
- Read/Write Through 更新模式
Cache Aside 策略,应用代码需要维护两个数据库存储,一个是缓存(cache),一个是数据库。所以,应用程序比较啰嗦。而Read/Write Through策略是把更新数据库的操作由缓存自己代理了,所以对应用层来说就简单了。可以理解为,应用认为后端就是一个单一的存储,而存储自己维护自己的cache。
Read Through 就是在查询操作中更新缓存,也就是说,当缓存失效的时候(过期或LRU换出),Cache Aside由调度方负责把数据加载入缓存,而Read Through则用缓存服务器自己来加载,从而对应用方是透明的
Write Through 和Read Through相仿,不过是在更新数据时发生。当有数据更新的时候,如果没有命中缓存,直接更新数据库,然后返回。如果命中了缓存,则更新缓存,然后由缓存自己更新数据库(这是一个同步操作)
- Write Behind Caching 更新模式
Write Back 套路就是,在更新数据的时候,只更新缓存,不更新数据库,而我们的缓存会异步地批量更新数据库。这个设计的好处就是让数据的 I/O 操作飞快无比(因为直接操作内存嘛)。因为异步,Write Back 还可以合并对同一个数据的多次操作,所以性能的提高是相当可观的。
但其带来的问题是,数据不是强一致性的,而且可能会丢失(我们知道 Unix/Linux 非正常关机会导致数据丢失,就是因为这个事)。在软件设计上,我们基本上不可能做出一个没有缺陷的设计,就像算法设计中的时间换空间、空间换时间一个道理。有时候,强一致性和高性能,高可用和高性能是有冲突的。软件设计从来都是 trade-off(取舍)。
另外,Write Back 实现逻辑比较复杂,因为它需要 track 有哪些数据是被更新了的,需要刷到持久层上。操作系统的 Write Back 会在仅当这个 cache 需要失效的时候,才会把它真正持久起来。比如,内存不够了,或是进程退出了等情况,这又叫 lazy write。
缓存设计的重点
缓存更新的模式基本如前面所说,不过这还没完,缓存已经成为高并发高性能架构的一个关键组件了。现在,很多公司都在用 Redis 来搭建他们的缓存系统。一方面是因为 Redis 的数据结构比较丰富。另一方面,我们不能在 Service 内放 local cache,一是每台机器的内存不够大,二是我们的 Service 有多个实例,负载均衡器会把请求随机分布到不同的实例。缓存需要在所有的 Service 实例上都建好,这让我们的 Service 有了状态,更难管理了。
所以,在分布式架构下,一般都需要一个外部的缓存集群。关于这个缓存集群,你需要保证的是内存要足够大,网络带宽也要好,因为缓存本质上是个内存和 IO 密集型的应用。
另外,如果需要内存很大,那么你还要动用数据分片技术来把不同的缓存分布到不同的机器上。这样,可以保证我们的缓存集群可以不断地 scale 下去。关于数据分片的事,我会在后面讲述。
缓存的好坏要看命中率。缓存的命中率高说明缓存有效,一般来说命中率到 80% 以上就算很高了。当然,有的网络为了追求更高的性能,要做到 95% 以上,甚至可能会把数据库里的数据几乎全部装进缓存中。这当然是不必要的,也是没有效率的,因为通常来说,热点数据只会是少数。
另外,缓存是通过牺牲强一致性来提高性能的,这世上任何事情都不是免费的,所以并不是所有的业务都适合用缓存,这需要在设计的时候仔细调研好需求。使用缓存提高性能,就是会有数据更新的延迟。
缓存数据的时间周期也需要好好设计,太长太短都不好,过期期限不宜太短,因为可能导致应用程序不断从数据存储检索数据并将其添加到缓存。同样,过期期限不宜太长,因为这会导致一些没人访问的数据还在内存中不过期,而浪费内存。
使用缓存的时候,一般会使用 LRU 策略。也就是说,当内存不够需要有数据被清出内存时,会找最不活跃的数据清除。所谓最不活跃的意思是最长时间没有被访问过了。所以,开启 LRU 策略会让缓存在每个数据访问的时候把其调到前面,而要淘汰数据时,就从最后面开始淘汰。
于是,对于 LRU 的缓存系统来说,其需要在 key-value 这样的非顺序的数据结构中维护一个顺序的数据结构,并在读缓存时,需要改变被访问数据在顺序结构中的排位。于是,我们的 LRU 在读写时都需要加锁(除非是单线程无并发),因此 LRU 可能会导致更慢的缓存存取的时间。这点要小心。
最后,我们的世界是比较复杂的,很多网站都会被爬虫爬,要小心这些爬虫。因为这些爬虫可能会爬到一些很古老的数据,而程序会把这些数据加入到缓存中去,而导致缓存中那些真实的热点数据被挤出去(因为机器的速度足够快)。对此,一般来说,我们需要有一个爬虫保护机制,或是我们引导这些人去使用我们提供的外部 API。在那边,我们可以有针对性地做多租户的缓存系统(也就是说,把用户和第三方开发者的缓存系统分离开来)
Understand range and select with channel
-
Go提供了range关键字,将其使用在channel上时,会自动等待channel的动作一直到channel被关闭,当channel被关闭时,接收者的for循环也被自动停止了。
-
select关键字用于
多个channel
的结合,这些channel会通过类似于are-you-ready polling的机制来工作,default代码块,其一直是准备好。
检查每个case代码块 如果任意一个case代码块准备好发送或接收,执行对应内容 如果多个case代码块准备好发送或接收,随机选取一个并执行对应内容 如果任何一个case代码块都没有准备好,等待 如果有default代码块,并且没有任何case代码块准备好,执行default代码块对应内容
初识数据库的WAL
预写式日志(Write-ahead logging,缩写 WAL)是关系数据库系统中用于提供原子性和持久性(ACID属性中的两个)的一系列技术。在使用WAL的系统中,所有的修改在提交之前都要先写入log文件中
log文件中通常包括redo和undo信息。假设一个程序在执行某些操作的过程中机器掉电了。在重新启动时,程序可能需要知道当时执行的操作是成功了还是部分成功或者是失败了。如果使用了WAL,程序就可以检查log文件,并对突然掉电时计划执行的操作内容跟实际上执行的操作内容进行比较。在这个比较的基础上,程序就可以决定是撤销已做的操作还是继续完成已做的操作,或者是保持原样。
1、修改记录前,一定要先写日志;
2、事务提交过程中,一定要保证日志先落盘,才能算事务提交完成。
MySQL中,存储引擎实现事务的通用方式是基于 redo log 和 undo log。
简单来说,redo log 记录事务修改后的数据, undo log 记录事务前的原始数据。二者应该同时进行记录
开启了 binlog 的事务执行:
- 先记录 undo和redo log, 确保日志刷到磁盘上持久存储
- 更新数据记录,缓存操作并异步刷盘
- 将事务日志持久化到binlog
- 提交事务,在redo log中写入commit记录 这样,只要binlog没有写成功,整个事务是需要回滚,而binlog写成功后即使MySQL Crash了,都可以恢复事务并完成提交
redo log 和 binlog 保证了数据库的一致性。binlog适用于主从复制和数据恢复,redo log 适用于崩溃恢复
由 binlog
和 redo log
的区别可知:binlog
日志只用于归档,只依靠 binlog
是没有 crash-safe
能力的。
但只有 redo log
也不行,因为 redo log
是 InnoDB
特有的,且日志上的记录落盘后会被覆盖掉。因此需要 binlog
和 redo log
二者同时记录,才能保证当数据库发生宕机重启时,数据不会丢失。
undo log
数据库事务四大特性中有一个是 原子性 ,具体来说就是 原子性是指对数据库的一系列操作,要么全部成功,要么全部失败,不可能出现部分成功的情况。
实际上, 原子性 底层就是通过 undo log
实现的。undo log
主要记录了数据的逻辑变化,比如一条 INSERT
语句,对应一条DELETE
的 undo log
,对于每个 UPDATE
语句,对应一条相反的 UPDATE
的 undo log
,这样在发生错误时,就能回滚到事务之前的数据状态。
MySQL事务实现原理
这里所说的MySQL事务是指使用InnoDB引擎时的事务。MySQL在5.5版本之前默认的数据库引擎时MyISAM,虽然性能极佳,而且提供了大量的特性,包括全文索引、压缩、空间函数等,但MyISAM不支持事务和行级锁,而且最大的缺陷就是崩溃后无法安全恢复。5.5版本之后,MySQL引入了InnoDB(事务性数据库引擎),MySQL 5.5版本后默认的存储引擎为InnoDB。
redo log和undo log来保证事务的原子性、一致性和持久性,同时采用预写日志(WAL)方式将随机写入变成顺序追加写入,提升事务性能。而隔离性是通过锁技术来保证的。
这里我们不妨先来了解一下redo log和undo log。redo log是重做日志,提供前滚操作,undo log是回滚日志,提供回滚操作。undo log不是redo log的逆向过程,其实它们都算是用来恢复的日志:
- redo log通常是物理日志,记录的是数据页的物理修改,而不是某一行或某几行修改成怎样怎样,它用来恢复提交后的物理数据页(恢复数据页,且只能恢复到最后一次提交的位置)。
- undo用来回滚行记录到某个版本。undo log一般是逻辑日志,根据每行记录进行记录。
redo log
redo log 又称为重做日志,保证数据的可靠性。它包含两部分:一是内存中的日志缓冲(redo log buffer),该部分日志是易失性的;二是磁盘上的重做日志文件(redo log file),该部分日志是持久的。
当需要修改事务中的数据时,先将对应的redo log写入到redo log buffer中,然后才在内存中执行相关的数据修改操作。InnoDB通过“force log at commit”机制实现事务的持久性,即在事务提交的时候,必须先将该事务的所有redo log都写入到磁盘上的redo log file中,然后待事务的commit操作完成才算整个事务操作完成。
在每次将redo log buffer中的内容写入redo log file时,都需要调用一次fsync操作,以此确保redo log成功写入到磁盘上(参考下图,内容的流向为:用户态的内存->操作系统的页缓存->物理磁盘)。因此,磁盘的性能在一定程度上也决定了事务提交的性能。这里还可以通过innodb_flush_log_at_trx_commit来控制redo log刷磁盘的策略,这里就不做赘述了。
undo log
undo log有2个功能:实现回滚和多版本并发控制(MVCC, Multi-Version Concurrency Control)。
在数据修改的时候,不仅记录了redo log,还记录了相对应的undo log,如果因为某些原因导致事务失败或回滚了,可以借助该undo log进行回滚。
undo log和redo log记录物理日志不一样,它是逻辑日志。可以认为当delete一条记录时,undo log中会记录一条对应的insert记录,反之亦然,当update一条记录时,它记录一条对应相反的update记录。
当执行rollback时,就可以从undo log中的逻辑记录读取到相应的内容并进行回滚。有时候应用到行版本控制的时候,也是通过undo log来实现的:当读取的某一行被其他事务锁定时,它可以从undo log中分析出该行记录以前的数据是什么,从而提供该行版本信息,让用户实现非锁定一致性读取。
MVCC
说到undo log,就不得不顺带提一下MVCC了,因为MVCC的实现依赖了undo log。当然,MVCC的实现还依赖了隐藏字段(DB_TRX_ID,DB_ROLL_PTR,DB_ROW_ID)、Read View等。
MVCC的全称是多版本并发控制,它使得在使用READ COMMITTD、REPEATABLE READ这两种隔离级别的事务下执行一致性读操作有了保证。换言之,就是为了查询一些正在被另一个事务更新的行,并且可以看到它们被更新之前的值。这是一个可以用来增强并发性的强大技术,因为这样的一来的话查询就不用等待另一个事务释放锁,使不同事务的读-写、写-读操作并发执行,从而提升系统性能。
这里的读指的是“快照读”。普通的SELECT操作就是快照读,有的地方也称之为“一致性读”或者“一致性无锁读”。它不会对表中的任何记录做加锁动作,即不加锁的非阻塞读。快照读的前提是隔离级别不是串行化级别,串行化级别下的快照读会退化成当前读。之所以出现快照读的情况,是基于提高并发性能的考虑,这里可以认为MVCC是行锁的一个变种,但它在很多情况下,避免了加锁操作,降低了开销。当然,既然是基于多版本,即快照读可能读到的并不一定是数据的最新版本,而有可能是之前的历史版本。
对应的还有“当前读”。类似UPDATE、DELETE、INSERT、SELECT…LOCK IN SHARE MODE、SELECT…FOR UPDATE这些操作就是当前读。为什么叫当前读?就是它读取的是记录的最新版本,读取时还要保证其他并发事务不能修改当前记录,会对读取的记录进行加锁
MySQL实现事务ACID特性的方式总结如下:
- 原子性:使用 undo log来实现,如果事务执行过程中出错或者用户执行了rollback,系统通过undo log日志返回事务开始的状态。
- 持久性:使用 redo log来实现,只要redo log日志持久化了,当系统崩溃,即可通过redo log把数据恢复。
- 隔离性:通过锁以及MVCC来实现。
- 一致性:通过回滚、恢复以及并发情况下的隔离性,从而实现一致性
https://mp.weixin.qq.com/s?__biz=MzU0MzQ5MDA0Mw==&mid=2247489849&idx=1&sn=cbac2a6ad99ac466f2ba8d69507fd2fe&chksm=fb0bf3adcc7c7abb565a9865e14b357888f7b7b78874b74c18bfdc5a4278ec2503b258c27730&scene=21#wechat_redirect
关于WAL性能优化问题:
WAL机制一方面是为了确保数据即使写入缓存丢失也可以恢复,另一方面是为了集群之间异步复制。默认WAL机制开启且使用同步机制写入WAL。首先考虑业务是否需要写WAL,通常情况下大多数业务都会开启WAL机制(默认),但是对于部分业务可能并不特别关心异常情况下部分数据的丢失,而更关心数据写入吞吐量,比如某些推荐业务,这类业务即使丢失一部分用户行为数据可能对推荐结果并不构成很大影响,但是对于写入吞吐量要求很高,不能造成数据队列阻塞。这种场景下可以考虑关闭WAL写入,写入吞吐量可以提升2x~3x。退而求其次,有些业务不能接受不写WAL,但可以接受WAL异步写入,也是可以考虑优化的,通常也会带来1x~2x的性能提升。
优化推荐:根据业务关注点在WAL机制与写入吞吐量之间做出选择
- 同步WAL,保证数据安全和数据一致性,吞吐量最差
- 异步WAL,降低了数据安全和数据一致性,吞吐量提升
- 关闭WAL,数据安全和数据一致性没有保证,吞吐量最大
参考链接:
http://m.blog.itpub.net/15498/viewspace-2134411/
http://hbasefly.com/2016/12/10/hbase-parctice-write/
gorilla/websocket 的Write要加锁,解决并发写问题
gorilla/websocket 的write操作在高并发时会有报错导致write操作失败,解决方式是加锁。例子:
func (socket *TSocket) WriteMessage(message []byte) error {
socket.Lock()
defer socket.Unlock()
err := socket.Conn.WriteMessage(TextMsg, message)
if err != nil {
socket.Logger.Error("TSocket WriteMessage Error", err)
}
return err
}
func (s *TSocket) Write(b []byte) (n int, err error) {
s.Lock()
defer s.Unlock()
var w io.WriteCloser
if w, err = s.Conn.NextWriter(websocket.BinaryMessage); err == nil {
if n, err = w.Write(b); err == nil {
err = w.Close()
}
}
return
}
git push –force-with-lease
不要用 git push --force
,而要用 git push --force-with-lease
代替。在你上次提交之后,只要其他人往该分支提交代码,git push --force-with-lease
会拒绝覆盖