说说 time_wait

不只是time_wait, 说说tcp关闭状态

TCP CLOSE_WAIT

03130408-930b424bf5384c80b677b6a50f1c6edc

几个问题

  • 主动关闭进入FIN_WAIT_1状态后,被动方没有回ack,主动方回怎样?如果大量堆积FIN_WAIT_1会怎样?怎么避免?
  • 主动的一方进入FIN_WAIT_2

参考

  • 火丁笔记 这两篇博文非常棒。解释了大量FIN_WAIT1和大量TIME_WAIT的问题,就比如说很多解决TIME_WAIT的方案简单说下配置就完了,这几篇文章说了背后的问题,还带了rfc,专业。
    • https://huoding.com/2014/11/06/383
    • https://huoding.com/2013/12/31/316
  • https://benohead.com/tcp-about-fin_wait_2-time_wait-and-close_wait/

TCP CLOSE_WAIT

Read More

redis命令使用建议

redis命令使用建议

Key

Redis采用Key-Value型的基本数据结构,任何二进制序列都可以作为Redis的Key使用(例如普通的字符串或一张JPEG图片) 关于Key的一些注意事项:

  • 不要使用过长的Key。例如使用一个1024字节的key就不是一个好主意,不仅会消耗更多的内存,还会导致查找的效率降低
  • Key短到缺失了可读性也是不好的,例如”u1000flw”比起”user:1000:followers”来说,节省了寥寥的存储空间,却引发了可读性和可维护性上的麻烦
  • 最好使用统一的规范来设计Key,比如”object-type:id:attr”,以这一规范设计出的Key可能是”user:1000”或”comment:1234:reply-to”
  • Redis允许的最大Key长度是512MB(对Value的长度限制也是512MB)

String

String是Redis的基础数据类型,Redis没有Int、Float、Boolean等数据类型的概念,所有的基本类型在Redis中都以String体现。

与String相关的常用命令:

  • SET:为一个key设置value,可以配合EX/PX参数指定key的有效期,通过NX/XX参数针对key是否存在的情况进行区别操作,时间复杂度O(1)
  • GET:获取某个key对应的value,时间复杂度O(1)
  • GETSET:为一个key设置value,并返回该key的原value,时间复杂度O(1)
  • MSET:为多个key设置value,时间复杂度O(N)
  • MSETNX:同MSET,如果指定的key中有任意一个已存在,则不进行任何操作,时间复杂度O(N)
  • MGET:获取多个key对应的value,时间复杂度O(N)

上文提到过,Redis的基本数据类型只有String,但Redis可以把String作为整型或浮点型数字来使用,主要体现在INCR、DECR类的命令上:

  • INCR:将key对应的value值自增1,并返回自增后的值。只对可以转换为整型的String数据起作用。时间复杂度O(1)
  • INCRBY:将key对应的value值自增指定的整型数值,并返回自增后的值。只对可以转换为整型的String数据起作用。时间复杂度O(1)
  • DECR/DECRBY:同INCR/INCRBY,自增改为自减。

INCR/DECR系列命令要求操作的value类型为String,并可以转换为64位带符号的整型数字,否则会返回错误。 也就是说,进行INCR/DECR系列命令的value,必须在[-2^63 ~ 2^63 - 1]范围内。

前文提到过,Redis采用单线程模型,天然是线程安全的,这使得INCR/DECR命令可以非常便利的实现高并发场景下的精确控制。

例1:库存控制

在高并发场景下实现库存余量的精准校验,确保不出现超卖的情况。

设置库存总量:

SET inv:remain "100"

库存扣减+余量校验:

DECR inv:remain

当DECR命令返回值大于等于0时,说明库存余量校验通过,如果返回小于0的值,则说明库存已耗尽。

假设同时有300个并发请求进行库存扣减,Redis能够确保这300个请求分别得到99到-200的返回值,每个请求得到的返回值都是唯一的,绝对不会找出现两个请求得到一样的返回值的情况。

例2:自增序列生成

实现类似于RDBMS的Sequence功能,生成一系列唯一的序列号

设置序列起始值:

SET sequence "10000"

获取一个序列值:

INCR sequence

直接将返回值作为序列使用即可。

获取一批(如100个)序列值:

INCRBY sequence 100

假设返回值为N,那么[N - 99 ~ N]的数值都是可用的序列值。

当多个客户端同时向Redis申请自增序列时,Redis能够确保每个客户端得到的序列值或序列范围都是全局唯一的,绝对不会出现不同客户端得到了重复的序列值的情况。

List

Redis的List是链表型的数据结构,可以使用LPUSH/RPUSH/LPOP/RPOP等命令在List的两端执行插入元素和弹出元素的操作。虽然List也支持在特定index上插入和读取元素的功能,但其时间复杂度较高(O(N)),应小心使用。

与List相关的常用命令:

  • LPUSH:向指定List的左侧(即头部)插入1个或多个元素,返回插入后的List长度。时间复杂度O(N),N为插入元素的数量
  • RPUSH:同LPUSH,向指定List的右侧(即尾部)插入1或多个元素
  • LPOP:从指定List的左侧(即头部)移除一个元素并返回,时间复杂度O(1)
  • RPOP:同LPOP,从指定List的右侧(即尾部)移除1个元素并返回
  • LPUSHX/RPUSHX:与LPUSH/RPUSH类似,区别在于,LPUSHX/RPUSHX操作的key如果不存在,则不会进行任何操作
  • LLEN:返回指定List的长度,时间复杂度O(1)
  • LRANGE:返回指定List中指定范围的元素(双端包含,即LRANGE key 0 10会返回11个元素),时间复杂度O(N)。应尽可能控制一次获取的元素数量,一次获取过大范围的List元素会导致延迟,同时对长度不可预知的List,避免使用LRANGE key 0 -1这样的完整遍历操作。

应谨慎使用的List相关命令:

  • LINDEX:返回指定List指定index上的元素,如果index越界,返回nil。index数值是回环的,即-1代表List最后一个位置,-2代表List倒数第二个位置。时间复杂度O(N)
  • LSET:将指定List指定index上的元素设置为value,如果index越界则返回错误,时间复杂度O(N),如果操作的是头/尾部的元素,则时间复杂度为O(1)
  • LINSERT:向指定List中指定元素之前/之后插入一个新元素,并返回操作后的List长度。如果指定的元素不存在,返回-1。如果指定key不存在,不会进行任何操作,时间复杂度O(N)

由于Redis的List是链表结构的,上述的三个命令的算法效率较低,需要对List进行遍历,命令的耗时无法预估,在List长度大的情况下耗时会明显增加,应谨慎使用。

换句话说,Redis的List实际是设计来用于实现队列,而不是用于实现类似ArrayList这样的列表的。如果你不是想要实现一个双端出入的队列,那么请尽量不要使用Redis的List数据结构。

