Redis基础学习笔记

  Memcached和Redis都是非常有名的旁路缓存,但显然这些年Redis的风头大大盖过了Memcached,Memcached作为一个单纯的键值缓存或许够用,但是相比而言Redis提供了丰富的数据结构、主从复制、持久化的功能,俨然就是个高性能的精简数据库(所以他也被认为NoSQL的一种),所以如果两者性能没有大的差异的话估计正常人都会选择Redis。
  缓存用于降低IO延迟,避免重复存取、重复计算等效果显著;而且在运营技术越来越讲究的情况下,无状态服务的弹性计算和容灾也越来越流行,无状态的服务器可以快速添加到负载均衡中,失效的服务器也可以快速被替换掉,不过这些无状态服务也通常也会采用缓存来共享数据(虽然数据库的SELECT FOR UPDATE也可以实现一定的同步功能,通常用在对数据完整性要求高、性能求其次的情况下);而且Redis本身是单线程的,且提供简单的流水(事务)操作机制,所以基于Redis的简易分布式应用案例也数见不鲜,比如分布式锁、分布式队列、分布式ID生成器等案例。
  本文就对Redis的基本数据类型进行整理,同时对其流水化、持久化等特性进行整理。

一、数据结构和基本命令

  Memcached只支持简单的键值存储,而Redis除了支持字符串值外,还支持列表、集合、散列表、有序集合这四种数据类型,这和很多编程语言内置的数据集合类型极为相似,更为神奇的是根据不同的数据类型还能进行一些逻辑、集合运算,使用这些Redis内置的数据结构和操作支持很容易组建出简单逻辑事务。
redis

1.1 Redis基本数据结构

1.1.1 STRING 字符串

  字符串类型可以用来存储字符串、整数或者浮点数,支持对整个或者部分字符串的字符串常用操作,而对数字类型提供自增、自减操作。
  SET、GET、DEL是常见的键值操作命令。
  INCR | DECR 将键存储的值自增、自减1
  INCRBY | DECRBY 将键存储的值自增、自减整数值
  INCRBYFLOAT 将键存储的值加上浮点数
  需要注意的是,将对存储在Redis中的字符串进行自增等计算时,如果该字符串可以被解释为十进制整数或者浮点数,则在原来的基础上进行正常的计算,而如果对一个不存在的键,或者其值为空的键进行上述操作,其初始值被假定为0,而如果其值无法被解释为数字的时候,Redis将返回一个错误。
  APPEND 将值追加到指定键当前值的末尾
  GETRANGE | SETRANGE 获取|设置键对应值的指定偏移子串,其范围两端都是闭区间
  GETBIT | SETBIT 将字符串看作是二进制数,获取或者设置指定位置的值
  当上面的范围函数所指明的范围超过值的长度时候,根据情况会把末尾将会使用空字符(null)或者0来进行补充。
  BITCOUNT 统计二进制字符串中1的数量
  BITOP 对一个欧哲多个二进制串进行按位逻辑运算

1.1.2 LIST 列表

  为一个链表,链表的每个节点都包含了一个字符串,链表中存储的元素是保持插入顺序的。该类型支持从链表的两端压入或者弹出元素;根据在列表中的偏移量对链表进行修剪;一次读取单个或者多个元素操作;根据值查找或者移除元素。
  LPUSH | RPUSH 将指定元素从左边或者右边推入队列,可以一次同时推入多个值
  LPOP | RPOP 将列表最左边|右边的元素弹出去,并返回被弹出的值
  LLEN 返回列表中元素的个数
  LRANGE 获取列表在给定范围上的所有值,位置从0开始索引,-1表示某尾
  LINDEX 获取列表在指定位置上的单个元素
  LTRIM 对列表进行修剪,值保留指定范围内的元素,闭区间所以范围边界本身也会被保存
  BLPOP | BRPOP 从第一个非空列表中探测最左端|右端的元素,或者在指定timeout之内等待可弹出的元素出现
  RPOPLPUSH 从源列表中弹出最右端的元素,然后将这个元素推入到目标列表的最左端,并向用户返回这个元素
  BRPOPLPUSH 类似的,进行timeout阻塞等待操作。这类阻塞操作通常用于消息传递、任务队列等场景