为了更好支持队列的特性,Redis还提供了一系列阻塞式的操作命令,如BLPOP/BRPOP等,能够实现类似于BlockingQueue的能力,即在List为空时,阻塞该连接,直到List中有对象可以出队时再返回。针对阻塞类的命令,此处不做详细探讨,请参考官方文档(https://redis.io/topics/data-types-intro) 中”Blocking operations on lists”一节。

Hash

Hash即哈希表,Redis的Hash和传统的哈希表一样,是一种field-value型的数据结构,可以理解成将HashMap搬入Redis。 Hash非常适合用于表现对象类型的数据,用Hash中的field对应对象的field即可。 Hash的优点包括:

  • 可以实现二元查找,如”查找ID为1000的用户的年龄”
  • 比起将整个对象序列化后作为String存储的方法,Hash能够有效地减少网络传输的消耗
  • 当使用Hash维护一个集合时,提供了比List效率高得多的随机访问命令

与Hash相关的常用命令:

  • HSET:将key对应的Hash中的field设置为value。如果该Hash不存在,会自动创建一个。时间复杂度O(1)
  • HGET:返回指定Hash中field字段的值,时间复杂度O(1)
  • HMSET/HMGET:同HSET和HGET,可以批量操作同一个key下的多个field,时间复杂度:O(N),N为一次操作的field数量
  • HSETNX:同HSET,但如field已经存在,HSETNX不会进行任何操作,时间复杂度O(1)
  • HEXISTS:判断指定Hash中field是否存在,存在返回1,不存在返回0,时间复杂度O(1)
  • HDEL:删除指定Hash中的field(1个或多个),时间复杂度:O(N),N为操作的field数量
  • HINCRBY:同INCRBY命令,对指定Hash中的一个field进行INCRBY,时间复杂度O(1)

应谨慎使用的Hash相关命令:

  • HGETALL:返回指定Hash中所有的field-value对。返回结果为数组,数组中field和value交替出现。时间复杂度O(N)
  • HKEYS/HVALS:返回指定Hash中所有的field/value,时间复杂度O(N)

上述三个命令都会对Hash进行完整遍历,Hash中的field数量与命令的耗时线性相关,对于尺寸不可预知的Hash,应严格避免使用上面三个命令,而改为使用HSCAN命令进行游标式的遍历,具体请见 https://redis.io/commands/scan

Set

Redis Set是无序的,不可重复的String集合。

与Set相关的常用命令:

  • SADD:向指定Set中添加1个或多个member,如果指定Set不存在,会自动创建一个。时间复杂度O(N),N为添加的member个数
  • SREM:从指定Set中移除1个或多个member,时间复杂度O(N),N为移除的member个数
  • SRANDMEMBER:从指定Set中随机返回1个或多个member,时间复杂度O(N),N为返回的member个数
  • SPOP:从指定Set中随机移除并返回count个member,时间复杂度O(N),N为移除的member个数
  • SCARD:返回指定Set中的member个数,时间复杂度O(1)
  • SISMEMBER:判断指定的value是否存在于指定Set中,时间复杂度O(1)
  • SMOVE:将指定member从一个Set移至另一个Set

慎用的Set相关命令:

  • SMEMBERS:返回指定Hash中所有的member,时间复杂度O(N)
  • SUNION/SUNIONSTORE:计算多个Set的并集并返回/存储至另一个Set中,时间复杂度O(N),N为参与计算的所有集合的总member数
  • SINTER/SINTERSTORE:计算多个Set的交集并返回/存储至另一个Set中,时间复杂度O(N),N为参与计算的所有集合的总member数
  • SDIFF/SDIFFSTORE:计算1个Set与1或多个Set的差集并返回/存储至另一个Set中,时间复杂度O(N),N为参与计算的所有集合的总member数

上述几个命令涉及的计算量大,应谨慎使用,特别是在参与计算的Set尺寸不可知的情况下,应严格避免使用。可以考虑通过SSCAN命令遍历获取相关Set的全部member(具体请见 https://redis.io/commands/scan ),如果需要做并集/交集/差集计算,可以在客户端进行,或在不服务实时查询请求的Slave上进行。

Sorted Set

Redis Sorted Set是有序的、不可重复的String集合。Sorted Set中的每个元素都需要指派一个分数(score),Sorted Set会根据score对元素进行升序排序。如果多个member拥有相同的score,则以字典序进行升序排序。

Sorted Set非常适合用于实现排名。

Sorted Set的主要命令:

  • ZADD:向指定Sorted Set中添加1个或多个member,时间复杂度O(Mlog(N)),M为添加的member数量,N为Sorted Set中的member数量
  • ZREM:从指定Sorted Set中删除1个或多个member,时间复杂度O(Mlog(N)),M为删除的member数量,N为Sorted Set中的member数量
  • ZCOUNT:返回指定Sorted Set中指定score范围内的member数量,时间复杂度:O(log(N))
  • ZCARD:返回指定Sorted Set中的member数量,时间复杂度O(1)
  • ZSCORE:返回指定Sorted Set中指定member的score,时间复杂度O(1)
  • ZRANK/ZREVRANK:返回指定member在Sorted Set中的排名,ZRANK返回按升序排序的排名,ZREVRANK则返回按降序排序的排名。时间复杂度O(log(N))
  • ZINCRBY:同INCRBY,对指定Sorted Set中的指定member的score进行自增,时间复杂度O(log(N))

慎用的Sorted Set相关命令:

  • ZRANGE/ZREVRANGE:返回指定Sorted Set中指定排名范围内的所有member,ZRANGE为按score升序排序,ZREVRANGE为按score降序排序,时间复杂度O(log(N)+M),M为本次返回的member数
  • ZRANGEBYSCORE/ZREVRANGEBYSCORE:返回指定Sorted Set中指定score范围内的所有member,返回结果以升序/降序排序,min和max可以指定为-inf和+inf,代表返回所有的member。时间复杂度O(log(N)+M)
  • ZREMRANGEBYRANK/ZREMRANGEBYSCORE:移除Sorted Set中指定排名范围/指定score范围内的所有member。时间复杂度O(log(N)+M)

上述几个命令,应尽量避免传递[0 -1]或[-inf +inf]这样的参数,来对Sorted Set做一次性的完整遍历,特别是在Sorted Set的尺寸不可预知的情况下。可以通过ZSCAN命令来进行游标式的遍历(具体请见 https://redis.io/commands/scan ),或通过LIMIT参数来限制返回member的数量(适用于ZRANGEBYSCORE和ZREVRANGEBYSCORE命令),以实现游标式的遍历。

Bitmap和HyperLogLog

Redis的这两种数据结构相较之前的并不常用,在本文中只做简要介绍,如想要详细了解这两种数据结构与其相关的命令,请参考官方文档https://redis.io/topics/data-types-intro 中的相关章节

Bitmap在Redis中不是一种实际的数据类型,而是一种将String作为Bitmap使用的方法。可以理解为将String转换为bit数组。使用Bitmap来存储true/false类型的简单数据极为节省空间。

HyperLogLogs是一种主要用于数量统计的数据结构,它和Set类似,维护一个不可重复的String集合,但是HyperLogLogs并不维护具体的member内容,只维护member的个数。也就是说,HyperLogLogs只能用于计算一个集合中不重复的元素数量,所以它比Set要节省很多内存空间。

其他常用命令

  • EXISTS:判断指定的key是否存在,返回1代表存在,0代表不存在,时间复杂度O(1)
  • DEL:删除指定的key及其对应的value,时间复杂度O(N),N为删除的key数量
  • EXPIRE/PEXPIRE:为一个key设置有效期,单位为秒或毫秒,时间复杂度O(1)
  • TTL/PTTL:返回一个key剩余的有效时间,单位为秒或毫秒,时间复杂度O(1)
  • RENAME/RENAMENX:将key重命名为newkey。使用RENAME时,如果newkey已经存在,其值会被覆盖;使用RENAMENX时,如果newkey已经存在,则不会进行任何操作,时间复杂度O(1)
  • TYPE:返回指定key的类型,string, list, set, zset, hash。时间复杂度O(1)
  • CONFIG GET:获得Redis某配置项的当前值,可以使用*通配符,时间复杂度O(1)
  • CONFIG SET:为Redis某个配置项设置新值,时间复杂度O(1)
  • CONFIG REWRITE:让Redis重新加载redis.conf中的配置

数据持久化

Redis提供了将数据定期自动持久化至硬盘的能力,包括RDB和AOF两种方案,两种方案分别有其长处和短板,可以配合起来同时运行,确保数据的稳定性。

必须使用数据持久化吗?

Redis的数据持久化机制是可以关闭的。如果你只把Redis作为缓存服务使用,Redis中存储的所有数据都不是该数据的主体而仅仅是同步过来的备份,那么可以关闭Redis的数据持久化机制。 但通常来说,仍然建议至少开启RDB方式的数据持久化,因为:

  • RDB方式的持久化几乎不损耗Redis本身的性能,在进行RDB持久化时,Redis主进程唯一需要做的事情就是fork出一个子进程,所有持久化工作都由子进程完成
  • Redis无论因为什么原因crash掉之后,重启时能够自动恢复到上一次RDB快照中记录的数据。这省去了手工从其他数据源(如DB)同步数据的过程,而且要比其他任何的数据恢复方式都要快
  • 现在硬盘那么大,真的不缺那一点地方

RDB

采用RDB持久方式,Redis会定期保存数据快照至一个rbd文件中,并在启动时自动加载rdb文件,恢复之前保存的数据。可以在配置文件中配置Redis进行快照保存的时机:

save [seconds] [changes]

意为在[seconds]秒内如果发生了[changes]次数据修改,则进行一次RDB快照保存,例如

save 60 100

会让Redis每60秒检查一次数据变更情况,如果发生了100次或以上的数据变更,则进行RDB快照保存。 可以配置多条save指令,让Redis执行多级的快照保存策略。 Redis默认开启RDB快照,默认的RDB策略如下:

save 900 1
save 300 10
save 60 10000

也可以通过BGSAVE命令手工触发RDB快照保存。

RDB的优点:

  • 对性能影响最小。如前文所述,Redis在保存RDB快照时会fork出子进程进行,几乎不影响Redis处理客户端请求的效率。
  • 每次快照会生成一个完整的数据快照文件,所以可以辅以其他手段保存多个时间点的快照(例如把每天0点的快照备份至其他存储媒介中),作为非常可靠的灾难恢复手段。
  • 使用RDB文件进行数据恢复比使用AOF要快很多。

RDB的缺点:

  • 快照是定期生成的,所以在Redis crash时或多或少会丢失一部分数据。
  • 如果数据集非常大且CPU不够强(比如单核CPU),Redis在fork子进程时可能会消耗相对较长的时间(长至1秒),影响这期间的客户端请求。

AOF

采用AOF持久方式时,Redis会把每一个写请求都记录在一个日志文件里。在Redis重启时,会把AOF文件中记录的所有写操作顺序执行一遍,确保数据恢复到最新。

AOF默认是关闭的,如要开启,进行如下配置:

appendonly yes

AOF提供了三种fsync配置,always/everysec/no,通过配置项[appendfsync]指定:

  • appendfsync no:不进行fsync,将flush文件的时机交给OS决定,速度最快
  • appendfsync always:每写入一条日志就进行一次fsync操作,数据安全性最高,但速度最慢
  • appendfsync everysec:折中的做法,交由后台线程每秒fsync一次

随着AOF不断地记录写操作日志,必定会出现一些无用的日志,例如某个时间点执行了命令SET key1 “abc”,在之后某个时间点又执行了SET key1 “bcd”,那么第一条命令很显然是没有用的。大量的无用日志会让AOF文件过大,也会让数据恢复的时间过长。 所以Redis提供了AOF rewrite功能,可以重写AOF文件,只保留能够把数据恢复到最新状态的最小写操作集。 AOF rewrite可以通过BGREWRITEAOF命令触发,也可以配置Redis定期自动进行:

auto-aof-rewrite-percentage 100
auto-aof-rewrite-min-size 64mb

上面两行配置的含义是,Redis在每次AOF rewrite时,会记录完成rewrite后的AOF日志大小,当AOF日志大小在该基础上增长了100%后,自动进行AOF rewrite。同时如果增长的大小没有达到64mb,则不会进行rewrite。

AOF的优点:

  • 最安全,在启用appendfsync always时,任何已写入的数据都不会丢失,使用在启用appendfsync everysec也至多只会丢失1秒的数据。
  • AOF文件在发生断电等问题时也不会损坏,即使出现了某条日志只写入了一半的情况,也可以使用redis-check-aof工具轻松修复。
  • AOF文件易读,可修改,在进行了某些错误的数据清除操作后,只要AOF文件没有rewrite,就可以把AOF文件备份出来,把错误的命令删除,然后恢复数据。

AOF的缺点:

  • AOF文件通常比RDB文件更大
  • 性能消耗比RDB高
  • 数据恢复速度比RDB慢

内存管理与数据淘汰机制

最大内存设置

默认情况下,在32位OS中,Redis最大使用3GB的内存,在64位OS中则没有限制。

在使用Redis时,应该对数据占用的最大空间有一个基本准确的预估,并为Redis设定最大使用的内存。否则在64位OS中Redis会无限制地占用内存(当物理内存被占满后会使用swap空间),容易引发各种各样的问题。

通过如下配置控制Redis使用的最大内存:

maxmemory 100mb

在内存占用达到了maxmemory后,再向Redis写入数据时,Redis会:

  • 根据配置的数据淘汰策略尝试淘汰数据,释放空间
  • 如果没有数据可以淘汰,或者没有配置数据淘汰策略,那么Redis会对所有写请求返回错误,但读请求仍然可以正常执行

在为Redis设置maxmemory时,需要注意:

  • 如果采用了Redis的主从同步,主节点向从节点同步数据时,会占用掉一部分内存空间,如果maxmemory过于接近主机的可用内存,导致数据同步时内存不足。所以设置的maxmemory不要过于接近主机可用的内存,留出一部分预留用作主从同步。

数据淘汰机制

Redis提供了5种数据淘汰策略:

  • volatile-lru:使用LRU算法进行数据淘汰(淘汰上次使用时间最早的,且使用次数最少的key),只淘汰设定了有效期的key
  • allkeys-lru:使用LRU算法进行数据淘汰,所有的key都可以被淘汰
  • volatile-random:随机淘汰数据,只淘汰设定了有效期的key
  • allkeys-random:随机淘汰数据,所有的key都可以被淘汰
  • volatile-ttl:淘汰剩余有效期最短的key

最好为Redis指定一种有效的数据淘汰策略以配合maxmemory设置,避免在内存使用满后发生写入失败的情况。

一般来说,推荐使用的策略是volatile-lru,并辨识Redis中保存的数据的重要性。对于那些重要的,绝对不能丢弃的数据(如配置类数据等),应不设置有效期,这样Redis就永远不会淘汰这些数据。对于那些相对不是那么重要的,并且能够热加载的数据(比如缓存最近登录的用户信息,当在Redis中找不到时,程序会去DB中读取),可以设置上有效期,这样在内存不够时Redis就会淘汰这部分数据。

配置方法:

maxmemory-policy volatile-lru   #默认是noeviction,即不进行数据淘汰

Pipelining

Pipelining

Redis提供许多批量操作的命令,如MSET/MGET/HMSET/HMGET等等,这些命令存在的意义是减少维护网络连接和传输数据所消耗的资源和时间。 例如连续使用5次SET命令设置5个不同的key,比起使用一次MSET命令设置5个不同的key,效果是一样的,但前者会消耗更多的RTT(Round Trip Time)时长,永远应优先使用后者。

然而,如果客户端要连续执行的多次操作无法通过Redis命令组合在一起,例如:

SET a "abc"
INCR b
HSET c name "hi"

此时便可以使用Redis提供的pipelining功能来实现在一次交互中执行多条命令。 使用pipelining时,只需要从客户端一次向Redis发送多条命令(以\r\n)分隔,Redis就会依次执行这些命令,并且把每个命令的返回按顺序组装在一起一次返回,比如:

$ (printf "PING\r\nPING\r\nPING\r\n"; sleep 1) | nc localhost 6379
+PONG
+PONG
+PONG

大部分的Redis客户端都对Pipelining提供支持,所以开发者通常并不需要自己手工拼装命令列表。

Pipelining的局限性

Pipelining只能用于执行连续且无相关性的命令,当某个命令的生成需要依赖于前一个命令的返回时,就无法使用Pipelining了。

通过Scripting功能,可以规避这一局限性

事务与Scripting

Pipelining能够让Redis在一次交互中处理多条命令,然而在一些场景下,我们可能需要在此基础上确保这一组命令是连续执行的。

比如获取当前累计的PV数并将其清0

> GET vCount
12384
> SET vCount 0
OK

如果在GET和SET命令之间插进来一个INCR vCount,就会使客户端拿到的vCount不准确。

Redis的事务可以确保复数命令执行时的原子性。也就是说Redis能够保证:一个事务中的一组命令是绝对连续执行的,在这些命令执行完成之前,绝对不会有来自于其他连接的其他命令插进去执行。

通过MULTI和EXEC命令来把这两个命令加入一个事务中:

> MULTI
OK
> GET vCount
QUEUED
> SET vCount 0
QUEUED
> EXEC
1) 12384
2) OK

Redis在接收到MULTI命令后便会开启一个事务,这之后的所有读写命令都会保存在队列中但并不执行,直到接收到EXEC命令后,Redis会把队列中的所有命令连续顺序执行,并以数组形式返回每个命令的返回结果。

可以使用DISCARD命令放弃当前的事务,将保存的命令队列清空。

需要注意的是,Redis事务不支持回滚: 如果一个事务中的命令出现了语法错误,大部分客户端驱动会返回错误,2.6.5版本以上的Redis也会在执行EXEC时检查队列中的命令是否存在语法错误,如果存在,则会自动放弃事务并返回错误。 但如果一个事务中的命令有非语法类的错误(比如对String执行HSET操作),无论客户端驱动还是Redis都无法在真正执行这条命令之前发现,所以事务中的所有命令仍然会被依次执行。在这种情况下,会出现一个事务中部分命令成功部分命令失败的情况,然而与RDBMS不同,Redis不提供事务回滚的功能,所以只能通过其他方法进行数据的回滚。

通过事务实现CAS

Redis提供了WATCH命令与事务搭配使用,实现CAS乐观锁的机制。

假设要实现将某个商品的状态改为已售:

if(exec(HGET stock:1001 state) == "in stock")
    exec(HSET stock:1001 state "sold");

这一伪代码执行时,无法确保并发安全性,有可能多个客户端都获取到了”in stock”的状态,导致一个库存被售卖多次。

使用WATCH命令和事务可以解决这一问题:

exec(WATCH stock:1001);
if(exec(HGET stock:1001 state) == "in stock") {
    exec(MULTI);
    exec(HSET stock:1001 state "sold");
    exec(EXEC);
}

WATCH的机制是:在事务EXEC命令执行时,Redis会检查被WATCH的key,只有被WATCH的key从WATCH起始时至今没有发生过变更,EXEC才会被执行。如果WATCH的key在WATCH命令到EXEC命令之间发生过变化,则EXEC命令会返回失败。

Scripting

通过EVAL与EVALSHA命令,可以让Redis执行LUA脚本。这就类似于RDBMS的存储过程一样,可以把客户端与Redis之间密集的读/写交互放在服务端进行,避免过多的数据交互,提升性能。

Scripting功能是作为事务功能的替代者诞生的,事务提供的所有能力Scripting都可以做到。Redis官方推荐使用LUA Script来代替事务,前者的效率和便利性都超过了事务。

关于Scripting的具体使用,本文不做详细介绍,请参考官方文档 https://redis.io/commands/eval

Read More

redis io复用

首先把reactor和proactor流程图抄过来

反应器Reactor Reactor模式结构

Reactor

Reactor包含如下角色:

Handle 句柄;用来标识socket连接或是打开文件;
Synchronous Event Demultiplexer:同步事件多路分解器:由操作系统内核实现的一个函数;用于阻塞等待发生在句柄集合上的一个或多个事件;(如select/epoll;)
Event Handler:事件处理接口
Concrete Event HandlerA:实现应用程序所提供的特定事件处理逻辑;
Reactor:反应器,定义一个接口,实现以下功能: 
1)供应用程序注册和删除关注的事件句柄; 
2)运行事件循环; 
3)有就绪事件到来时,分发事件到之前注册的回调函数上处理;

“反应”器名字中”反应“的由来: “反应”即“倒置”,“控制逆转”,具体事件处理程序不调用反应器,而是由反应器分配一个具体事件处理程序,具体事件处理程序对某个指定的事件发生做出反应;这种控制逆转又称为“好莱坞法则”(不要调用我,让我来调用你) 业务流程及时序图

seq_Reactor

应用启动,将关注的事件handle注册到Reactor中;
调用Reactor,进入无限事件循环,等待注册的事件到来;
事件到来,select返回,Reactor将事件分发到之前注册的回调函数中处理;

主动器Proactor Proactor模式结构

Proactor

Proactor主动器模式包含如下角色

Handle 句柄;用来标识socket连接或是打开文件;
Asynchronous Operation Processor:异步操作处理器;负责执行异步操作,一般由操作系统内核实现;
Asynchronous Operation:异步操作
Completion Event Queue:完成事件队列;异步操作完成的结果放到队列中等待后续使用
Proactor:主动器;为应用程序进程提供事件循环;从完成事件队列中取出异步操作的结果,分发调用相应的后续处理逻辑;
Completion Handler:完成事件接口;一般是由回调函数组成的接口;
Concrete Completion Handler:完成事件处理逻辑;实现接口定义特定的应用处理逻辑;