1.1.3 SET 集合

  包含字符串的无序容器,其中包含的每个字符串不允许重复。该类型支持添加、查找、获取、移除单个元素;支持计算交集、并集、差集操作;支持从集合里边随机获取元素。
  SADD 将元素添加到集合中,返回1表示添加成功,返回0表示该元素已经存在于集合中
  SREM 从集合里面移除元素,该命令返回移除元素的数量
  SISMEMBER 可以快速检查一个元素是否存在于集合当中
  SMEMBERS 获取集合中的所有元素
  SCARD 返回集合包含元素的数量
  SRANDMEMBER 从集合中随机返回一个或者多个元素,当count为正数时候返回的随机元素不重复,如果是负数则返回的元素可能会出现重复
  SPOP 随机移除集合中的一个元素,并返回移除的元素
  SMOVE 如果源集合中包含元素item,则将该元素从源集合中移除并添加到目标集合中,如果进行了该操作命令返回1,否则返回0
  SDIFF | SDIFFSTORE 返回存在于第一个集合但不存在于第二个集合中的元素(或存储到目的键中)
  SINTER | SINTERSTORE 返回同时存在于所有集合中的元素(或存储到目的键中)
  SUNION | SUNIONSTORE 返回至少存在于一个集合中的元素(或存储到目的键中)

1.1.4 HASH 散列

  包含键值对的无序散列表,散列存储的值既可以是字符串又可以是数字。该类型支持添加、移除、获取单个键值对和所有键值对,且散列存储的是数字类型的话,支持对其自增、自减等操作。
  HSET 在散列中添加关联的键值对,其返回0表示给定的键值已经存在于散列集合中
  HGET 获取指定键对应的值
  HGETALL 获取散列报中所有的键值对
  HDEL 从散列中移除散列键及其对应值
  HMGET | HMSET 从散列中获取一个或者多个键的值,或者为散列中一个或者多个键设置值。上面的操作是单参数版本,这里的HMGET、HMSET操作是多参数版本,可以一次对多个键值进行操作,这样可以通过减少命令调用次数和客户端与服务端之间的通信往返次数,是提升优化性能的一种方式。
  HLEN 返回散列包含的键值对数量
  HEXISTS 检查指定的键是否存在于散列中
  HKEYS | HVALS 返回散列表所有的的键和值
  HINCRBY 将键存储的数值加上整数值,如果指定了一个尚未存在的键值对操作,则将键的值自动当做0来处理
  HINCRBYFLOAT 将键存储的数值加上浮点数值

1.1.5 ZSET 有序集合

  这是一个字符串成员member和浮点分数score之间的有序映射,元素的排列顺序由分值大小决定,而score必须是浮点类型。该类型既支持和散列一样根据成员的方式访问,同时支持根据分数值、分数范围、分数排序的方式访问数据。有序集合的获取命令默认只返回member,如果想同时获取score,则在命令的结尾添加withscores。ZSET的排名是从0开始计数的。
  ZADD 将一个带有指定分支的成员添加到有序集合里面
  ZRANGE | ZREVRANGE 根据元素在有序集合中的位置,根据下标范围从其中获取多个元素,如果添加了withscores选项,则分值也会一并返回
  ZRANGEBYSCORE | ZREVRANGEBYSCORE 根据score的范围,返回有序集合中的多个元素
  ZREM 从有序集合中移除对应元素
  ZCARD 返回有序集合包含的成员数量
  ZINCRBY 将成员的分值加上某一个值
  ZCOUNT 返回分值介于范围之间的成员数量
  ZRANK | ZREVRANK 返回指定成员在有序集合中的排名,默认是升序排名
  ZSCORE 返回成员对应的分值
  ZREMRANGEBYRANK 移除有序集合中排名介于范围之类的所有成员
  ZREMRANGEBYSCORE 移除序列中分值介于范围之间的所有成员
  ZINTERSTORE 对给定的有序集合的键执行集合交集运算,其键的结果按照score可选择进行SUM、MIN、MAX方式进行聚合
  ZUNIONSTORE 对给定的有序集合的键执行集合并集运算,其键的结果按照score可选择进行SUM、MIN、MAX方式进行聚合

1.2 其他Redis命令

1.2.1 发布和订阅

  发布和订阅即普通的生产者和消费者关系,但是Redis的发布和订阅在客户端执行订阅的过程中如果断线,则客户端将丢失在断线期间发送的所有笑嘻嘻,这么看来Redis的消息是不可靠的,所以使用它的时候除非能够承担丢失部分数据的风险。
  SUBSCRIBE 订阅指定的一个或者多个频道
  UNSUBSCRIBE 退订给定的一个或者多个频道,如果没有指定则退订所有的频道
  PUBLISH 向给定的频道发送(二进制)信息
  PSUBSCRIBE 订阅和给定模式相匹配的所有频道
  PUNSUBSCRIBE 退订给定模式相匹配的频道,如果没有指定则退订所有的频道

1.2.2 SORT 排序命令

  这是个多参数的排序命令,类似于关系数据库的order by子句:

1
SORT source-key [BY pattern] [LIMIT offset count] [GET pattern ...] [ASC|DESC] [ALPHA] [STORE dest-key]

  通过上面的参数,可以指定升降序;指定使用数字、字符串序排等各项自定义操作。

1.2.3 EXPIRE 过期失效

  EXPIRE会给指定的数据一个生存时间,Redis会在其过期时间到达时自动删除该键,不过除了字符串外,针对列表、集合、散列和有序容器,键过期只能为整个容器设置,而没办法对其中指定的单个、部分元素设置过期时间。
  PERSIST 移除键的过期时间设置
  TTL 查看给定键距离过期还有多少秒
  EXPIRE 让给定键在指定的秒数之后过去
  EXPIREAT 将给定键的过期时间设置为给定的UNIX事件戳
  PTTLEXPIREPEXPIREAT 命令和上面的类似,只不过返回或者设置的事件是ms级的精度

二、数据安全和保障

2.1 持久化

  Redis使用小而紧凑的方式将数据持久化到硬盘上:第一种为快照持久化,可通过制定事件段内写入次数自动触发,以及转储指令触发将内存数据持久化到硬盘上;第二种方法为记录数据修改命令并追加到一个文件中去,后续通过重放这些命令就可以实现数据恢复。对数据进行持久化的主要原因是为了重用数据,或者为了防止系统故障而对数据进行备份和恢复。

2.1.1 快照持久化

  下面是一个快照持久化的常用设置命令:

1
2
3
4
save 60 1000
stop-writes-on-bgsave-error no # 在创建快照失败后是否仍然执行写命令
rdbcompression yes
dbfilename dumpFile.rdb # 位置位于dir指定的目录

  在新的快照文件创建完毕之前,Redis、系统或者硬件任意一个崩溃,那么Redis将会丢失最近一次创建快照之后写入的所有数据。触发创建快照的方法有:
  (1). 客户端可以通过向Redis显式发送BGSAVE命令来创建一个快照,此时Redis会调用fork创建一个子进程,然后子进程负责将快照写入硬盘,父进程继续处理命令请求。
  (2). 客户端还可以通过向Redis发送SAVE命令来创建一个快照,此时Redis在快照创建完成之前不再响应任何其他命令,通常只会在没有足够内存执行BGSAVE命令、不响应命令无所谓的情况下才会执行该命令。
  (3). 如果设置了save配置选项,那么Redis会在指定条件(比如上面60s内有1000次写入)满足时候,自动触发BGSAVE命令,Redis可以设置多个save条件,任何一个满足就会触发BGSAVE命令。
  (4). 如果Redis通过SHUTDOWN命令,或者收到标准的TERM信号时候,会执行一个SAVE命令,该命令完成后会关闭Redis服务器。
  (5). 当一个Redis服务器连接到另外一个Redis服务器,并向对方发送一个SYNC命令来开始一次复制操作的时候,如果主服务目前没有在执行BGSAVE操作,那么主服务会执行BGSAVE命令。
  快照持久化保存数据的时候,如果系统崩溃会丢失最后一个快照成功后的所有数据,所以只能在可以仍受这部分数据丢失的前提下才可以依赖于他。在生产环境下,通常Redis执行快照的消耗会比较大,建议的情况是关闭自动快照选项,在合适的时间点手动触发快照;虽然SAVE执行时候会阻塞所有请求,但是由于不需要创建新的进程,所以执行速度会比BGSAVE快一些。