业务流程及时序图

seq_Proactor

应用程序启动,调用异步操作处理器提供的异步操作接口函数,调用之后应用程序和异步操作处理就独立运行;应用程序可以调用新的异步操作,而其它操作可以并发进行;
应用程序启动Proactor主动器,进行无限的事件循环,等待完成事件到来;
异步操作处理器执行异步操作,完成后将结果放入到完成事件队列;
主动器从完成事件队列中取出结果,分发到相应的完成事件回调函数处理逻辑中;

对比两者的区别 主动和被动

以主动写为例: Reactor将handle放到select(),等待可写就绪,然后调用write()写入数据;写完处理后续逻辑; Proactor调用aoi_write后立刻返回,由内核负责写操作,写完后调用相应的回调函数处理后续逻辑;

可以看出,Reactor被动的等待指示事件的到来并做出反应;它有一个等待的过程,做什么都要先放入到监听事件集合中等待handler可用时再进行操作; Proactor直接调用异步读写操作,调用完后立刻返回; 实现

Reactor实现了一个被动的事件分离和分发模型,服务等待请求事件的到来,再通过不受间断的同步处理事件,从而做出反应;

Proactor实现了一个主动的事件分离和分发模型;这种设计允许多个任务并发的执行,从而提高吞吐量;并可执行耗时长的任务(各个任务间互不影响) 优点

Reactor实现相对简单,对于耗时短的处理场景处理高效; 操作系统可以在多个事件源上等待,并且避免了多线程编程相关的性能开销和编程复杂性; 事件的串行化对应用是透明的,可以顺序的同步执行而不需要加锁; 事务分离:将与应用无关的多路分解和分配机制和与应用相关的回调函数分离开来,

Proactor性能更高,能够处理耗时长的并发场景; 缺点

Reactor处理耗时长的操作会造成事件分发的阻塞,影响到后续事件的处理;

Proactor实现逻辑复杂;依赖操作系统对异步的支持,目前实现了纯异步操作的操作系统少,实现优秀的如windows IOCP,但由于其windows系统用于服务器的局限性,目前应用范围较小;而Unix/Linux系统对纯异步的支持有限,应用事件驱动的主流还是通过select/epoll来实现; 适用场景

Reactor:同时接收多个服务请求,并且依次同步的处理它们的事件驱动程序;

Proactor:异步接收和同时处理多个服务请求的事件驱动程序

reference

  • redis与reactor模式。这个文章讲的不错。里面的论文值得读一下。http://www.dengshenyu.com/%E5%90%8E%E7%AB%AF%E6%8A%80%E6%9C%AF/2016/01/09/redis-reactor-pattern.html
    • http://www.dre.vanderbilt.edu/~schmidt/PDF/reactor-siemens.pdf
  • reactor和proactor https://blog.csdn.net/wanbf123/article/details/78062802

todo list

  • 读参考链接中的论文
Read More

LD_PRELOAD为何不能劫持printf

环境gcc linux

简而言之是gcc在某些时刻会优化掉printf,优化成puts


下面是牢骚。群友们针对printf咋被优化的进行了探讨,可能是弱符号,或者涉及到变参的复杂场景,以及printf可以使用寄存器作为参数。一顿天马行空

这个链接具有一定的参考性

特别摘抄过来,作为后续分析glibc的一个参考思路

printf的代码在哪里?

显然,Helloworld的源代码需要经过编译器编译,操作系统的加载才能正确执行。而编译器包含预编译、编译、汇编和链接四个步骤。

#include<stdio.h>
int main()
{
​    **printf("Hello World !\n");**
​    return 0;
}

首先,预编译器处理源代码中的宏,比如#include。预编译结束后,我们发现printf函数的声明。

$/usr/lib/gcc/i686-linux-gnu/4.7/cc1 -E -quiet
​ main.c -o main.i # 1 “main.c” # 1 “<命令行>" \# 1 "main.c" ... extern int printf (const char *__restrict __format, ...); ...

int main() { printf(“Hello World!\n”); return 0; }

然后编译器将高级语言程序转化为汇编代码。

$/usr/lib/gcc/i686-linux-gnu/4.7/cc1 -fpreprocessed -quiet  \
​    main.i -o main.s
​    .file      "main.c"
​    .section   .rodata
.LC0:
​    .string    "Hello World!"
​    .text
​    .globl     main
​    .type      main, @function
main:
​    pushl      %ebp
​    movl       %esp,  %ebp
​    andl       $-16,  %esp
​    subl       $16,   %esp
​    movl       $.LC0, (%esp)
​    `call       puts`
​    movl       $0,    %eax
​    leave
​    ret
​    .size      main, .-main
...

我们发现printf函数调用被转化为call puts指令,而不是call printf指令,这好像有点出乎意料。不过不用担心,这是编译器对printf的一种优化。实践证明,对于printf的参数如果是以’\n’结束的纯字符串,printf会被优化为puts函数,而字符串的结尾’\n’符号被消除。除此之外,都会正常生成call printf指令。

如果我们仍希望通过printf调用”Hello World !\n”的话,只需要按照如下方式修改即可。不过这样做就不能在printf调用结束后立即看到打印字符串了,因为puts函数可以立即刷新输出缓冲区。我们仍然使用puts作为例子继续阐述。

    .section   .rodata
.LC0:
​    .string    "hello world!\n"
​    ...
​    call       printf
...

接下来,汇编器开始工作。将汇编文件转化为我们不能直接阅读的二进制格式——可重定位目标文件,这里我们需要gcc工具包的objdump命令查看它的二进制信息。可是我们发现call puts指令里保存了无效的符号地址。

$as -o main.o main.s
$objdump –d main.o
main.o:     文件格式 elf32-i386
Disassembly of section .text:
00000000 <main>:
   0:  55                     push   %ebp
   1:  89 e5                  mov    %esp,%ebp
   3:  83 e4 f0               and    $0xfffffff0,%esp
   6:  83 ec 10               sub    $0x10,%esp
   9:  c7 04 24 00 00 00 00   movl   $0x0,(%esp)
  10:  e8 fc ff ff ff         call   11 <main+0x11>
  15:  b8 00 00 00 00         mov    $0x0,%eax
  1a:  c9                     leave  
  1b:  c3                     ret

而链接器最终会将puts的符号地址修正。由于链接方式分为静态链接和动态链接两种,虽然链接方式不同,但是不影响最终代码对库函数的调用。我们这里关注printf函数背后的原理,因此使用更易说明问题的静态链接的方式阐述。

$/usr/lib/gcc/i686-linux-gnu/4.7/collect2                   \
​    -static -o main                                         \
​    /usr/lib/i386-linux-gnu/crt1.o                          \
​    /usr/lib/i386-linux-gnu/crti.o                          \
​    /usr/lib/gcc/i686-linux-gnu/4.7/crtbeginT.o             \
​    main.o                                                  \
​    --start-group                                           \
​    /usr/lib/gcc/i686-linux-gnu/4.7/libgcc.a                \
​    /usr/lib/gcc/i686-linux-gnu/4.7/libgcc_eh.a             
​    /usr/lib/i386-linux-gnu/libc.a                          \
​    --end-group                                             \
​    /usr/lib/gcc/i686-linux-gnu/4.7/crtend.o                \
​    /usr/lib/i386-linux-gnu/crtn.o

$objdump –sd main

Disassembly of section .text:

...

08048ea4 <main>:
 8048ea4:  55                     push   %ebp
 8048ea5:  89 e5                  mov    %esp,%ebp
 8048ea7:  83 e4 f0               and    $0xfffffff0,%esp
 8048eaa:  83 ec 10               sub    $0x10,%esp
 8048ead:  c7 04 24 e8 86 0c 08   movl   $0x80c86e8,(%esp)
 8048eb4:  e8 57 0a 00 00         call   8049910 <_IO_puts>
 8048eb9:  b8 00 00 00 00         mov    $0x0,%eax
 8048ebe:  c9                     leave  
 8048ebf:  c3                     ret
...

静态链接时,链接器将C语言的运行库(CRT)链接到可执行文件,其中crt1.o、crti.o、crtbeginT.o、crtend.o、crtn.o便是这五个核心的文件,它们按照上述命令显示的顺序分居在用户目标文件和库文件的两侧。由于我们使用了库函数puts,因此需要库文件libc.a,而libc.a与libgcc.a和libgcc_eh.a有相互依赖关系,因此需要使用-start-group和-end-group将它们包含起来。

链接后,call puts的地址被修正,但是反汇编显示的符号是_IO_puts而不是puts!难道我们找的文件不对吗?当然不是,我们使用readelf命令查看一下main的符号表。竟然发现puts和_IO_puts这两个符号的性质是等价的!objdump命令只是显示了全局的符号_IO_puts而已。

$readelf main –s
Symbol table '.symtab' contains 2307 entries:
   Num:    Value  Size Type    Bind   Vis      Ndx Name
...
  1345: 08049910   352 FUNC    WEAK   DEFAULT    6 puts
...
  1674: 08049910   352 FUNC    GLOBAL DEFAULT    6 _IO_puts
...

那么puts函数的定义真的是在libc.a里吗?我们需要对此确认。我们将libc.a解压缩,然后全局符号_IO_puts所在的二进制文件,输出结果为ioputs.o。然后查看该文件的符号表。发现ioputs.o定义了puts和_IO_puts符号,因此可以确定ioputs.o就是puts函数的代码文件,且在库文件libc.a内。

$ar -x /usr/lib/i386-linux-gnu/libc.a
$grep -rin "_IO_puts" *.o
​    $readelf -s ioputs.o
Symbol table '.symtab' contains 20 entries:
   Num:    Value  Size Type    Bind   Vis      Ndx Name
...
​    11: 00000000   352 FUNC    GLOBAL DEFAULT    1 _IO_puts
...
​    19: 00000000   352 FUNC    WEAK   DEFAULT    1 puts

二、printf的调用轨迹

我们知道对于”Hello World !\n”的printf调用被转化为puts函数,并且我们找到了puts的实现代码是在库文件libc.a内的,并且知道它是以二进制的形式存储在文件ioputs.o内的,那么我们如何寻找printf函数的调用轨迹呢?换句话说,printf函数是如何一步步执行,最终使用Linux的int 0x80软中断进行系统调用陷入内核的呢?

如果让我们向终端输出一段字符串信息,我们一般会使用系统调用write()。那么打印Helloworld的printf最终是这样做的吗?我们借助于gdb来追踪这个过程,不过我们需要在编译源文件的时候添加-g选项,支持调试时使用符号表。

$/usr/lib/gcc/i686-linux-gnu/4.7/cc1 -fpreprocessed -quiet -g\

​ main.i -o main.s

然后使用gdb调试可执行文件。

$gdb ./main

(gdb)break main

(gdb)run

(gdb)stepi

在main函数内下断点,然后调试执行,接着不断的使用stepi指令执行代码,直到看到Hello World !输出为止。这也是为什么我们使用puts作为示例而不是使用printf的原因。

(gdb)

0xb7fff419 in __kernel_vsyscall ()

(gdb)

Hello World!

我们发现Hello World!打印位置的上一行代码的执行位置为0xb7fff419。我们查看此处的反汇编代码。

(gdb)disassemble
Dump of assembler code for function __kernel_vsyscall:
   0xb7fff414 <+0>:  push   %ecx
   0xb7fff415 <+1>:  push   %edx
   0xb7fff416 <+2>:  push   %ebp
   0xb7fff417 <+3>:  mov    %esp,%ebp
   0xb7fff419 <+5>:  sysenter
   0xb7fff41b <+7>:  nop
   0xb7fff41c <+8>:  nop
   0xb7fff41d <+9>:  nop
   0xb7fff41e <+10>: nop
   0xb7fff41f <+11>: nop
   0xb7fff420 <+12>: nop
   0xb7fff421 <+13>: nop
   0xb7fff422 <+14>: int    $0x80
=> 0xb7fff424 <+16>: pop    %ebp
   0xb7fff425 <+17>: pop    %edx
   0xb7fff426 <+18>: pop    %ecx
   0xb7fff427 <+19>: ret    
End of assembler dump.

我们惊奇的发现,地址0xb7fff419正是指向sysenter指令的位置!这里便是系统调用的入口。如果想了解这里为什么不是int 0x80指令,请参考文章《Linux 2.6 对新型 CPU 快速系统调用的支持》。或者参考Linus在邮件列表里的文章《Intel P6 vs P7 system call performance》

系统调用的位置已经是printf函数调用的末端了,我们只需要按照函数调用关系便能得到printf的调用轨迹了。

(gdb)backtrace
#0  0xb7fff424 in __kernel_vsyscall ()
#1  0x080588b2 in __write_nocancel ()
#2  0x0806fa11 in _IO_new_file_write ()
#3  0x0806f8ed in new_do_write ()
#4  0x080708dd in _IO_new_do_write ()
#5  0x08070aa5 in _IO_new_file_overflow ()
#6  0x08049a37 in puts ()
#7  0x08048eb9 in main () at main.c:4

我们发现系统调用前执行的函数是__write_nocancel,它执行了系统调用__write!

三、printf源码阅读

虽然我们找到了Hello World的printf调用轨迹,但是仍然无法看到函数的源码。跟踪反汇编代码不是个好主意,最好的方式是直接阅读glibc的源代码!我们可以从官网下载最新的glibc源代码(glibc-2.18)进行阅读分析,或者直接访问在线源码分析网站LXR。然后按照调用轨迹的的逆序查找函数的调用点。

1.puts 调用 _IO_new_file_xsputn

具体的符号转化关系为:_IO_puts => _IO_sputn => _IO_XSPUTN => __xsputn => _IO_file_xsputn => _IO_new_file_xsputn

$cat ./libio/ioputs.c
int
_IO_puts (str)
​     const char *str;
{
  int result = EOF;
  _IO_size_t len = strlen (str);
  _IO_acquire_lock (_IO_stdout);

  if ((_IO_vtable_offset (_IO_stdout) != 0
​       || _IO_fwide (_IO_stdout, -1) == -1)
​      && **_IO_sputn (_IO_stdout, str, len)** == len
​      && _IO_putc_unlocked ('\n', _IO_stdout) != EOF)
​    result = MIN (INT_MAX, len + 1);
  _IO_release_lock (_IO_stdout);
  return result;
}
#ifdef weak_alias
weak_alias (_IO_puts, puts)
#endif

这里注意weak_alias宏的含义,即将puts绑定到符号_IO_puts,并且puts符号为weak类型的。这也就解释了puts符号被解析为_IO_puts的真正原因。

2._IO_new_file_xsputn 调用 _IO_new_file_overflow

具体的符号转化关系为:_IO_new_file_xsputn => _IO_OVERFLOW => __overflow => _IO_new_file_overflow

$cat ./libio/fileops.c
_IO_size_t
_IO_new_file_xsputn (f, data, n)
​     _IO_FILE *f;
​     const void *data;
​     _IO_size_t n;
{
 ...
  if (to_do + must_flush > 0)
​    {
​      _IO_size_t block_size, do_write;
​      /* Next flush the (full) buffer. */
​      if (**_IO_OVERFLOW (f, EOF)** == EOF)
​    /* If nothing else has to be written or nothing has been written, we
​       must not signal the caller that the call was even partially
​       successful.  */
​    return (to_do == 0 || to_do == n) ? EOF : n - to_do;
...

3._IO_new_file_overflow 调用 _IO_new_do_write

具体的符号转化关系为:_IO_new_file_overflow =>_IO_do_write =>_IO_new_do_write

$cat ./libio/fileops.c
int
_IO_new_file_overflow (f, ch)
​      _IO_FILE *f;
​      int ch;
{
 ...
  if (INTUSE(**_IO_do_write**) (f, f->_IO_write_base,
  f->_IO_write_ptr - f->_IO_write_base) == EOF)
  return EOF;
  return (unsigned char) ch;
}

4. _IO_new_do_write 调用 new_do_write

具体的符号转化关系为:_IO_new_do_write => new_do_write

$cat ./libio/fileops.c
int
_IO_new_do_write (fp, data, to_do)
​     _IO_FILE *fp;
​     const char *data;
​     _IO_size_t to_do;
{
  return (to_do == 0
​      || (_IO_size_t) **new_do_write** (fp, data, to_do) == to_do) ? 0 : EOF;
}

5. new_do_write调用 _IO_new_file_write

具体的符号转化关系为:new_do_write =>_IO_SYSWRITE => __write() => write() => _IO_new_file_write

$cat ./libio/fileops.c
_IO_size_t
new_do_write (fp, data, to_do)
_IO_FILE *fp;
const char *data;
_IO_size_t to_do;
{
 ...
  count = **_IO_SYSWRITE** (fp, data, to_do);
  if (fp->_cur_column && count)
  fp->_cur_column = INTUSE(_IO_adjust_column) (fp->_cur_column - 1, data, count) + 1;
 ...
}

6. _IO_new_file_write调用 write_nocancel

具体的符号转化关系为:_IO_new_file_write=>write_not_cancel => write_nocancel

$cat ./libio/fileops.c
_IO_ssize_t
_IO_new_file_write (f, data, n)
_IO_FILE *f;
const void *data;
_IO_ssize_t n;
{
 _IO_ssize_t to_do = n;
  while (to_do > 0)
  {
​    _IO_ssize_t count = (__builtin_expect (f->_flags2& _IO_FLAGS2_NOTCANCEL, 0)? **write_not_cancel** (f->_fileno, data, to_do): write (f->_fileno, data, to_do));
...
}

7. write_nocancel 调用 linux-gate.so::__kernel_vsyscall

具体的符号转化关系为:write_nocancel => INLINE_SYSCALL => INTERNAL_SYSCALL =>__kernel_vsyscall

注意 linux-gate.so在磁盘上并不存在,它是内核镜像中的特定页,由内核编译生成。关于它的更多信息,可以参考文章《linux-gate.so技术细节》和《What is linux-gate.so.1?》

Read More

redis release note 与 redis命令cheatsheet

redis release note

redis版本 功能点  
2.6 Lua脚本支持  
2.6 新增PEXIRE、PTTL、PSETEX过期设置命令,key过期时间可以设置为毫秒级  
2.6 新增位操作命令:BITCOUNT、BITOP  
2.6 新增命令:dump、restore,即序列化与反序列化操作  
2.6 新增命令:INCRBYFLOAT、HINCRBYFLOAT,用于对值进行浮点数的加减操作  
2.6 新增命令:MIGRATE,用于将key原子性地从当前实例传送到目标实例的指定数据库上  
2.6 放开了对客户端的连接数限制  
2.6 hash函数种子随机化,有效防止碰撞  
2.6 SHUTDOWN命令添加SAVE和NOSAVE两个参数,分别用于指定SHUTDOWN时用不用执行写RDB的操作  
2.6 虚拟内存Virtual Memory相关代码全部去掉  
2.6 sort命令会拒绝无法转换成数字的数据模型元素进行排序  
2.6 不同客户端输出缓冲区分级,比如普通客户端、slave机器、pubsub客户端,可以分别控制对它们的输出缓冲区大小  
2.6 更多的增量过期(减少阻塞)的过期key收集算法 ,当非常多的key在同一时间失效的时候,意味着redis可以提高响应的速度  
2.6 底层数据结构优化,提高存储大数据时的性能  
2.8 引入PSYNC,主从可以增量同步,这样当主从链接短时间中断恢复后,无需做完整的RDB完全同步  
2.8 从显式ping主,主可以扫描到可能超时的从  
2.8 新增命令:SCAN、SSCAN、HSCAN和ZSCAN  
2.8 crash的时候自动内存检查  
2.8 新增键空间通知功能,客户端可以通过订阅/发布机制,接收改动了redis指定数据集的事件  
2.8 可绑定多个IP地址  
2.8 可通过CONFIGSET设置客户端最大连接数  
2.8 新增CONFIGREWRITE命令,可以直接把CONFIGSET的配置修改到redis.conf里  
2.8 新增pubsub命令,可查看pub/sub相关状态  
2.8 支持引用字符串,如set ‘foo bar’ “hello world\n”  
2.8 新增redis master-slave集群高可用解决方案(Redis-Sentinel)  
2.8 当使用SLAVEOF命令时日志会记录下新的主机  
3.0 实现了分布式的Redis即Redis Cluster,从而做到了对集群的支持  
3.0 全新的”embedded string”对象编码方式,从而实现了更少的缓存丢失和性能的提升  
3.0 大幅优化LRU近似算法的性能  
3.0 新增CLIENT PAUSE命令,可以在指定时间内停止处理客户端请求  
3.0 新增WAIT命令,可以阻塞当前客户端,直到所有以前的写命令都成功传输并和指定的slaves确认  
3.0 AOF重写过程中的”last write”操作降低了AOF child -> parent数据传输的延迟  
3.0 实现了对MIGRATE连接缓存的支持,从而大幅提升key迁移的性能  
3.0 为MIGRATE命令新增参数:copy和replace,copy不移除源实例上的key,replace替换目标实例上已存在的key  
3.0 提高了BITCOUNT、INCR操作的性能  
3.0 调整Redis日志格式  
3.2 新增对GEO(地理位置)功能的支持  
3.2 新增Lua脚本远程debug功能  
3.2 SDS相关的优化,提高redis性能  
3.2 修改Jemalloc相关代码,提高redis内存使用效率  
3.2 提高了主从redis之间关于过期key的一致性  
3.2 支持利用upstart和systemd管理redis进程  
3.2 将list底层数据结构类型修改为quicklist,在内存占用和RDB文件大小方面有极大的提升  
3.2 SPOP命令新增count参数,可控制随机删除元素的个数  
3.2 支持为RDB文件增加辅助字段,比如创建日期,版本号等,新版本可以兼容老版本RDB文件,反之不行  
3.2 通过调整哈希表大小的操作码RDB_OPCODE_RESIZEDB,redis可以更快得读RDB文件  
3.2 新增HSTRLEN命令,返回hash数据类型的value长度  
3.2 提供了一个基于流水线的MIGRATE命令,极大提升了命令执行速度  
3.2 redis-trib.rb中实现将slot进行负载均衡的功能  
3.2 改进了从机迁移的功能  
3.2 改进redis sentine高可用方案,使之可以更方便地监控多个redis主从集群  
4.0 加入模块系统,用户可以自己编写代码来扩展和实现redis本身不具备的功能,它与redis内核完全分离,互不干扰  
4.0 优化了PSYNC主从复制策略,使之效率更高  
4.0 为DEL、FLUSHDB、FLUSHALL命令提供非阻塞选项,可以将这些删除操作放在单独线程中执行,从而尽可能地避免服务器阻塞  
4.0 新增SWAPDB命令,可以将同一redis实例指定的两个数据库互换  
4.0 新增RDB-AOF持久化格式,开启后,AOF重写产生的文件将同时包含RDB格式的内容和AOF格式的内容,其中 RDB格式的内容用于记录已有的数据,而AOF格式的内存则用于记录最近发生了变化的数据  
4.0 新增MEMORY内存命令,可以用于查看某个key的内存使用、查看整体内存使用细节、申请释放内存、深入查看内存分配器内部状态等功能  
4.0 兼容NAT和Docker  
5.0 新的流数据类型(Stream data type) https://redis.io/topics/streams-intro  
5.0 新的 Redis 模块 API:定时器、集群和字典 API(Timers, Cluster and Dictionary APIs)  
5.0 RDB 现在可存储 LFU 和 LRU 信息  
5.0 redis-cli 中的集群管理器从 Ruby (redis-trib.rb) 移植到了 C 语言代码。 执行 redis-cli –cluster help 命令以了解更多信息
5.0 新的有序集合(sorted set)命令:ZPOPMIN/MAX 和阻塞变体(blocking variants)  
5.0 升级 Active defragmentation 至 v2 版本  
5.0 增强 HyperLogLog 的实现  
5.0 更好的内存统计报告  
5.0 许多包含子命令的命令现在都有一个 HELP 子命令  
5.0 客户端频繁连接和断开连接时,性能表现更好  
5.0 许多错误修复和其他方面的改进  
5.0 升级 Jemalloc 至 5.1 版本  
5.0 引入 CLIENT UNBLOCK 和 CLIENT ID  
5.0 新增 LOLWUT 命令 http://antirez.com/news/123  
5.0 在不存在需要保持向后兼容性的地方,弃用 “slave” 术语  
5.0 网络层中的差异优化  
5.0 Lua 相关的改进:将 Lua 脚本更好地传播到 replicas / AOF, Lua 脚本现在可以超时并在副本中进入 -BUSY 状态  
5.0 引入动态的 HZ(Dynamic HZ) 以平衡空闲 CPU 使用率和响应性  
5.0 对 Redis 核心代码进行了重构并在许多方面进行了改进  

redis cheatsheet

命令 版本 复杂度 可选 返回值
SET 1.0 O1 2.6.12后增加EX PX NX XX 2.6.12永远回复ok,之后如果有NX XX导致的失败,NULL Bulk Reply
SETNX 1.0 O(1)   1/0
SETEX 2.0 O(1) set +expire 院子 ok
PSETEX 2.6 O(1) s时间单位毫秒 ok
GET 1.0 O(1)   value/nil/特定的错误字符串
GETSET 1.0 O(1)   value/nil/特定的错误字符串
STRLEN 2.2 O(1)   不存在返回0
APPEND 2.0 平摊O(1)   返回长度
SETRANGE 2.2 如果本身字符串短。平摊O(1),否则为O(len(value)) 如果字符串长会阻塞(?不是inplace?) 返回长度
GETRANGE 2.4 O(N)N为返回字符串的长度 2.0以前是SUBSTR 子串
INCR 1.0 O(1)   返回+1之后的值字符串,如果不是数字,会报错(error) ERR value is not an integer or out of range
INCRBY 1.0 O(1) 负数也可以。 返回增量之后的值字符串
INCRBYFLOAT 2.6 O(1)   返回增量之后的值字符串
DECR 1.0 O(1)   返回-1之后的值字符串
DECRBY 1.0 O(1)   返回减法操作之后的值字符串
MSET 1.0.1 O(N) N为键个数 multiple set,原子性  
MSETNX 1.0.1 O(N) N为键个数 都不存在则进行操作,否则都不操作。  
MGET 1.0.0 O(N) N为键个数   如果有不存在的键,返回nil
SETBIT 2.2 O(1) offset 参数必须大于或等于 0 ,小于 2^32 (bit 映射被限制在 512 MB 之内) 该位原来的值
GETBIT 2.2 O(1)    
BITCOUNT 2.6 O(N)    
BITPOS 2.8.7 O(N)    
BITOP 2.6 O(N)    
BITFIELD 3.2 O(1) ?这个命令的需求?  
HSET 2.0 O(1)   新的值返回1 覆盖了返回0
HSETNX 2.0 O(1)   设置成功返回1,已经存在放弃执行返回0
HGET 2.0 O(1)   不存在返回nil
HEXISTS 2.0 O(1)   字段存在返回1不存在返回0
HDEL 2.0 O(N) N为删除的字段个数 2.4之前只能一个字段一个字段的删除,如果要求原子需要MULTI+EXEC,后续版本支持多字段删除 被成功移除的字段数量 (<=N)
HLEN 2.0 O(1)   返回字段数量,不存在返回0
HSTRLEN 3.2 O(1) 类似STRLEN,操作哈希表的字段 返回字段关联的值的字符串长度
HINCRBY 2.0 O(1) 类似INCRBY,操作哈希表的字段  
HINCRBYFLOAT 2.6 O(1) 类似INCRBYFLOAT  
HMSET 2.0 O(N) N为filed-value数量 类似MSET 能不能用在集群? OK
HMGET 2.0 O(N) 类似MGET 能不能用在集群? 不存在的字段返回nil
HKEYS 2.0 O(N)N为哈希表大小?   返回所有字段的一个表(list or set)不存在返回空表
HVALS 2.0 O(N)   返回所有字段对应的值的表
HGETALL 2.0 O(N)   返回字段和值的表,奇数字段偶数值
HSCAN 2.0 O(N)? 类似SCAN  
LPUSH 1.0 O(1) 2.4以前只接受单个值,之后接受多个值。 返回列表长度
LPUSHX 2.2 O(1) key不存在什么也不做 返回长度,不存在返回0
RPUSH 1.0 O(1) 2.4以前只接受单个值  
RPUSHX 2.2 O(1)    
LPOP 1.0 O(1)   不存在返回nil
RPOP 1.0 O(1)   不存在返回nil
RPOPLPUSH 1.2 O(1) 原子的,原地址右边弹出目的地址左边插入。如果源地址目的地址相同就是旋转列表,可以实现循环列表 返回弹出元素,不存在就是nil
LREM 1.0 O(N) LREM key count value 移除与value相等的count个值,0表示所有,负数从右向左正数从左向右 被移除的数量。
LLEN 1.0 O(1)   返回列表长度
LINDEX 1.0 O(N) N为遍历到index经过的数量 为啥不叫LGET,怕使用者漏了index参数吗?参数和GET系命令不一致做个区分? 返回下表为index的值,不存在返回nil
LINSERT 2.2 O(N) N 为寻找目标值经过的值   返回成功后列表的长度。如果没找到目标值pivot,返回-1,不存在或空列表返回0
LSET 1.0 O(N) N为遍历到index处的元素个数   OK
LRANGE 1.0 O(S+N) S为start偏移量,N为区间stop-start LRANGE的区间是全闭区间,包含最后一个元素,注意stop取值范围。不过超出下表范围不会引起命令错误。可以使用负数下表,和python用法一致 返回一个列表
LTRIM 1.0 O(N) N为被移除元素的数量 和LRANGE类似,要注意下标 OK
BLPOP 2.0 O(1) LPOP阻塞版本,timeout0表示不超时 阻塞命令和事务组合没有意义,因为会阻塞整个服务器,其他客户端无法PUSH,会退化成LPOP  
BRPOP 2.0 O(1)    
BRPOPLPUSH 2.2 O(1) 实现安全队列  
SADD 1.0 O(N) N是元素个数 2.4版本之前只能一个一个添加 返回总数
SISMEMBER 1.0 O(1)   1/0
SPOP 1.0 O(1) 随机返回一个值并移除, 值或nil
SRANDMEMBER 1.0 O(N) N为返回值的个数 2.6开始支持count 类似SPOP但是不移除 值 nil/数组
SREM 1.0 O(N) N为移除的元素个数 2.4以前只支持单个移除 移除成功的数量
SMOVE 1.0 O(1) 原子的,移除src添加dst 移除成功1,其他0或者错误
SCARD 1.0 O(1) cardinalty基数 不存在返回0
SMEMBERS 1.0 O(N) N为集合基数   不存在返回空集合
SSCAN     SCAN  
SINTER 1.0 O(NxM) N是集合最小基数,M是集合个数 交集  
SINTERSTORE 1.0 O(N*M) 同上,返回并保存结果  
SUION 1.0 O(N) N是所有给定元素之和 并集  
SUIONSTORE 1.0 O(N) 同上,返回并保存结果  
SDIFF 1.0 O(N) 差集  
SDIFFSTORE 1.0 O(N)    
ZADD 1.2 O(M*logN) N 是基数M是添加新成员个数 2.4之前只能一个一个加 成员数量
ZSCORE 1.2 O(1) HGET和LINDEX api风格  
ZINCRBY 1.2 O(logN) HINCRBY  
ZCARD 1.2 O(1) SCARD  
ZCOUNT 2.0 O(logN) ZRANGEBYSCORE拼的 返回score在所给范围内的个数
ZRANGE 1.2 O(logN+M) M结果集基数 N有序集基数 从小到大排序  
ZREVRANGE 1.2 O(logN+M) M结果集基数 N有序集基数 和上面相反  
ZRANGEBYSCORE 1.05 O(logN+M) M结果集基数 N有序集基数 按照score过滤  
ZREVRANGEBYSCORE 1.05 O(logN+M) M结果集基数 N有序集基数    
ZRANK 2.0 O(logN)   返回排名,从0开始,从小到大,0最小
ZREVRANK 2.0 O(logN)   从大到小 0最大
ZREM 1.2 O(logN*M) N基数M被移除的个数 2.4之前只能删除一个 个数
ZREMRANGEBYRANK 2.0 O(logN+M) N基数 M被移除数量   被移除的数量
ZREMRANGEBYSCORE 1.2 O(logN+M) N基数 M被移除数量   被移除的数量
ZRANGEBYLEX 2.8.9 O(logN+M) N基数 M返回元素数量 分值相同 字典序  
ZLEXCOUNT 2.8.9 O(logN) N为元素个数 类似ZCOUNT,前提是分值相同,不然没意义 数量
ZREMRANGEBYLEX 2.8.9 O(logN+M) N基数 M被移除数量 分值相同 被移除数量
ZSCAN     SCAN  
ZUNIONSTORE 2.0 时间复杂度: O(N)+O(M log(M)), N 为给定有序集基数的总和, M 为结果集的基数。   结果集基数
ZINTERSTORE 2.0 O(NK)+O(Mlog(M)), N 为给定 key 中基数最小的有序集, K 为给定有序集的数量, M 为结果集的基数。 交集 结果集基数
ZPOPMAX 5.0 O(log(N)*M) M最大值个数    
ZPOPMIN 5.0 O(log(N)*M)    
BZPOPMAX 5.0 O(log(N))    
BZPOPMIN 5.0 O(log(N))    
PFADD 2.8.9 O(1)   变化返回1不变0
PFCOUNT 2.8.9 O(1),多个keyO(N)   个数
PFMERGE 2.8.9 O(N)   OK
GEOADD 3.2 O(logN)    
GEOPOS 3.2 O(logN) GET  
GEODIST 3.2 O(logN)   返回距离,节点不存在就返回nil
GEORADIUS 3.2 O(N+logM)N元素个数M被返回的个数 范围内所有元素  
GEORADIUSBYMEMBER 3.2 O(N+logM)    
GEOHASH 3.2 O(logN)   字段的hash
XADD 5.0 O(1)    
XACK 5.0 O(1)    
XCLAIM 5.0 O(log N)    
XDEL 5.0 O(1)    
XGROUP 5.0 O(1)    
XINFO 5.0 O(N)    
XLEN 5.0 O(1)    
XPENDING 5.0 O(N) 可以退化为O(1)    
XRANGE 5.0 O(N)    
XREAD 5.0 O(N) 可以退化为O(1)    
XREADGROUP 5.0 O(M) 可以退化为O(1)    
XREVRANGE 5.0 O(N) 可以退化为O(1)    
XTRIM 5.0 O(N)    
EXISTS 1.0 O(1)    
TYPE 1.0 O(1)   类型
RENAME 1.0 O(1) rocksdb不能in-place改key,seek出来,复制value,写旧key删除,写新key。pika为什么不支持? OK
RENAMENX 1.0 O(1) 新key不存在才改 1成功
MOVE 1.0 O(1) redis支持多库,redis多库数据导入pika? 1成功0失败
DEL 1.0 O(N) N为key个数    
RANDOMKEY 1.0 O(1) 怎么实现的O1?pika O(N)  
DBSIZE 1.0 O(1)   所有key数量
KEYS 1.0 O(N) N大会阻塞redis  
SCAN 2.8 O(1)迭代,O(N)完整迭代 keys替代品  
SORT 1.0 O(N+MlogM) 返回结果不是in-place,可以STORE保存  
FLUSHDB 1.0 O(1) drop db OK
FLUSHALL 1.0 O(1) drop all db OK
SELECT 1.0 O(1) 切换数据库 OK
SWAPDB 4.0 O(1) 交换数据库 OK
EXPIRE 1.0 O(1) 单位秒 随机的过期时间防止雪崩 1成功0失败
EXPIREAT 1.2 O(1) 时间戳  
TTL 1.0 O(1)   不存在返回-2过期返回-1其余返回时间
PERSIST 2.2 O(1) 去除失效时间 1成功
PEXPIRE 2.6 O(1) 毫秒为单位 1成功
PEXPIREAT 2.6 O(1) 同expireat  
PTTL 2.6 O(1) 同ttl,2.8以前 失败或不存在都返,回-1后来用-2区分不存在  
MULTI 1.2 O(1) 事务块开始 OK
EXEC 1.2 事务块内执行的命令复杂度和 执行事务块 被打断返回nil
DISCARD 2.2 O(1) 放弃事务块内命令 OK
WATCH 2.2 O(1) 乐观锁 OK
UNWATCH 2.2 O(1) 当EXEC DISCARD没执行的时候取消WATCH OK
EVAL 2.6 O(1) 找到脚本。其余复杂度取决于脚本本身 推荐纯函数,有全局变量会报错  
EVALSHA 2.6 根据脚本的复杂度而定 缓存过的脚本。可能不存在  
SCRIPT LOAD 2.6 O(N) N为脚本长度 添加到脚本缓存  
SCRIPT EXISTS 2.6 O(N) N为判断的sha个数   0 1
SCRIPT FLUSH 2.6 O(N) N为缓存中脚本个数   OK
SCRIPT KILL 2.6 O(1) 如果脚本中没有写,能杀掉,否则无效,只能shutdown nosave  
SAVE 1.0 O(N) N为key个数 保存当前快照到rdb,阻塞,保存数据的最后手段 OK
BGSAVE 1.0 O(N) fork执行复制。,lastsave查看bgsave执行成功 反馈信息
BGREWRITEAOF 1.0 O(N) N为追加到AOF文件中的数据数量 AOF redis自己会重写,该命令是手动重写 反馈信息
LASTSAVE 1.0 O(1)   时间戳
PUBLISH 2.0 O(M+N) channel订阅者数量+模式订阅客户端数量   收到消息的个数
SUBSCRIBE 2.0 O(N) N是channel个数    
PSUBSCRIBE 2.0 O(N) N是模式的个数 通配符模式。满足该通配符字符串的channel  
UNSUBSCRIBE 2.0 O(N) N是channel个数 不指定则退订所有  
PUNSUBSCRIBE 2.0 O(N) N是channel个数    
PUBSUB CHANNELS
PUBSUB NUMSUB
PUBSUB NUMPAT
2.8 O(N) N是频道个数 统计信息,活跃频道,频道关注数 频道模式个数  
SLAVEOF 1.0 O(1) SLAVEOF NO ONE升主 OK
ROLE 2.8.12 O(1)    
AUTH 1.0 O(1)    
QUIT 1.0 O(1)    
INFO 1.0 O(1)    
SHUTDOWN 1.0 O(1) SAVE QUIT有可能丢失数据 该命令屏蔽了后续的写动作?  
TIME 2.6 O(1)   时间戳
CLIENT GETNAME CLIENT KILL CLIENT LIST SETNAME PAUSE REPLY ID 2.6.9
2.4
O(1)
O(N
O(N))
O(1)
   
CONFIG SET CONFIG GET CONFIG RESETSTAT CONFIG REWRITE 2.0…2.8 O(1)O(N)O(1)O(N)    
PING 1.0 O(1)   pong
ECHO 1.0 O(1)    
OBJECT 2.2.3 O(1) 查引用次数,编码格式,空闲状态  
SLOWLOG 2.2.12 O(1)    
MONITOR 1.0 O(N) 监视命令  
DEBUG OBJECT
DEBUG SEGFAULT
1.0 O(1) 调试命令,查对象信息,模拟segfault  
MIGRATE 2.6 O(N) dump key + restore  
DUMP 2.6 O(1)查找O(N*size)序列化    
RESTORE 2.6 O(1)查找O(N*size)反序列化,有序集合还要再乘logN,插入排序的代价    
SYNC 1.0 O(N)    
PSYNC 2.8 NA    
Strings List Set Sorted Set hash stream
APPEND          
BITCOUNT          
BITFIELD          
BITOP          
BITPOS          
DECR          
DECRBY          
GET          
GETBIT          
GETRANGE          
GETSET          
INCR          
INCRBY          
INCRBYFLOAT          
MGET          
MSET          
MSETNX          
PSETEX          
SET          
SETBIT          
SETEX          
SETNX          
SETRANGE          
STRLEN          
Read More

2018年度总结

昨天玩了一天荒野大镖客2,忘记写了。。今天补上 我收藏的待整理的文件和资料都没有处理。。搞不好得拖延到2019年年底。。新年计划就是整理完收藏夹,读更多的书看更多好看的电影电视剧了。

今年最大的改变就是从东北来到深圳。上半年就想走,一直拖延到十月末。总算过来了。在哈尔滨的日子十分悠闲,感觉提前退休了一样。没啥机遇和挑战,也学不到什么真正的东西。都是自己在研究,不能落地的知识那不是知识,何况我在那边就是在闭门造车。

新工作强度也挺大,变化挺快,我感觉自己都有点跟不上了。刚开始第一个月闲得很,然后这个月忙的很,每天东忙西芒东西都不能落地,都是研究。这种工作看起来爽实际上很有危机感,没有输出很容易被干掉。目前没转正应该不会这么快辞退我。抓点紧赶紧输出

还是挺舍不得哈尔滨那边的工作环境的。十分舒适。同事们也都就很友善,即使我看上去很不合群大家也能和我唠起来。舒适。跳出舒适区太难了。我费了大半年才跳出来。

2018年基本上都在做无用功,找工作等等,时间碎片化。看的书电影数量一年不如一年。不过还能检出一些值得推荐的

今年都看了啥电影 剧集

今年我看完了老友记十季和老爸老妈浪漫史九季,我之前以为比较难看完,2018年终于搞定了。

《老友记》比较无害,黄段子也很少,这也是老少咸宜的原因吧。我是不太喜欢。第四季第五季比较好看,第七季往后就可以不看了,太烂。

《老爸老妈浪漫史》比较推荐。太好看了。我反反复复看了两遍,下了网易云听力听了两遍。现在基本上梗如数家珍。好看。这应该是未来这些年情景喜剧的巅峰了。桥段设计和故事叙述模式都十分先进。(而且被爱情公寓抄了个遍)只要不看第九季最后两集,或者最后五分钟,这个剧还是很感人的。

《我是大哥大》也挺好看。浮夸搞笑型的。里面的理子太美了。

《马男波杰克 第五季》,这季十分真实,故事剪辑模式十分先锋。看的爽的不行。bojack又搞砸了,我也是。

《硅谷 第五季》 创业团队总算成功了,Gilfoyle也有对象了。我还没有。

《进击的巨人 第三季》这季总算知道点之前买下的伏笔。不过动画片节奏太慢了。不太推荐。

《非自然死亡》 全方位展示为什么石原里美这么好看的一部日剧。确实好看。

《扯蛋英国史》 一本正经的恶搞。被采访的教授忍者不打记者

《四畳半神话大系》换着法讲一个故事。风格型

《荒川爆笑团》也算有趣。扯淡的

电影

《大佛普拉斯》 他人是深渊。这个电影是2018年最值得看的国产片。很会玩。

《三块广告牌》 杰作。突然的真实。谁都没错,但就是不对。冲突让人难过。

《斯大林之死》 十分讽刺,想看讽刺的这个不要错过

《文科恋曲》 老爸老妈浪漫史里的Ted自导自演的一部作品。讲不要逃避年龄,正确的时间做正确的事。装嫩逃避是不对的。个人比较喜欢

脱口秀以及其他

这些都能在bilibili找到

瑞奇·热维斯:人性 https://movie.douban.com/subject/30156995/
Ricky的段子十分冒犯。比路易CK冒犯多了。这个视频讲的主要就是喜剧中嘲讽元素是为了表达什么,不要因为圣母玻璃心感觉被冒犯了就蹦出来抬杠。首先事实摆对,然后就是角度问题,有人通过喜剧嘲讽,有人简单陈述,但你不能不接受把脑袋埋起来。

克里斯·洛克:塔姆柏林https://movie.douban.com/subject/30148257/
讲的自身以及他人的自我认知。不要太把自己当回事儿

还看了腾讯吐槽大会。可以看看笑果团队讲的(就是那些叫不出名的人),明星基本背稿表达可能不太行。

还看了PewDiePie视频。真是快乐源泉,如果感到烦恼,点开PewDiePie视频,随便傻乐一下

今年都看了啥书

当当优惠和京东优惠可坑苦了我。买的书基本都没有看完

看的都是网上找的电子版。惭愧惭愧。有的实在是买不到

甲骨文 流離時空裡的新生中國
这本书也是我自己审视自己来读的书,这本书真挚。亲切,看一个美国人怎么亲切的描述中国的。这本书能更好的了解这个国家。

花街往事
波澜壮阔的故事。平凡但让人感动

冬泳
这本书是微博上的坦克手贝吉塔写的。东北人。书写的沉稳,锋利。建议东北人人手一本。

鱼翅与花椒
这本书总结起来就是一句话,看饿了。这书有一种让我快速搬到成都的冲动。英国人写的。同时吐槽她们自家的土豆料理。真的亲切。我挺佩服这些老外的。怎么写的呢

剩下的书都是些计算机方面的书,《高性能MySQL》,读完不会MySQL也能胡诌几句忽悠技术面试官,《深入C++对象模型》深入C++细节的入门书,年轻人别学C++,太让人头大。

和同事/朋友吃饭。没有聊关于未来的话题。没有啥未来。走一步算一步健康活着就好啦。哪里想那么多啊。 2016这句话原封不动抄过来 现在明白是做的太少而想的太多。新的一年没有任何宏大蓝图,只求能够独立生活不耽误别人。 外加多和同学朋友唠唠嗑。多对社会做点贡献。 外加保持健康


Read More

MongoDB权威指南笔记

##< MongoDB权威指南>笔记

MongoDB基础知识

  • 文档(行)-》 集合(动态模式的表,集合可以有子集合(GridFS))-》数据库

  • 每个文档有个特殊的键_id (唯一生成方式,时间戳+机器ID+PID+计数器)

  • 命名
  • 集合system保留,注意有些保留字没有强制限定,比如version,就只能用getCollection来访问了,或者跳过直接访问,使用数组迭代语法
  • 数据库,admin local config 保留

  • JavaScriptShell操作 use db CRUD

基本数据结构 ALL IN JSON

  • null 二进制以及代码

  • bool 数值,默认64位浮点型,可以用NumberInt类NumberLong类

  • 字符串,日期(new Date() 直接调用Date构造函数得到的是个字符串,所以这里用new,标识Date 对象,而不是对象生成的字符串)
  • 正则 /foobar/i js正则语法(需要学一下js)

  • 数组,数组可以包含不同类型的元素,数组内容可以建索引,查询以及更新
  • 内嵌文档(嵌套Json也可以建索引,查询以及更新
  • 对象ID
  • _id默认是ObjectID对象,每个文档都有唯一的 _id ObjectId全局唯一的原理
  • 自动生成id 通常能交给客户端驱动程序搞定就不放在服务器,扩展应用层比扩展数据库层要方便

JS Shell

  • .mongorc.js文件,放在bin下自动运行,可以覆盖危险Shell辅助函数,取消定义,也可以启动Shell时 –norc 取消加载
  • Windows用户,该文件会默认生成在用户 用户名或administrator目录下

创建,更新和删除文档

插入文档
  • insert, batchinsert 批量插入,多个文档插入到同一个集合中,多个文档插入多个集合不行。

  • 导入原始数据使用mongoimport (需要学一下go)
  • 插入最大接受48M插入,多于48M会分片插入,如果插入途中有一个文档插入失败,前面的灰尘共,后面的都失败,不是强事务的。
  • 插入校验,添加_id字段,检查大小(目前是小于16M)(可能是不良的设计)
删除
  • db.foo.remove() 删除集合的所有文档删除数据是永久的,不能撤销,也不能恢复
  • drop删除整个表(和数据库一样,快,没有限定条件)
更新文档
  • 文档替换,直接更改文档的字段,扩展,重命名,建立子文档等 可以用=,直接用delete删除字段(只能用替换,不能直接删数据库,用findOne,find返回值是个cursor

  • 文档更新相同字段导致的更新错误(不唯一,有可能给id在前的更新了),索引问题,用id来查找

  • 修改器,一个Json封起来。key是动作,value是个修改的Json,其中,内部key对应修改的字段,value对应修改的值

  • $set == replace or insert, $unset 直接就可以删掉这个键 可以修改内嵌文档

  • $inc == add or insert

  • 数组修改器

  • $push 添加元素 添加一个数组元素,$push 和$each结合使用,添加多个数组
  • 数组作为数据集,保证数据不重复 $ne一个限定集,或使用$addToSet($addToSet和$each可以结合使用)

  • 删除元素 {“$pop”:{“key”:-1}}基于位置,如果基于条件,使用$pull
  • 基于位置的数组修改器 直接使用定位符$来匹配,匹配第一个 db.blog.update({“cmt.author”:”John”},{“$set”:{“cmt.$.author”:”Jim”}})

  • 修改器速度, 主要取决于文档大小是否变化,如果有变化,就会影响速度,因为插入是相邻的,如果一个文档变大,位置就放不下,就会有移动
  • paddingFactor,填充因子,MongoDB位每个新文档预留的增长空间,(db.coll.stats(),Windows上我运行这个没找到paddingFactor项,比较尴尬)

  • upsert update + insert Update第三个参数true 原子性
  • $setOnInsert 考虑寻找并修改同一个字段,多次运行可能会生成多个文档,setOnInsert会找到之前生成的文档,不会多次生成。
  • save shell (db.foo.save(x))在写到这行之前我都是先findOne查回来,改,在Update回去。。。

  • 更新多个文档 db.user.update({“birthday”:”10/13/1978”},{““$set”:{“gift”:”Happy Birthday”}},faset,true)
  • db.runCommand({getLaseError:1}) 查看更新文档数量

  • 返回被更新的文档 findAndModify ?有点没理解
写入安全

查询

  • find 默认匹配全部文档{},也可以写多个条件(and关系)
  • 指定返回,第二个参数指定{“key”:1/0},类似select指定,其中1是选中,0是排除
  • 限制 ?有点没理解

  • 查询条件 “$lt” “$lte” “$gt” “$gte” “$ne”
  • OR “$in” “$or “ “$not” “$nin”
  • 条件语义
特定类型的查询
  • null
  • 正则表达式
  • 查询数组 与查询普通文档是一样的,直接find value能匹配到数组中的元素,就会被选出来
  • $all 特定元素的并列条件,顺序无关紧要,普通find子数组的形式不行,会有顺序限制,如果想找特定位置的元素,需要使用key.index (上面的 key.$就是个迭代形式。)
  • $size 指定长度的数组
  • $slice 返回一个子集 截取一段
  • 数组和范围查询的相互作用,因为查找数组会匹配所有元素,有满足的就会被选出来,所以范围查询可能会和设想的结果不一样
  • 针对数组,使用 $elemMatch 效率稍低
  • 如果该列有索引,直接用min max

  • 查询内嵌文档 用.访问 (URL作为键的弊端)
  • $where 结合函数 会很慢,用不了索引,还有文档转换为js(配合where函数)的开销
  • 注意可能引入的注入攻击

  • 游标 find

  • while(cursor.hasNext()){obj=cursor.next(),do….}
  • cursor.forEach(function(x){…})
  • limit skip sort 类似SQL中的limit sort
  • skip不要过滤大量结果,会慢
  • 不要用skip对结果分页
  • 注意比较顺序
  • 随机选取文档,不要算出所有然后在找随机数,太坑了。可以每个文档加一个随机数key,在key上建索引,然后用随机随机数过滤就ok
  • 也有可能找不到结果,那就反向找一下,还没有那就是空的

  • 高级查询选项
  • 获取一致结果:迭代器修改文档可能引入的问题,修改的文档大小发生变化,结果只能放在文档最后,导致同一个文档返回多次(太坑了)
  • 解决方案,snapshot,会使查询变慢,只在必要的时候使用快照,mongodump

  • 游标的生命周期 默认十分钟自动销毁,有immortal选项

  • 数据库命令 runCommand 实际上内置命令是db.$cmd.findOne({“”})的语法糖

设计应用

索引

  • 和关系型数据库类似,也有explain
  • 复合索引 db.coll.ensureIndex({“key1:1”,”key2:!”})
  • 覆盖索引与隐式索引
  • $如何使用索引
  • 低效率的操作符,基本都不能用索引
  • 范围 要考虑与索引结合导致的效率低下问题
  • $or使用索引

  • 索引对象和数组
  • 可以嵌套对象的任意层级来做索引 a.b.c.d.e.f ,得用最后级别的对象来搜索才能用上这个索引,只用其中一个字段是不行的。
  • 索引数组 实际上是对每一个数组元素都建立了索引,代价很大,如果有更新操作,那就完了,不过这里可以对字段来查找,毕竟每个字段都有索引。但是索引不包含位置信息。没法针对特定位置来找。
  • 限定只有一个数组可以索引。为了避免多键索引中索引条目爆炸

  • 多键索引 多键索引可能会有多个索引条目指向同一个文档 ?为什么 会很慢,mongo要先去重
索引基数
  • 基数 衡量复杂度,某个字段拥有不同值的数量,基数越高索引越有效,索引能将搜索范围缩小到一个小的结果集

  • 查询优化器

何时不应该使用索引
  • 索引扫描和全盘扫描在集合与返回结果上进行比较

索引类型

  • 唯一索引 和主键概念一致(虽然已经有_id 这个主键了,新增的唯一索引是可以删除的)db.coll.ensureindex({“ukey”:1},{“unique”:true})

  • 复合唯一索引。一例 GridFS files_id:ObjectId, n:1 所有索引键的组合必须唯一

  • 去处重复,建唯一索引失败 ,加个条件ensureindex({“ukey”:1},{“unique”:true,”dropDups”:true}) ( 慎用)

  • 稀疏索引 和关系型数据库中的索引不同?
  • 有些文档可能没有索引这个字段,建立稀疏索引就把这种过滤掉了。

  • 索引管理 system.indexes db.coll.getIndexes()
  • 标识索引 keyname1_dir1_keyname2_dir2….keynameN_dirN 名字,方向
  • 修改索引 db.coll.dropIndex(“indexname”)

特殊的索引和集合

固定集合capped collection
  • 事先创建好,大小固定,类似循环队列 在机械硬盘写入速度很快 顺序写入,没有随机访问。如果拥有专属磁盘,没有其他集合的写开销,更快

  • db.createCollection(“my_coll”,{“capped”:true,”size”:100000,”max”:100}); 也可以用普通的集合转成固定集合 db.runCommand(“convertToCapped”:”test”,”size”:100000),无法将固定集合改成非固定集合,只能删掉
  • 自然排序
  • 循环游标 tailable cursor 判断cursor是否tailable 一直循环处理,直到cursor死了(灵感来自tail命令)

  • 没有_id索引的集合,在插入上带来速度提上,但是不符合mongod复制策略
TTL索引
  • 创建索引有个expireAfterSecs参数 db.foo.ensureIndex({}”lastUpdated”:1},{“expireAfterSecs”:60*60*24})
全文本索引
  • 自然语言处理?

  • 优化的全文本索引

  • 在其他语言中搜索,设定语言

地理空间索引2dsphere

  • 复合地理空间索引
  • 2D索引
使用GridFS存储文件
  • 使用场景,不经常改变的需要连续访问的大文件,缺点性能低,文件是多个文档,无法同一时间对所有文档加锁
  • mongofiles put foo.txt get foo.txt

聚合

聚合框架
  • 管道pipeline 投射project 过滤filter 分组group 排序sort限制limit跳过skip,都在内存中进行,顺序不影响结果(但可能影响性能)
db.articles.aggregate({"$project"{"author"1}}

{"$group":{"_id":"author","count":{"$sum":1}}},

{"$sort":{"$count":-1}},

{"$limit":5})

$match 最开始用可以用上索引减小结果集

$project 修改字段前先用上索引

  • 管道表达式 数学表达式 $add $ subtract $multiply $divide $mod 都是接一个数组对象的

  • 日期表达式 $year $month $week $dayOfMonth $dayOfWeek $dayOfYear $hour $minute $second

  • 字符串表达式 $substr $concat $toLower:expr $toUpper:expr

  • 逻辑表达式

  • $cmp $strcasecmp $eq ne ge get lt lte and or not

  • $cond :[boolexpr,trueexpr, falseexpr] $ifNull:[expr,replacementexpr]

$group 分组 SQL - groupby

  • 算术操作符 $sum $average:value
  • 极值操作符 $max:expr $min:expr $first:expr $last:expr
  • 数组操作符 $addToSet $push
  • 分组行为不能用于流式工作,有个归结的过程,是个整体
  • $unwind 拆分成独立的文档
  • $sort limit skip
  • 使用管道开始阶段前尽可能把多于的文档和字段过滤掉,可以排序来方便使用索引 $match
mapreduce?
聚合命令 和上面的不一样,但是感觉差不多。
  • db.col.count()
  • runCommand({“distinct”:”key1”,”…”}
  • runCommand({“group”:} SQL group by

应用程序设计

注意一致性,以及mongodb不支持事务

复制

创建副本集 replica set

副本集概念 大多数,半数以上,低于半数,全部降备

选举机制

选举仲裁者 最多一个 arbiter 只参与选举,放在外部观察的故障域中,与其他成员分开,尽量不要用,虽然轻量,极端场景 1主1备1仲裁,主挂掉,备升主,新主机还要承担起复制备机的责任

尽量使用奇数个数据成员

优先级与被动成员

隐藏成员?为啥有这个设定

延迟备份节点,主节点意外被毁被删库还能活过来 不支持回滚只好这么搞

备份节点可以手动设定不创建索引(得是被动成员,优先级为0

副本集的组合

同步 操作日志oplog,主节点local数据库的一个固定集合,备份节点查这个集合来进行复制,每个节点维护自己的oplog,每个成员都可以作为同步源提供给其他成员使用

  • oplog同一条记录执行一次和执行多次效果一致

初始化同步 会先将现有的数据删除 克隆 通过oplog复制数据,然后复制oplog 然后建索引

  • 如果当前节点远远落后于同步源,oplog通不过城最后一步就是将创建索引期间的所有操作全同步过来,防止该成员成为备份节点
  • 克隆可能损坏同步元的工作集?
  • 克隆或创建索引耗时过长,导致新成员与同步源oplog脱节 初始化同步无法正常进行

处理陈旧数据 stale 会从成员中的oplog(足够详尽)中同步恢复,如果都没有参考价值,这个成员的复制操作就会停止,这个成员需要重新进行完全同步

  • 心跳 让主节点知道自己是否满足集合的大多数条件
  • 成员状态 主节点备份节点
  • STARTUP成员刚启动 ->STARTUP2初始化同步 ->RECOVERING 成员运转正常,但暂时还不能处理读取请求 比如成为备份节点前的准备活动,在处理非常耗时的操作(压缩,replSetMaintenance),成员脱节
  • ARBITER 仲裁者
  • DOWN变的不可达 UNKNOWN 无法到达其他成员 分别描述对端和自己
  • REMOVED移除工作集
  • ROLLBACK 数据回滚
  • FATAL发生了不可挽回的错误 grep replSet FATAL 通常只能重启服务器

  • 选举 选举通常很快,太多错误发生比较倒霉的话可能花费较长
  • 回滚
  • 一个场景,假如主节点op写写死了,然后选举新的主节点没有这条记录,则原主节点需要回滚
  • 如果回滚失败,备分节点差的太多,升主回滚内容太多,mongodb受不了。

从应用程序连接副本集

主节点崩溃的错误?最后一次操作成功失败?留给应用程序去查询
  • db.runCommand({“getLastError”:1,”w”:majority})检查是否写入操作被同步到了副本集的大多数 阻塞,可以设置超时时间,阻塞不一定失败,超时不一定失败

  • 使用majority选项可以避免一场写入失败导致的回滚丢失。一直阻塞直到大家都有这条数据,或失败。
  • “w”也可以设定其他值,值包含主节点。应该设置成n+1 默认1
自定义复制保证规则
  • 保证复制到每个数据中心的一台服务器上
  • 重写rs.config,rs.reconfig(config) 添加字段
  • 隐藏节点到底是干啥的?
将读请求发送到备份节点
  • 对一致性要求不是特别的高
  • 分布式负载(另一个选择,分片分布式负载)
  • 何时可以从备份节点读取数据?
  • 失去主节点时,应用程序进入只读状态-》主节点优先

管理

维护独立的成员
  • 单机模式启动成员 改端口,重启时不使用replSet选项,然后访问维护,其他成员会连接失败,认为它挂了,维护结束后重新以原有的参数启动,自动连接原来的副本集,进行同步
副本集设置

local.system.replSet

var config = {"_id":setname,

"members":["_id":0,"host":host1,

"_id":1,"host":host2,

"_id":2,"host":host3

]}

re.initiate(config)

修改副本集成员rs.add remove reconfig

修改成员状态 re.stepDown降备rs.freeze (time)在time时间内阻止升主

使用维护模式 RECOVERING replSetManitenanceMode

监控复制

获取状态 replSetGetStatus

复制图谱,指定复制syncFrom

  • 复制循环
  • 禁用复制链,强制从主节点复制

计算延迟 lag

调整oplog大小 维护工作的时间窗,超过改时间就不得不重新进行完全同步

  • 在被填满之前,没有办法确认大小,即使他是个capped collection,也不可以运行时调整大小,因为他是个capped collection

  • 修改步骤,如果是主节点,先退位,关闭服务器,单机模式启动,临时将oplog中最后一条insert操作保存到其他集合中 确认保存成功 删除当前oplog,创建一个新的oplog,将最后一条操作记录写回oplog确保写入成功

var cursor = db.oplog.rs.find({"op":i})
var lastInser = cusor.sort({"$narutal":-1}).limit(1).next()
db.tempLastOp.save(lastInsert)
db.tempLastOp.findOne()
db.oplog.rs.drop()
db.createCollection("oplog.rs",{"capped":1,"size":10000})
var temp = db.tempLastOp.findOne()
db.oplog.rs.insert(temp)
db.oplog.rs.findOne()
从延迟备份节点中恢复
  • 简单粗暴方法,关闭所有其他成员,删掉其他成员的数据,重启所有成员,数据量大可能会过载
  • 稍作修改,关闭所有成员,删掉其他成员的数据,把延迟备份节点数据文件复制到其他数据目录,重启所有成员,所有服务器都与延迟被分界点拥有同样大小的oplog
创建索引

创建索引消耗巨大,成员不可用,避免最糟糕的情况,所有成员节点同时创建索引

  • 可以每次只在一个成员创建索引,分别备份节点单机启动创建索引然后重新启动,然后主节点创建索引
  • 可以直接创建,选择负载较少的空闲期间
  • 修改读取首选项,在创建节点期间把读分散在备份节点上
  • 主节点创建索引后,备份节点仍然会复制这个操作,但是由于备份节点中已经有同样的索引,实际上不会再次创建索引

  • 让主节点退化为备份节点,执行上面的步骤,这时就会发生故障转移
  • 可以使用这个技术为某个备份节点创建与其他成员不同的索引,在离线数据处理时非常有用?
  • 但是如果某个备份节点的索引与其他成员不同,它永远不会成为主节点,应该将它的优先级设为0
  • 如果要创建唯一的索引,需要确保主节点中没有被插入重复的数据,或者首先为主节点穿件唯一索引,否则主节点重复数据,备份节点复制出错,备份节点下线,你不得不单机启动,删除索引,重新加入副本集
在预算有限的情况下进行复制

没有多台高性能服务器,考虑将备份节点只用于灾难恢复

  • “priority”:0 优先级0, 永远不会成为主节点
  • “hidden”:1 隐藏,客户端无法将读请求发送给他
  • “buildIndexes”:0 optional 备份节点创建索引的话开极大地降低备份节点的性能。如果不在备份节点上创建索引,从备份节点上恢复数据需要重新创建索引
  • “votes”:0 只有两台服务器的情况下,备份节点挂掉后,主节点仍然一直会是主节点,不会因为达不到大多数要求而推诿,如果还有第三台服务器,设为仲裁者
主节点如何跟踪延迟

local.me local.slaves

##### 主从模式 传统模式,会被废弃

从主从模式切换到副本集模式 

  • 停止所有系统的写操作哦,关闭所有mongod 使用–replSet选项重启主节点不再使用–master 初始化这个只有一个成员的副本集,这个成员会成为副本集中的主节点,使用–replSet和–fastsync启动从节点,使用rs.add将之前的从节点添加到副本集,然后去掉fastsync

让副本集模仿主从模式行为

  • 除了主节点,都是优先级0投票0 备份节点挂了无所谓,主节点不会退位,主节点下线手动选父节点,指定优先级和投票,运行rs.reconfig(config,{“force”,1})

分片

配置分片 数据分片
拆分块
  • 有服务器不可达,尝试拆分,拆分失败 拆分风暴
  • mongos频繁重启,,重新记录点,永远达不到阈值点
均衡器 config.locks

均衡阈值

选择片键

如何在多个可用的片键中作出选择?不同使用场景中的片键选择?哪些键不能作为片键?自定义数据分发的可选策略?如何手动对数据分片

分片的目的
  • 减少读写延迟?将请求分布在近的机器或高性能服务器
  • 增大读写吞吐量?集群1000次/20 ms 可能需要5000次/20ms,需要提高并行,保证请求均匀分布在各个集群成员上
  • 增加系统资源?每GB数据提供Mongodb更多的可用RAM,使工作集尽量小
数据分发
  • 升序片键 date ObjectId

  • 随机分发片键
  • 基于位置片键 tag
片键策略
  • 散列片键 数据加载速度
  • 缺点 无法范围查询
  • 无法使用unique和数组字段
  • 浮点型的值会被先取整,然后算hash

  • GridFStab的散列片键
  • 流水策略,一个SSD接着写,指定tag,更新tag范围,把片转到其他磁盘上
  • 多热点?
片键规则和指导方针
  • 不能是数组
  • 片键的势要高一些,或者组合起来

##### 控制数据分发

  • addShardTag第二个参数可以指定高低,然后tagrange将不同集合放到不同的分片上。

  • 手动分片 moveChunk

分片管理

检查集群状态 sh.status() use config config.shards config.chunks config.collections cofig.changelog

应用管理

了解应用的动态

mongotop

mongostat

  • insert/query/update/delete/getmore/command

  • fulshed mongod将数据刷新到磁盘的次数
  • mapped所映射的内存大小,约等于数据目录大小
  • vsize 虚拟内存大小,通常为数据目录二倍
  • res正在使用的内存大小
  • locked db锁定时间最长的数据库
  • qrw 阻塞的读写队列大小
  • arw正在读写的客户端数量
  • netin 网络传输进来的字节数
  • netOut网络传输输出的字节数
  • conn打开的连接数

数据管理

身份认证

建立和删除索引

OOM Killer 日志在/var/log/messages

数据预热

for file in /data/db/* do

dd if=$file of=/dev/null

done
  • fine-grained 细粒度的预热

  • 将集合移动至内存

db.runCommand({"touch":"logs","data":1,"index":1})
  • 加载特定的索引 覆盖查询
  • 加载最新的文档
  • 加载最近创建的文档 结合ObjectId
  • 重放应用使用记录 诊断日志?
压缩数据
  • 会进入RECOVERING模式

  • 运行repair来回收磁盘空间,需要有当前数据大小同样的空闲空间,或指定外部的磁盘目录,修复路径。

移动集合
  • renameCollection 改集合名。不用担心性能消耗
  • 数据库间移动,只能dump&restore也可以用cloneCollection

预分配数据文件

if test $# -lt 2 || test $# -gt 3 then
echo "$0 <db> <number-of-files> "
fi

db =$1
num=$2
for i in {0..$num}
do
echo "preallocation %db.$i"
head -c 2146435072 /dev/zero >$db.$i
done

持久性

日记系统

批量提交写入操作 100ms 或写入数据MB以上

设定提交时间间隔

关闭日记系统 数据可能损坏,两种替代方案

  • 替换数据文件,用副本集的,或者初始化同步,重启同步
  • 修复数据文件,mongod内置(repair)或mongodump 耗时较长

关于mongod.lock文件,别手动删。。恢复数据再搞。也有可能是隐蔽的异常退出

MongoDB无法保证的事项

  • 检验数据损坏
  • 副本集的持久性

服务器管理

停止和启动MongoDB

监控MongoDB

  • 内存使用情况
  • 跟踪监测缺页中断
  • 索引树的脱靶次数
  • IO延迟
  • 后台刷新平均时间,将脏页写入磁盘所花费的时间
  • 跟踪监测性能状况
  • 空余空间
  • 监控副本集
  • oplog长度

备份

  • 文件系统快照,开启日记系统,文件系统本身支持快照
  • 复制数据文件,先db.fsyncLock() 然后cp就完了
  • 不要同时使用fsyncLock和mongodump 会死锁

部署

Read More

MongoDB中的装饰器模式

实现在util/Decorable.h中 本质是CRTP的一个使用

子类继承Decorable 就可以了。能保证每个实例在不同的装饰器实例上有不同的表现,完全正交。

装饰的原理:

先定义装饰器实例DecorableInstance,“被装饰的类”

通过DecorableInstance::declareDecoration调用获得若干“装饰”实例

用DecorableInstance的生成若干实例,每个Decoration在DecorableInstance的表现都是分开的。

每个组件都是个加强版的单例,可以直接通过declareDecoration()实例来访问,对应每个被装饰的类都不一样

简单示例在util/decorable_test.cpp中

具体在MongoDB中

比如ServiceContext 本身应用了这个装饰

class ServiceContext : public Decorable<ServiceContext>// service_context.h

通过ServiceContext::declareDecoration来为ServiceContext添加“装饰”组件,简单grep

...
mongo/src/mongo/util/net/listen.cpp:const auto getListener = ServiceContext::declareDecoration<Listener*>();

直接通过getListener(ser)来访问当前service_context的Listener组件的相关信息。

类似还有许多类是这么实现的

mongo/src/mongo/db/client.h:class Client: public Decorable<Client> {
mongo/src/mongo/db/operation_context.h:class OperationContext : public Decorable<OperationContext> {
mongo/src/mongo/db/service_context.h:class ServiceContext : public Decorable<ServiceContext> {
mongo/src/mongo/transport/session.h:class Session : public std::enable_shared_from_this<Session>, public Decorable<Session> {

在MongoDB中的 类图

img

实现原理

Decorable有DecorationRegistry和DecorationContainer成员

其中

  • DecorationRegistry内部持有DecorationInfoVector和totalsize,将数组偏移信息用DecorationDescriptorWithType 抛出来
  • DecorationContainer内部持有DecorationRegistry(Decorable的)和一个字符数组,间接通过DecorationContainer来访问DecorationRegistry
  • DecorationContainer构造函数会构造DecorationRegistry中的DecorationInfo对象
  • 字符数组就当一块内存使用,连续排放各种装饰对象实例T,placement new
  • 每个DecorationInfo会记录自己的index,index是std::alignment_of::value算出来的。

  • DecorationDescriptorWithType是DecorationDescriptor的封装,DecorationDescriptor内部就记录一个index,通过调用一层一层的把index抛出来

调用declareDecoration会间接调用DecorationRegistry->declareDecoration,底层调用栈是DecorationContainer构造 ->返回DecorationDescriptor -> 返回DecorationDescriptorWithType ->返回到Decoration

当使用declareDecoration生成的实例的时候,实际上调用的是T& Decoration::operator(),

该函数会调用把Decorable内部的DecorationContainer传进去,结合该Decoration自身记录的index来定位到具体的DecorationInfo的T

挺复杂的。没能理解为啥实现的这么复杂

PS1: CRTP常见用法

singleton

template <class T>

class singleton{

public:

singleton()=delete;

static void release(){  delete p;   p=nullptr;}

static T* get(){

    if (!p)p=   new T();    

    return p;

}

static T * p;

};

然后单例类就继承singleton就可以了

还有比较常见的是std::enable_shared_from_this

其他用法见WIKI

PS2: plantUML

@startuml
class Decorable {
 -_decorations:DecorationContainer 

 ~static DecorationRegistry* getRegistry()
 +static Decoration<T> declareDecoration()
}


class Decoration{
 -_raw:DecorationContainer::DecorationDescriptorWithType<T>
 +explicit Decoration(DecorationContainer::DecorationDescriptorWithType<T> raw)
 +T&:operator()
}

class DecorationRegistry{
 -_decorationInfo:std::vector<DecorationInfo>
 -_totalSizeBytes:size_t
 +DecorationContainer::DecorationDescriptorWithType<T> declareDecoration()
 +size_t getDecorationBufferSizeBytes()
 +void construct(DecorationContainer* decorable) const
 +void destruct(DecorationContainer* decorable) const
 ~DecorationContainer::DecorationDescriptor declareDecoration(size_t sizeBytes, 
size_t alignBytes, 
function<void(void*)>constructor, 
function<void(void*)>destructor)
}

class DecorationInfo {
 -descriptor:DecorationContainer::DecorationDescriptor 
 -constructor :function<void(void*)>
 -destructor :function<void(void*)>
 +DecorationInfo(DecorationContainer::DecorationDescriptor descriptor,
                       function<void(void*)>constructor,
                       function<void(void*)>destructor)
}

class DecorationContainer {
 -const DecorationRegistry* const _registry
 -const std::unique_ptr<unsigned char[]> _decorationData
 +explicit DecorationContainer(const DecorationRegistry* registry)
 +~DecorationContainer()
 +T& getDecoration(DecorationDescriptorWithType<T> descriptor)
 +void* getDecoration(DecorationDescriptor descriptor)

}

class DecorationDescriptorWithType<T> {
 - _raw: DecorationDescriptor
 -friend class DecorationContainer
 -friend class DecorationRegistry
 +explicit DecorationDescriptorWithType(DecorationDescriptor raw)
}

class DecorationDescriptor {
 -_index:size_t
 -friend class DecorationContainer;
 -friend class DecorationRegistry;
 +explicit DecorationDescriptor(size_t index)
 
}

Decorable *- DecorationContainer
DecorationContainer *- DecorationRegistry
DecorationContainer +- DecorationDescriptor 
DecorationContainer +- DecorationDescriptorWithType
DecorationRegistry *- DecorationInfo 
DecorationInfo *- DecorationDescriptor 
Decoration *- DecorationDescriptorWithType
DecorationDescriptorWithType *- DecorationDescriptor

Decorable --> Decoration 
Decorable --> DecorationRegistry
DecorationRegistry -->DecorationDescriptorWithType
@enduml
Read More

(译)写好Pull Requests(PR)

原文链接

野生翻译,欢迎意见

最近和同事聊了聊关于合入请求(PR)的事儿,进过几个回合的讨论我觉得还是写下我的想法(对我俩都好)

简单介绍一下PR,GitHub术语,是提交请求改变代码库代码的一个方法,在过去(以及现在),内核社区一直用邮件系统。在GitLab这个叫合入请求(Merge Request)。以下原则在任何场景(代码管理方法)都适合使用

动机

动机是决定一个PR如何结束的主要因素,当我建了一个PR,我的主要动机是

  • 我希望评审员能理解我改动的意图
  • 评审员应该能够指出改动中遗漏疏忽的点

当我考虑这些动机,我的意图反应在我创建的PR改动中,当我的首要动机是尽快的解决想解决工单,这个PR在评审员严重可能就是噩梦。我不是贬低修改工单这个需求,尽管这和工资息息相关,单这个动机是次要的,在这片文章中我会列出我认为创建一个PR中至关重要的三条原则

1.PR 改动特别大

当一个PR特别长,这对评审员是个心智负担,改动越长,评审员想要在脑海中记住变更的全貌就越难。我倾向于短的PR,对于我个人可能多点努力,但是能保证我的PR能轻松的审计并及时合入,长的PR带来的长时间的评审

2.如果你非得传大量改动,使用git commits

有时候会遇到这样的场景:很难写出小的改动(PR),要不就功能实现不完整,要不就测试不通过,或者你的啥理由,这种场景我最起码把这些改动拆成小的git commits,这样评审能通过我的git log来看改动过程

3.一个PR里不要提交太多改动

当你改动一个bug的时候,往往也想顺便改个变量名字顺便改个文件结构,请不要这么做。这会使评审走读原本的改动变得困难。本来就是几个小改动,上面这样改可能看起来就是一大堆函数添加删除改动很大。将这种改动和修bug拆开

代码是写出来给别人读的。我认为这在社区中已经说过很多次,PR更应该遵循。它表达出我作为一个作者在项目中改动的意图。这不简单,这需要时间和思考。但利大于弊。如果我希望评审做好工作,那我的工作应该让他更轻松。记住这个动机,它会改变创建PR这个流程


Read More

(译)代码审核:关于信号

//翻译的十分糟糕,就当做自己的阅读笔记了原文点击

POSIX信号是个挺让人畏惧的话题,在这个帖子中,我会用几个真实环境中的例子来消除这些疑惑,看这些例子是如何通过信号来解决问题的

第一部分 POSIX 信号(Linux)

POSIX 信号有很复杂的规则,伴随而来的是一些bug(”段错误“带来的恐惧),基本上在核心代码中很少出现

本帖希望列举一些有用的依赖信号的设计模式并且解释内部信号的传递过程。我会主要在Linux环境上阐述,我以前的工作也基于Linux,大家对Linux或Unix-like系统也熟悉

这个帖子不会包含所有信号的整体介绍,这已经有很多不错的帖子列举了

我只会指出大家在使用信号时的错误观念

thread-local signal masks 和全局handler回调函数

我工作用的系统上有很多bug,和开源代码中一样,这些bug的根源在于对下列基本概念的误解

​ 信号处理(是否注册回调函数,回调函数做什么)是全局函数

​ 信号掩码(是否一个线程可以收到信号)是thread-local类型

这个现象一部分原因在于 线程工作之前信号就已经处理好了,所以就没必要为线程写特定的信号回调handler了,

另一部分原因在于POSIX sigprocmask(3) 文档包含了下面这行吓人的话

​ sigprocmask函数在多线程进程中是未明确的(unspecified )

这技术上是正确的,POSIX值明确表示pthread_sigmosk在多线程环境中是安全的

pthread_sigmask和sigprocmask在linux上的区别是pthread_sigmask(libc)是sigprocmask(syscall)实现的

信号到任意一个线程

POSIX标准明确区分了两种信号的产生

Thread-targeted 信号,标准明确了在线程中任何动作触发的信号会在这个线程收到

即信号从哪个线程生成就会给哪个线程

Process-targeted 信号,任何不是线程产生的信号就是Process-targeted信号

那些和进程ID或进程组ID或异步事件相关的生成的信号,会给进程本身,比如终端活动

如果你关心的信号时thread-targeted,其他线程是收不到的,如果你阻塞他(sigmask是thread-local对象),你就是在用默认的处理方式

如果这个信号是进程相关的,任何进程都能收到,然而,这不是什么魔法,内核中的代码能自圆其说,主线程(tid==pid)会尝试收到该信号,其他的线程会循环获取来平衡信号传递

人们想到许多可以知道 方法去处理这些乱七八糟的东西-从信号管道 到一个信号处理线程,总有一些你可以放到你应用中的点子,或许,你可以通过kill和tgkill发送信号

你不可以处理“致命”错误

POSIX标准对信号处理回调说过

对于 SIGBUS, SIGFPE, SIGILL, SIGSEGV这些不是由kill(), sigqueue(), raise(),生成的信号,假如用回调函数捕获处理了这些信号,之后程序的行为将是未定义的

在Linux上,内核会重新发陷阱指令并重新发信号,这个行为是ABI的一部分,不会改变

这也说明,程序返回并不是跳出信号处理回调函数的唯一办法,也可以调用setjmp/longjmp 或者make/set/getcontext (不反回),可以在程序处理期间做一些有意思的事。实际上POSIX在一开始就考虑到了,这也是为什么信号掩码,sigsetjmp siglongjmp还保留至今。

    thread_local jmp_context;
    thread_local in_dangerous_work;
    handler() {
      if (in_dangerous_work) {
        in_dangerous_work = false;
        siglongjmp(&jmp_context, 0);
      }
      // call previous handler here
    }
    work() {
      register_handler_for_dangerous_work(&handler);
      // calls sigaction for the signals we care about
      // the second argument means that the return
      // value from the sigsetjmp will be 1 if we
      // jumped to it
      if (sigsetjmp(&jmp_context, 1) == 0) {
        in_dangerous_work = true;
        do_dangerous_work();
        in_dangerous_work = false;
      } else {
        // we crashed while doing the dangerous work,
        // do something else.
      }
    }

通过使用thread-local变量和longjump 我们可以安全的识别有风险的部分并且挽救回来,且不影响整体的正确性

例子中的可能需要放到沙盒中搞危险工作包括

  • 与不能保证安全的库(ABI)进行交互
  • 在没有简单的方法查询每条指令的详细存在情况下,检测平台对CPU扩展指令的支持情况(咳咳,arm)
  • 全地与程序的其他部分进行竞争(当所做的工作不是和竞争相关的,例如,收集特定于线程的诊断数据)

信号处理回调函数不能做什么有用的事儿

信号处理回调函数不过是个小函数去打断内核信号动作,函数可以操作一个人一多 上下文,可以访问当前被打断状态下的任何东西的任何状态(SA_SIGINFO flag中给了ucontext访问券,包含了在信号传递过程中所有线程寄存器的状态),并且有足够清晰的语义来分析这些状态是怎么被传递和处理的

然而这需要谨慎的设计出正确的同步原语,不是不可能,我也希望下面这个项目能演示出来

第二部分 一些信号的有趣用法

下面是一些我在工作中和google中而然发现的关于信号的有趣用法

虚拟机内部实现(JavaScriptCore)

当实现一个虚拟机,一个需要你实现的机制是在一个设定的时间点挂住一个线程的能力,有时,你需要实现这个机制去遍历这个线程的所有堆栈来进行垃圾回收(GC),或者你需要这个功能区实现调试器断点功能。

javascriptCore,webkit‘s javascript 内核使用信号来实现Linux上的悬停/恢复原语,它也是用信号来实现所谓的“VM陷阱” -就是线程运行时附到调试器断点,结束线程等等操作

在虚拟机中的异常处理 (JavaScriptCore,ART,HotSpot)

继续虚拟机中使用信号的话题,另一个有趣的用法是允许内存踩踏发生,检测出来,抛出异常

举个例子,在java中,当程序引用一个无效的引用,虚拟机会抛出NullPointerException异常

当虚拟机编译程序(比如JIT编译,或ART的例子,AOT ),NullPointerException不是个主要路径,虚拟机可以省略所有的null检查代码。然而,为了保证运行时正确,当编译过的代码抛出了异常,虚拟机会使用一个信号回调函数来处理

实际上,如果你读一下链接里的Hotspot虚拟机代码,虚拟机很多功能都通过信号来实现,除零错误和堆栈溢出就在那里

用户态页错误(libsigsegv)

在GNU libsigsegv项目中有一个SIGSEGV信号处理的有趣的应用,主页地址

  • 持久化数据库内存映射访问
  • 通用的垃圾收集器
  • 堆栈溢出处理函数
  • 分布式共享内存

当处理内存映射文件,一大堆控制动作藏在用户态程序和内核中,当预取页文件,不管我们正在连续读还是随机读,读硬盘上的数据,什么时候把数据从脏页刷到硬盘,这些决定都是内核代表用户态程序去做的

通过处理SIGSEGV信号,(无论是否是libsigsegv还是标准POSIX调用),我们就获得了执行的控制权,可以自由获取地址空间,自己做决定。

这个需求普遍通用,linux内核实现了userfaultfd,更简单的实现用户态页错误

性能分析(gperftools)

一些POSIX性能分析API是基于信号的,比如POSIX测量CPU时间的方法,使用setitimer 和ITIMER_PROF,这个定时器发送一个线程检测信号SIGPROF到线程移动指定的CPU频率(?)

比如,gperftools项目用setitimer和栈回溯来实现CPU-time堆栈跟踪

崩溃分析(breakpad)

最近我们接触到信号处理的最平常用法 - 崩溃报告,实际上并没有实际处理信号,只是记录发生

breakpad实现了崩溃手机系统,通过处理SIGSEGV,SIGSBRT和其他终端信号和通过信号处理函数尽可能的收紧更多的信息,它记录了寄存器的状态,不活了所有线程的堆栈信息,并且尽可能的展开崩溃线程的对竹山,这些动作是的信号处理函数中预分配的内存在不安全的上下文下来获得的

结论

希望这篇文章能让我们了解这个常常令人恐惧的POSIX信号世界。我希望它能启发你或至少能驱散你的恐惧。我知道我很喜欢读这样的东西。


Read More

^