2.1.2 AOF持久化

  下面是一个AOF持久化的常用设置命令:

1
2
3
4
5
6
appendonly yes
appendfsync everysec
no-appendffsync-on-rewrite no
auto-aof-rewrite-percentage 100 # 体积增大一倍触发重写AOF文件命令
auto-aof-rewrite-min-size 64mb
dir ./

  AOF持久化会将被执行的写命令写到AOF文件的末尾,所以Redis只需要从头到尾执行一次AOF的所有写命令,那么就可以恢复AOF文件记录的所有数据集。appendfsync可以控制AOF文件同步到磁盘的频率,always会在每个写命令时候都同步到磁盘,而no则让操作系统决定何时将数据刷新到磁盘,always对于固态磁盘还有损伤,因为每一次命令都会执行写入,而Redis无法将多个命令聚合起来进行一次写入,这杯称为写入放大,会严重缩减SSD磁盘的写入寿命。
  根据AOF的原理会知道AOF文件会无限制的增大,会给后面的保存和恢复带来负担,所以可以向Redis发送BGREWRITEAOF命令,该命令会通过移除AOF中冗余命令来重写AOF文件,从而降低AOF文件的体积。
  redis-check-aof和redis-check-dump命令可以用于检查AOF文件和快照文件的状态。如果redis-check-aof运行的时候添加fix参数,那么程序对AOF文件依次扫描检查,当发现第一个出错的命令的时候,会删除该命令及其后面所有的命令,只保留前面确信正确的命令;快照文件由于具有压缩特性,所以通常出现问题都无法进行修复。
  通过检查aof_pending_bio_fsync的属性是否为0,可以判定服务器是否将所有的数据都已经落盘了。

2.2 基本Redis事务

  Redis通过WATCHMULTIEXECUNWATCHDISCARD命令可以让用户在不被其他客户端命令打断的情况下连续执行多个操作,以达到事务的效果,但是和数据库不同的是,Redis会一个接着一个执行所有的命令直到命令执行完毕为止,因为执行的过程中客户端也没法接受反馈,所以也就没有常用关系数据库中回滚的概念。通过首先执行MULTI命令,此时Redis会将该客户端的所有命令都放到一个队列里面去,直到这个客户端发送EXEC命令为止,然后Redis会在不被打断的情况下依次执行队列中的命令。
  通过事务,底层的客户端可以使用流水线机制减少客户端和服务端的交互次数,所以可以提高命令执行的性能同时降低网络带宽和延迟,缺点是会消耗额外的资源,而且会导致其他重要命令的执行被延迟。当然,MGET、HMGET、RPUSH等这类可以接受多个参数的命令,在一个命令中传递多个执行参数也可以一定程度上增加效率。
  在关系数据库中,通常是使用SELECT FOR UPDATE对被访问的数据行进行加锁,直到事务被提交或者回滚为止,如果此时有其他客户端尝试进行写入,那么该客户端将会被阻塞起来。而Redis在执行WATCH命令的时候,不会对数据进行加锁,而是Redis会检查数据已经被其他客户端抢先修改的情况下,通知执行WATCH的客户端这个修改信息(比如有的客户端会抛出WatchError异常),被称之为乐观锁,其好处就是客户端不用等待其他取得锁的客户端,而是在被通知抢先修改的情况下,重新执行失败的事务就可以了。

2.3 主从复制

  Memcached采用的是线程池的设计模式,而Redis则可以实现数据库类似的主从复制的机制,执行复制的从服务器会连接上主服务器,接收主服务器发送的整个数据库的初始副本,之后所有在主服务器执行的写命令,都会发送给所有连接着的从服务器去执行,整个系统的读请求可以通过增加从服务器的方式来扩展(而且缓存通常都是读大于写操作,这种扩展大多会很有效)。话说虽然通常的缓存服务器的缓存性能都是足够了的,但是像Redis这样支持多种数据类型,支持多种数据计算和操作,也会有性能瓶颈的时刻。
  通过设置从服务器,主服务器的每次写入操作,从服务器都会实时的得到更新。SLAVEOF host port可以开始复制一个主服务器,而SLAVEOF no one可以终止复制操作,从服务器不再接收主服务的写入更新。

2.3.1 主从复制启动过程

  主从启动的时候,从服务器首先连接主服务器,然后发送SYNC命令;此时如果主服务未正在执行BGSAVE,则执行BGSAVE,并用缓冲区记录执行快照时候所有的写命令;从服务器根据配置决定是否继续使用现有的数据(如果有的话)来处理客户端的命令返回,还是想客户端直接返回错误;当主服务器的BGSAVE执行完毕,即向从服务器发送快照文件,并在发送期间同时执行缓冲区的写命令;从服务器会丢弃所有的旧数据(如果有的话),开始载入主服务发送的快照;主服务器快找文件发送完毕后,开始向从服务器发送存储在缓冲区的写命令,缓冲区的写命令发送完毕后,即进入正常工作状态,每一个写命令都会发送给从服务器;从服务完成快照载入后,正常开始正常同步主服务的写命令请求。
  注意Redis不支持主主同步的操作,同时从服务同步一个主服务器的时候,在其与主服务器进行初始连接时会删除数据库中素有的原有数据。还有就是当多个从服务器连接主服务器的时候,如果此时主服务正在执行BGSAVE命令,那么这些从服务器可以共用相同的快照文件和相同的缓冲区写命令。
·

2.3.2 主从复制链

  当冲服务器增多时候,其同步锁占用的带宽可能使正常的命令难以及时传递给主服务器,大量从服务器的更新也会让主服务器的负荷严重超载,这时候可以通过主从链的方式来解决:从服务器也可以有自己的从服务器,这种情况是可行而且合理的,除此之外还可以将AOF持久化的重任分布到多台从服务器上,天然形成异机备份。

三、其他

  对于C/C++的开发者来说,Redis客户端最广泛使用的非hiredis莫属了,封装一个C++用的连接池很简单,以至于我司的代码库中都有好几个封装版本了,所以应该没啥难度。除了通常的字符串格式化命令外,hiredis还支持二进制的参数,就是对应的redisCommandArgv接口。
`cpp // 注意,streamstring不允许使用.str().c_str(),前者是个临时对象,导致得到的指针悬空 std::string realKeyStr = realKey.str(); const char* argv[] = { "RPUSH", realKeyStr.c_str(), value.c_str() };
  对于非二进制但是比较复杂的数据键值(比如Json格式的数据),就可以用上面的方式封装参数列表使用。
  PS:线上这样用了一段时间,发现上面的接口出现各种问题,要么hiredis组合的时候参数不完整,要么后面释放内存的时候挂掉,现象发生十分偶然,同时也奇怪无法解决。Json数据打算用Base64编码后用传统的接口调用,这些鬼问题伤不起啊!

本文完!

参考