《Redis开发与运维》

第1章 初识Redis

1. Redis介绍:

  Redis是一种基于键值对(key-value)的NoSQL数据库

  与很多键值对数据库不同的是,Redis中的值可以是由string(字符串)、hash(哈希)、list(列表)、set(集合)、zset(有序集合等多种数据结构和算法组成,因此Redis可以满足很多的应用场景。

  而且因为Redis会将所有数据都存放在内存中,所以它的读写性能非常惊人

  不仅如此,Redis还可以将内存的数据利用快照(RDB)和日志(AOF)的形式保存到硬盘上,这样在发生类似断电或者机器故障的时候,内存中的数据不会“丢失”。

2. Redis特性:

(1)速度快。速度快的原因:

  • Redis的所有数据都是存放在内存中的,这是Redis速度快的最主要原因。
  • Redis是用C语言实现的,一般来说C语言实现的程序“距离”操作系统更近,执行速度相对会更快。
  • Redis使用了单线程架构,预防了多线程可能产生的竞争问题。

(2)基于键值对的数据结构服务器。

  与很多键值对数据库不同的是,Redis中的值不仅可以是字符串,而且还可以是具体的数据结构,它主要提供了5种数据结构:字符串、哈希、列表、集合、有序集合。这样不仅能便于在许多应用场景的开发,同时也能够提高开发效率。

(3)简单稳定

  • 首先,Redis的源码很少。
  • 其次,Redis使用单线程模型,这样不仅使得Redis服务端处理模型变得简单,而且也使得客户端开发变得简单
  • 最后,Redis不需要依赖于操作系统中的类库(例如Memcache需要依赖libevent这样的系统类库),Redis自己实现了事件处理的相关功能

(4)持久化

  通常看,将数据放在内存中是不安全的,一旦发生断电或者机器故障,重要的数据可能就会丢失,因此Redis提供了两种持久化方式:RDB和AOF,即可以用两种策略将内存的数据保存到硬盘中(如图1-1所示),这样就保证了数据的可持久性。

(5)主从复制

   Redis提供了复制功能,实现了多个相同数据的Redis副本。

(6)高可用和分布式

  Redis从2.8版本正式提供了高可用实现Redis Sentinel(哨兵模式),它能够保证Redis节点的故障发现和故障自动转移。

  Redis从3.0版本正式提供了分布式实现Redis Cluster(集群模式),它是Redis真正的分布式实现,提供了高可用、读写和容量的扩展性。

3. Redis使用场景:

  (1)缓存。合理地使用缓存不仅可以加快数据的访问速度,而且能够有效地降低后端数据源的压力。

  (2)排行榜系统。Redis提供了列表和有序集合数据结构,合理地使用这些数据结构可以很方便地构建各种排行榜系统。

  (3)计数器应用。Redis天然支持计数功能而且计数的性能也非常好。

  (4)社交网络。赞/踩、粉丝、共同好友/喜好、推送、下拉刷新等是社交网站的必备功能,由于社交网站访问量通常比较大,而且传统的关系型数据不太适合保存这种类型的数据,Redis提供的数据结构可以相对比较容易地实现这些功能。

  (5)消息队列系统。消息队列系统可以说是一个大型网站的必备基础组件,因为其具有业务解耦、非实时业务削峰等特性。Redis提供了发布订阅功能和阻塞队列的功能,虽然和专业的消息队列比还不够足够强大,但是对于一般的消息队列功能基本可以满足。

4. 在Linux系统上安装Redis

  第1步:将redis的源码包上传到linux系统。

      Alt+p打开sftp窗口:输入put "F:/java/ziyuan/redis-3.0.0.tar.gz"

  第2步:解压:tar -zxvf redis-3.0.0.tar.gz

  第3步:进行编译。 cd到解压后的目录 输入命令:make  

  第4步:进行安装。 输入命令:make install PREFIX=/usr/local/redis 

启动:redis-server (加上配置文件)      [root@localhost bin]# ./redis-server redis.conf

连接Redis服务:redis-cli       [root@localhost bin]# ./redis-cli

停止Redes服务:redis-cli shutdown       [root@localhost bin]# ./redis-cli shutdown

 第2章 API的理解和使用

2.1 预备

2.1.1 全局命令:

keys *       :将所有的键都输出

dbsize      :输出键总数

exits key  :检查某个键是否存在,如果存在返回1,不存在返回0

del key     :删除某个键

expire key 时间 :为某个键设置过期时间

ttl key       :观察某键的剩余过期时间

type key   :返回某键的数据结构类型,如果键不存在返回none

2.1.2 数据结构与内部编码:

  type命令实际返回的就是当前键的数据结构类型,它们分别是:string(字符串)、hash(哈希)、list(列表)、set(集合)、zset(有序集合),但这些只是Redis对外的数据结构。

  实际上每种数据结构都有自己底层的内部编码实现,而且是多种实现,这样Redis会在合适的场景选择合适的内部编码。

  多种内部编码实现可以在不同场景下发挥各自的优势,例如ziplist比较节省内存,但是在列表元素比较多的情况下,性能会有所下降,这时候Redis会根据配置选项将列表类型的内部实现转换为linkedlist。

2.1.3 单线程架构:

(1)单线程模型: 

  Redis使用了单线程架构I/O多路复用模型来实现高性能的内存数据库服务

  因为Redis是单线程来处理命令的,所以一条命令从客户端达到服务端不会立刻被执行,所有命令都会进入一个队列中,然后逐个被执行。所以假如有多个客户端命令,则这些命令的执行顺序是不确定的,但是可以确定不会有两条命令被同时执行。

  但是像发送命令、返回结果、命令排队肯定不像描述的这么简单,Redis使用了I/O多路复用技术来解决I/O的问题。

(2)为什么单线程号还能这么快?

为什么Redis使用单线程模型会达到每秒万级别的处理能力呢?可以将其归结为三点:

  第一,纯内存访问Redis将所有数据放在内存中,内存的响应时长大约为100纳秒,这是Redis达到每秒万级别访问的重要基础

  第二,非阻塞I/ORedis使用epoll作为I/O多路复用技术的实现,再加上Redis自身的事件处理模型将epoll中的连接、读写、关闭都转换为事件,不在网络I/O上浪费过多的时间

  第三,单线程避免了线程切换和竞态产生的消耗

2.2. 五种数据类型

2.2.1 字符串String

  字符串类型的值实际可以是字符串(简单的字符串、复杂的字符串(例如JSON、XML))、数字(整数、浮点数),甚至是二进制(图片、音频、视频),但是值最大不能超过512MB。

1、命令

  • 设置值:set key value
  • 获取值:get key
  • 批量设置值:mset key value key value ...  例如:mset  a 1 b 2 c 3
  • 批量获取值:mset key key ...  例如:mset  a b c
  • 计数:incr key(自增)、decr key(自减)、incrby key number(自增指定数字)、decrby key number(自减指定数字)

2、字符串类型的内部编码有3种

  • int:8个字节的长整型。
  • embstr:小于等于39个字节的字符串。
  • raw:大于39个字节的字符串。

  Redis会根据当前值的类型和长度决定使用哪种内部编码实现

3、典型使用场景

(1)缓存功能

  下图是比较典型的缓存使用场景,其中Redis作为缓存层,MySQL作为存储层,绝大部分请求的数据都是从Redis中获取。由于Redis具有支撑高并发的特性,所以缓存通常能起到加速读写和降低后端压力的作用。

  首先从Redis中获取用户信息(伪代码):

  

  如果没有从Redis获取到用户信息,需要从MySQL中进行获取,并将结果回写到Redis,添加1小时(3600秒)过期时间:(伪代码)

  

(2)计数

   例如使用Redis作为视频播放数计数的基础组件,用户每播放一次视频,相应的视频播放数就会自增1:

  

(3)共享sessio

  一个分布式Web服务将用户的Session信息(例如用户登录信息)保存在各自服务器中,这样会造成一个问题,出于负载均衡的考虑,分布式服务会将用户的访问均衡到不同服务器上,用户刷新一次访问可能会发现需要重新登录,这个问题是用户无法容忍的。

  为了解决这个问题,可以使用Redis将用户的Session进行集中管理,如下图所示,在这种模式下只要保证Redis是高可用和扩展性的,每次用户更新或者查询登录信息都直接从Redis中集中获取。

(4)限速

   很多应用出于安全的考虑,会在每次进行登录时,让用户输入手机验证码,从而确定是否是用户本人。但是为了短信接口不被频繁访问,会限制用户每分钟获取验证码的频率,例如一分钟不能超过5次。此功能可以使用Redis来实现,下面的伪代码给出了基本实现思路:

  

2.2.2  哈希Hash

  

 1、命令:

  • 设置值:hset key field value  例:为user:1 添加一对field-value:hset user:1 name tom
  • 获取值:hget key field  例hget user:1 name
  • 删除field:hdel key field [field ...] (可以同时删除多个)
  • 计算field的个数:hlen key
  • 批量设置或获取field-value:hmset key field value [field value ...]  hmget key field [field ...]
  • 判断field是否存在:hexits key field
  • 获取所有field:hkeys key
  • 获取所有value:hvals key
  • 获取所有field-value:hgetall key
  • field自增:hincrby
  • 计算value的字符串长度:hstrlen key field

 2、哈希类型的内部编码:

  哈希类型的内部编码有两种:

  ziplist(压缩列表):当哈希类型元素个数小于hash-max-ziplist-entries配置(默认512个)、同时所有值都小于hash-max-ziplist-value配置(默认64字节)时,Redis会使用ziplist作为哈希的内部实现,ziplist使用更加紧凑的结构实现多个元素的连续存储,所以在节省内存方面比hashtable更加优秀。

  hashtable(哈希表):当哈希类型无法满足ziplist的条件时,Redis会使用hashtable作为哈希的内部实现,因为此时ziplist的读写效率会下降,而hashtable的读写时间复杂度为O(1)。

2.2.3  列表List

   列表(list)类型是用来存储多个有序的字符串.

1、命令:

  • 从右边插入元素:rpush key value [value ...]
  • 从左边插入元素:lpush key value [value ...]
  • 向某个元素(pivot)前或者后插入元素:linsert key before | after pivot value
  • 查找指定范围内的元素列表:lrange key start end  例lrange listket 0 -1 查找全部元素
  • 获取列表指定索引下标的元素:lindex key index
  • 获取列表长度:llen key
  • 从列表左侧弹出元素:lpop key
  • 从列表左侧弹出元素:rpop key删除指定元素:lrem key count value (lrem命令会从列表中找到等于value的元素进行删除,根据count的不同分为三种情况:count>0,从左到右,删除最多count个元素;count<0,从右到左,删除最多count绝对值个元素;count=0,删除所有)
  • 修改:lset key index newvalue
  • 阻塞弹出:blpop key [key ...] timeout

2、内部编码
  列表类型的内部编码有两种:

  • ziplist(压缩列表):当列表的元素个数小于list-max-ziplist-entries配置(默认512个),同时列表中每个元素的值都小于list-max-ziplist-value配置时默认64字节),Redis会选用ziplist来作为列表的内部实现来减少内存的使用。
  • inkedlist(链表):当列表类型无法满足ziplist的条件时,Redis会使用linkedlist作为列表的内部实现。

 3、使用场景

 (1)消息队列

  Redis的lpush+brpop命令组合即可实现阻塞队列,生产者客户端使用lrpush从列表左侧插入元素,多个消费者客户端使用brpop命令阻塞式的“抢”列表尾部的元素,多个客户端保证了消费的负载均衡和高可用性。

 (2)文章列表

   每个用户有属于自己的文章列表,现需要分页展示文章列表。此时可以考虑使用列表,因为列表不但是有序的,同时支持按照索引范围获取元素。

 实际上列表的使用场景很多,在选择时可以参考以下口诀:

  • lpush+lpop=Stack(栈)
  • lpush+rpop=Queue(队列)
  • lpsh+ltrim=Capped Collection(有限集合)
  • lpush+brpop=Message Queue(消息队列)

2.2.4  集合Set

   集合(set)类型也是用来保存多个的字符串元素,但和列表类型不一样的是,集合中不允许有重复元素,并且集合中的元素是无序的,不能通过索引下标获取元素。

  一个集合最多可以存储2^32-1个元素。Redis除了支持集合内的增删改查,同时还支持多个集合取交集、并集、差集,合理地使用好集合类型,能在实际开发中解决很多实际问题。

1、命令:

  • 添加元素:sadd key element [element ...] (返回结果为添加成功的元素个数)
  • 删除元素:srem key element [element ...] (返回结果为成功删除的元素个数)
  • 计算元素个数:scard key
  • 判断元素是否在集合中:sismember key element
  • 随机从集合返回指定个数元素:srandmember key [count] (count如果不写默认为1)
  • 从集合随机弹出元素:spop key
  • 获取所有元素:smembers key

2、内部编码:

  • intset(整数集合):当集合中的元素都是整数且元素个数小于set-maxintset-entries配置(默认512个)时,Redis会选用intset来作为集合的内部实现,从而减少内存的使用。
  • hashtable(哈希表):当集合类型无法满足intset的条件时,Redis会使用hashtable作为集合的内部实现。

3、使用场景:

  集合类型比较典型的使用场景是标签(tag)。例如一个用户可能对娱乐、体育比较感兴趣,另一个用户可能对历史、新闻比较感兴趣,这些兴趣点就是标签。

  给用户添加标签:

  

2.2.5  有序集合zset

   它保留了集合不能有重复成员的特性,但不同的是,有序集合中的元素可以排序。但是它和列表使用索引下标作为排序依据不同的是,它给每个元素设置一个分数(score)作为排序的依据。

1、命令:

  • 添加成员:zadd key score member [score member ...]
  • 计算成员个数:zcard key
  • 计算某个成员的分数:zscore key member
  • 计算成员的排名:zrank key member
  • 删除成员:zrem key member [member ...]
  • 增加成员的分数:zincrby key increment member
  • 返回指定排名范围的成员:zrange key start end [withscores] (如果加上withscores选项,同时会返回成员的分数)

 2、内部编码:

  • ziplist(压缩列表):当有序集合的元素个数小于zset-max-ziplist-entries配置(默认128个),同时每个元素的值都小于zset-max-ziplist-value配置(默认64字节)时,Redis会用ziplist来作为有序集合的内部实现,ziplist可以有效减少内存的使用。
  • skiplist(跳跃表):当ziplist条件不满足时,有序集合会使用skiplist作为内部实现,因为此时ziplist的读写效率会下降。

3、使用场景:

  有序集合比较典型的使用场景就是排行榜系统。例如视频网站需要对用户上传的视频做排行榜,榜单的维度可能是多个方面的:按照时间、按照播放数量、按照获得的赞数。本节使用赞数这个维度,记录每天用户上传视频的排行榜。主要需要实现以下4个功能:

添加用户赞数:zadd和zincrby

取消用户赞数:zrem

展示获取赞数最多的十个用户:zrevrange

展示用户信息以及用户分数:zscore和zrank

 2.3 数据库管理

   Redis提供了几个面向Redis数据库的操作,它们分别是dbsize、select、flushdb/flushall命令。

(1) 切换数据库:select dbIndex

   许多关系型数据库,例如MySQL支持在一个实例下有多个数据库存在的,但是与关系型数据库用字符来区分不同数据库名不同,Redis只是用数字作为多个数据库的实现。Redis默认配置中是有16个数据库

  例:selet 15  切换到15号数据库

能不能像使用测试数据库和正式数据库一样,把正式的数据放在0号数据库,测试的数据库放在1号数据库,那么两者在数据上就不会彼此受影响了。事实真有那么好吗?

  Redis3.0中已经逐渐弱化这个功能,原因:

  1. Redis是单线程的。如果使用多个数据库,那么这些数据库仍然是使用一个CPU,彼此之间还是会受到影响的。
  2. 多数据库的使用方式,会让调试和运维不同业务的数据库变的困难,假如有一个慢查询存在,依然会影响其他数据库,这样会使得别的业务方定位问题非常的困难。
  3. 部分Redis的客户端根本就不支持这种方式。即使支持,在开发的时候来回切换数字形式的数据库,很容易弄乱。

  如果要使用多个数据库功能,完全可以在一台机器上部署多个Redis实例,彼此用端口来做区分,因为现代计算机或者服务器通常是有多个CPU的。这样既保证了业务之间不会受到影响,又合理地使用了CPU资源。

 (2)flushdb/flushall

   flushdb/flushall命令用于清除数据库,两者的区别的是flushdb只清除当前数据库,flushall会清除所有数据库

注意如果当前数据库键值数量比较多,flushdb/flushall存在阻塞Redis的可能性。

第3章 小功能大用处

3.1 慢查询分析

  许多存储系统(例如MySQL)提供慢查询日志帮助开发和运维人员定位系统存在的慢操作。所谓慢查询日志就是系统在命令执行前后计算每条命令的执行时间,当超过预设阀值,就将这条命令的相关信息(例如:发生时间,耗时,命令的详细信息)记录下来,Redis也提供了类似的功能。如图3-1所示,Redis客户端执行一条命令分为如下4个部分:

1)发送命令  2)命令排队  3)命令执行  4)返回结果

  慢查询的两个配置参数:slowlog-log-slower-thanslowlog-max-len

  • slowlog-log-slower-than是预设阀值,它的单位是微秒,默认值是10000,假如执行了一条“很慢”的命令(例如keys*),如果它的执行时间超过了10000微秒,那么它将被记录在慢查询日志中。 
  • Redis使用了一个列表来存储慢查询日志,slowlog-max-len就是列表的最大长度。一个新的命令满足慢查询条件时被插入到这个列表中,当慢查询日志列表已处于其最大长度时,最早插入的一个命令将从列表中移出

   获取慢查询日志:slow get

  获取慢查询日志列表当前的长度:slowlog len

  慢查询日志重置:slowlog reset

 3.2 Redis Shell

   Redis提供了redis-cli、redis-server、redis-benchmark等Shell工具。

启动:redis-server (加上配置文件)      [root@localhost bin]# ./redis-server redis.conf

连接Redis服务:redis-cli       [root@localhost bin]# ./redis-cli

停止Redes服务:redis-cli shutdown       [root@localhost bin]# ./redis-cli shutdown

redis-benchmark可以为Redis做基准性能测试:

  -c(clients)选项代表客户端的并发数量(默认是50)

  -n(num)选项代表客户端请求总量(默认是100000)

3.3 Pipeline

  Redis客户端执行一条命令分为如下四个过程:1)发送命令   2)命令排队   3)命令执行   4)返回结果。     其中1)+4)称为RTT(往返时间)

  Redis提供了批量操作命令(例如mget、mset等),有效地节约RTT。但大部分命令是不支持批量操作的,例如要执行n次hgetall命令,并没有mhgetall命令存在,需要消耗n次RTT。

  Pipeline(流水线)机制能将一组Redis命令进行组装,通过一次RTT传输给Redis,再将这组Redis命令的执行结果按顺序返回给客户端

3.4 事务与Lua

3.4.1 事务

  为了保证多条命令组合的原子性,Redis提供了简单的事务功能以及集成Lua脚本来解决这个问题。

  事务表示一组动作,要么全部执行,要么全部不执行。例如在社交网站上用户A关注了用户B,那么需要在用户A的关注表中加入用户B,并且在用户B的粉丝表中添加用户A,这两个行为要么全部执行,要么全部不执行,否则会出现数据不一致的情况。

  Redis提供了简单的事务功能,将一组需要一起执行的命令放到multiexec两个命令之间。multi命令代表事务开始,exec命令代表事务结束,它们之间的命令是原子顺序执行的

  Redis提供了简单的事务,之所以说它简单,主要是因为它不支持事务中的回滚特性,同时无法实现命令之间的逻辑关系计算。Lua脚本同样可以实现事务的相关功能,但是功能要强大很多。

3.4.2 Lua脚本

  Redis将Lua作为脚本语言可帮助开发者定制自己的Redis命令。Lua语言提供了如下几种数据类型:booleans(布尔)、numbers(数值)、strings(字符串)、tables(表格)。

  在Redis中执行Lua脚本有两种方法:evalevalsha

  • eval 脚本内容 key个数 key列表 参数列表

例:eval 'return "hello" .. KEYS[1] .. ARGV[1]' 1 redis word    (此时KEYS[1]="redis",ARGV[1]="world",所以最终的返回结果是"hello redisworld"。)

如果Lua脚本较长,还可以使用redis-cli--eval直接执行文件。

eval命令和--eval参数本质是一样的,客户端如果想执行Lua脚本,首先在客户端编写好Lua脚本代码,然后把脚本作为字符串发送给服务端,服务端会将执行结果返回给客户端。

  • 除了使用eval,Redis还提供了evalsha命令来执行Lua脚本。如下图所示,首先要将Lua脚本加载到Redis服务端,得到该脚本的SHA1校验和,evalsha命令使用SHA1作为参数可以直接执行对应Lua脚本,避免每次发送Lua脚本的开销。这样客户端就不需要每次执行脚本内容,而脚本也会常驻在服务端,脚本功能得到了复用。

Lua可以使用redis.call函数实现对Redis的访问,例如下面代码是Lua使用redis.call调用了Redis的get操作:

  除此之外Lua还可以使用redis.pcall函数实现对Redis的调用,redis.call和redis.pcall的不同在于,如果redis.call执行失败,那么脚本执行结束会直接返回错误,而redis.pcall会忽略错误继续执行脚本,所以在实际开发中要根据具体的应用场景进行函数的选择。

Lua脚本功能为Redis开发和运维人员带来如下三个好处:

  • Lua脚本在Redis中是原子执行的,执行过程中间不会插入其他命令。
  • Lua脚本可以帮助开发和运维人员创造出自己定制的命令,并可以将这些命令常驻在Redis内存中,实现复用的效果。
  • Lua脚本可以将多条命令一次性打包,有效地减少网络开销。

 Redis提供了4个命令实现对Lua脚本的管理:

  • script load sript:此命令用于将Lua脚本加载到Redis内存中。
  • script exists sha1 [sha1 ...]:此命令用于判断sha1是否已经加载到Redis内存中.
  • script flush:此命令用于清除Redis内存已经加载的所有Lua脚本。
  • script kill:此命令用于杀掉正在执行的Lua脚本。

3.5 Bitmaps

 

Redis提供了Bitmaps这个“数据结构”可以实现对位的操作。把数据结构加上引号主要因为:

  • Bitmaps本身不是一种数据结构,实际上它就是字符串,但是它可以对字符串的位进行操作
  • Bitmaps单独提供了一套命令,所以在Redis中使用Bitmaps和使用字符串的方法不太相同。可以把Bitmaps想象成一个以位为单位的数组,数组的每个单元只能存储0和1,数组的下标在Bitmaps中叫做偏移量

下面说下Bitmaps的命令。假设将每个独立用户是否访问过网站存放在Bitmaps中,将访问的用户记做1,没有访问的用户记做0,用偏移量作为用户的id:

(1)设置值:setbit key offset value设置键的第offset个位的值(从0算起)

  假设现在有20个用户,userid=0,5,11,15,19的用户对网站进行了访问,那么当前Bitmaps初始化结果如下图所示:

  

(2)获取值:getbit key offset 获取键的第offset位的值(从0开始算))

(3)获取Bitmaps指定范围值为1的个数:bitcount [start] [end]

(4)Bitmaps间的运算:bitop  and | or | not | xor destkey key [key ...]   (做多个Bitmaps的and(交集)、or(并集)、not(非)、xor(异或)操作并将结果保存在destkey中)

 

  假设网站有1亿用户,每天独立访问的用户有5千万,如果每天用集合类型和Bitmaps分别存储活跃用户,这种情况下使用Bitmaps能节省很多的内存空间,尤其是随着时间推移节省的内存还是非常可观的。

  

  但假如该网站每天的独立访问用户很少,例如只有10万(大量的僵尸用户),那么两者的对比如下表所示,很显然,这时候使用Bitmaps就不太合适了,因为基本上大部分位都是0。

  

3.6 发布订阅

   Redis提供了基于“发布/订阅”模式的消息机制,此种模式下,消息发布者和订阅者不进行直接通信,发布者客户端向指定的频道(channel)发布消息,订阅该频道的每个客户端都可以收到该消息

 

命令:

  • 发布消息:publish channel message ,返回结果为订阅者个数。
  • 订阅消息:subscribe channel [channel ...]  ,订阅者可以订阅一个或多个频道。
    • 注意:1)客户端在执行订阅命令之后进入了订阅状态,只能接收subscribe、psubscribe、unsubscribe、punsubscribe的四个命令。2)·新开启的订阅客户端,无法收到该频道之前的消息,因为Redis不会对发布的消息进行持久化。
  •  取消订阅:unsubscribe channel [channel ...]  
  • 查询订阅: 查看活跃的频道:pubsub channels [pattern] 、 查看频道订阅数:pubsub numsub [channel ...] 、查看模式订阅数:pubsub numpat

使用场景:

  聊天室、公告牌、服务之间利用消息解耦都可以使用发布订阅模式,下面以简单的服务解耦进行说明。如下图示,图中有两套业务,上面为视频管理系统,负责管理视频信息;下面为视频服务面向客户,用户可以通过各种客户端(手机、浏览器、接口)获取到视频信息。

第4章 客户端

   Redis是用单线程来处理多个客户端的访问,因此作为Redis的开发和运维人员需要了解Redis服务端和客户端的通信协议,以及主流编程语言的Redis客户端使用方法,同时还需要了解客户端管理的相应API以及开发运维中可能遇到的问题。本章将对这些内容进行详细分析,本章内容如下:

  • 客户端通信协议
  • Java客户端Jedis
  • 客户端管理
  • 客户端常见异常
  • 客户端案例分析

 4.1 客户端通信协议

  • 客户端与服务端之间的通信协议是在TCP协议之上构建的。
  • Redis制定了RESP(REdis Serialization Protocol,Redis序列化协议)实现客户端与服务端的正常交互,这种协议简单高效,既能够被机器解析,又容易被人类识别。

  例如客户端发送一条set hello world命令给服务端,按照RESP的标准,客户端需要将其封装为如下格式(每行用\r\n分隔):

  

  这样Redis服务端能够按照RESP将其解析为set hello world命令,执行后回复的格式如下:+OK
  Redis的返回结果类型分为以下五种:

  • 状态回复:在RESP中第一个字节为"+"。
  • 错误回复:在RESP中第一个字节为"-"。
  • 整数回复:在RESP中第一个字节为""。
  • 字符串回复:在RESP中第一个字节为"$"。
  • 多条字符串回复:在RESP中第一个字节为"*"。

4.2 Java客户端Jedis

Jedis属于Java的第三方开发包,在Java中获取第三方开发包通常有两种方式:

  • 直接下载目标版本的Jedis-${version}.jar包加入到项目中。
  • 使用集成构建工具,例如maven、gradle等将Jedis目标版本的配置加入到项目中。

通常在实际项目中使用第二种方式,但如果只是想测试一下Jedis,第一种方法也是可以的。以Maven为例子,在项目中加入下面的依赖即可:

<dependency>
    <groupId>redis.clients</groupId>
    <artifactId>jedis</artifactId>
    <version>2.8.2</version>
</dependency>

4.2.1  Jedis使用方法

//1. 生成一个Jedis对象,这个对象负责和指定Redis实例进行通信。 初始化Jedis需要两个参数:Redis实例的IP和端口
Jedis jedis = new Jedis("127.0.0.1", 6379);
//2. jedis执行set操作
jedis.set("hello", "world");
//3. jedis执行get操作, value="world"
String value = jedis.get("hello");

Jedis对于Redis五种数据结构的操作:

//-----------1.string------------
// 输出结果:OK
jedis.set("hello", "world");
// 输出结果:world
jedis.get("hello");
// 输出结果:1
jedis.incr("counter");
//-----------2.hash---------------
jedis.hset("myhash", "f1", "v1");
jedis.hset("myhash", "f2", "v2");
// 输出结果:{f1=v1, f2=v2}
jedis.hgetAll("myhash");
//-----------3.list---------------
jedis.rpush("mylist", "1");
jedis.rpush("mylist", "2");
jedis.rpush("mylist", "3");
// 输出结果:[1, 2, 3]
jedis.lrange("mylist", 0, -1);
//-----------4.set----------------
jedis.sadd("myset", "a");
jedis.sadd("myset", "b");
jedis.sadd("myset", "a");
// 输出结果:[b, a]
jedis.smembers("myset");
//------------5.zset----------------
jedis.zadd("myzset", 99, "tom");
jedis.zadd("myzset", 66, "peter");
jedis.zadd("myzset", 33, "james");
// 输出结果:[[["james"],33.0], [["peter"],66.0], [["tom"],99.0]]
jedis.zrangeWithScores("myzset", 0, -1);

4.2.2  Jedis连接池的使用方法

  • 前面介绍的是Jedis的直连方式,所谓直连是指Jedis每次都会新建TCP连接,使用后再断开连接,对于频繁访问Redis的场景显然不是高效的使用方式。
  • 因此生产环境中一般使用连接池的方式对Jedis连接进行管理。所有Jedis对象预先放在池子中(JedisPool),每次要连接Redis,只需要在池子中借,用完了在归还给池子。

  客户端连接Redis使用的是TCP协议,直连的方式每次需要建立TCP连接,而连接池的方式是可以预先初始化好Jedis连接,所以每次只需要从Jedis连接池借用即可,而借用和归还操作是在本地进行的,只有少量的并发同步开销,远远小于新建TCP连接的开销。另外直连的方式无法限制Jedis对象的个数,在极端情况下可能会造成连接泄露,而连接池的形式可以有效的保护和控制资源的使用。下表给出两种方式各自的优劣势。

 

  Jedis提供了JedisPool这个类作为对Jedis的连接池。使用JedisPool操作Redis的代码示例:

 (1)Jedis连接池(通常JedisPool是单例的):

// common-pool连接池配置,这里使用默认配置
GenericObjectPoolConfig poolConfig = new GenericObjectPoolConfig();
// 初始化Jedis连接池
JedisPool jedisPool = new JedisPool(poolConfig, "127.0.0.1", 6379);

(2)获取Jedis对象不再是直接生成一个Jedis对象进行直连,而是从连接池直接获取,代码如下:

Jedis jedis = null;
try {
    // 1. 从连接池获取jedis对象
    jedis = jedisPool.getResource();
// 2. 执行操作
    jedis.get("hello");
} catch (Exception e) {
    logger.error(e.getMessage(),e);
} finally {
    if (jedis != null) {
        // 如果使用JedisPool,close操作不是关闭连接,代表归还连接池
        jedis.close();
    }
}

4.2.3 Redis中Pipeline的使用方法

回顾:Pipeline(流水线)机制能将一组Redis命令进行组装,通过一次RTT传输给Redis,再将这组Redis命令的执行结果按顺序返回给客户端

  Jedis支持Pipeline特性,我们知道Redis提供了mget、mset方法,但是并没有提供mdel方法,如果想实现这个功能,可以借助Pipeline来模拟批量删除:

public void mdel(List<String> keys) {
    Jedis jedis = new Jedis("127.0.0.1");
    // 1)生成pipeline对象
    Pipeline pipeline = jedis.pipelined();
// 2)pipeline执行命令,注意此时命令并未真正执行
    for (String key : keys) {
        pipeline.del(key);
    }
    // 3)执行命令
    pipeline.sync();
}

4.2.4 Jedis的Lua脚本

  Jedis中执行Lua脚本和redis-cli十分类似,Jedis提供了三个重要的函数实现Lua脚本的执行:

Object eval(String script, int keyCount, String... params)
Object evalsha(String sha1, int keyCount, String... params)
String scriptLoad(String script)

以一个最简单的Lua脚本为例子进行说明: return redis.call('get',KEYS[1])

在redis-cli中执行上面的Lua脚本,方法如下:

  eval "return redis.call('get',KEYS[1])" 1 hello

 在Jedis中执行,方法如下:

String key = "hello";
String script = "return redis.call('get',KEYS[1])";
Object result = jedis.eval(script, 1, key);
System.out.println(result);

 

scriptLoad和evalsha函数要一起使用,首先使用scriptLoad将脚本加载到Redis中,代码如下:

String scriptSha = jedis.scriptLoad(script);

然后执行结果如下:

Stirng key = "hello";
Object result = jedis.evalsha(scriptSha, 1, key);
System.out.println(result);

4.3 客户端管理

  client list命令能列出与Redis服务端相连的所有客户端连接信息。

  Redis为每个客户端分配了输入缓冲区,它的作用是将客户端发送的命令临时保存,同时Redis从会输入缓冲区拉取命令并执行,输入缓冲区为客户端发送命令到Redis执行命令提供了缓冲功能,如下图所示。

  输入缓冲使用不当会产生两个问题:

  • 一旦某个客户端的输入缓冲区超过1G,客户端将会被关闭。
  • 输入缓冲区不受maxmemory控制,假设一个Redis实例设置了maxmemory为4G,已经存储了2G数据,但是如果此时输入缓冲区使用了3G,已经超过maxmemory限制,可能会产生数据丢失、键值淘汰、OOM等情况。

  Redis为每个客户端分配了输出缓冲区,它的作用是保存命令执行的结果返回给客户端,为Redis和客户端交互返回结果提供缓冲。与输入缓冲区不同的是,输出缓冲区的容量可以通过参数client-output-buffer-limit来进行设置,并且输出缓冲区做得更加细致,按照客户端的不同分为三种:普通客户端、发布订阅客户端、slave客户端,如下图所示。

 

  和输入缓冲区相同的是,输出缓冲区也不会受到maxmemory的限制,如果使用不当同样会造成maxmemory用满产生的数据丢失、键值淘汰、OOM等情况。

 第5章 持久化

   Redis支持RDBAOF两种持久化机制,持久化功能有效地避免因进程退出造成的数据丢失问题,当下次重启时利用之前持久化的文件即可实现数据恢复

 5.1 RDB(快照方式)

   RDB持久化是把当前进程数据生成快照保存到硬盘的过程。触发RDB持久化过程分为手动触发和自动触发:

(1)手动触发分别对应save和bgsave命令:

  • save命令:阻塞当前Redis服务器,直到RDB过程完成为止,对于内存比较大的实例会造成长时间阻塞,线上环境不建议使用。
  • bgsave命令:Redis进程执行fork操作创建子进程,RDB持久化过程由子进程负责,完成后自动结束。阻塞只发生在fork阶段,一般时间很短。

  显然bgsave命令是针对save阻塞问题做的优化。因此Redis内部所有的涉及RDB的操作都采用bgsave的方式,而save命令已经废弃。

  bgsave命令的运作过程:

  1. 执行bgsave命令,Redis父进程判断当前是否存在正在执行的子进程,如果存在bgsave命令直接返回。
  2. 父进程fork完成后,bgsave命令返回“Background saving started”信息并不再阻塞父进程,可以继续响应其他命令。
  3. 子进程创建RDB文件,根据父进程内存生成临时快照文件,完成后对原有文件进行原子替换。
  4. 进程发送信号给父进程表示完成,父进程更新统计信息。

(2)自动触发:

  • 使用save相关配置,如“save m n”。表示m秒内数据集存在n次修改时,自动触发bgsave。
  • 如果从节点执行全量复制操作,主节点自动执行bgsave生成RDB文件并发送给从节点。
  • 执行debug reload命令重新加载Redis时,也会自动触发save操作。
  • 默认情况下执行shutdown命令时,如果没有开启AOF持久化功能则自动执行bgsave。

 

RDB的优点:

  • RDB是一个紧凑压缩的二进制文件,代表Redis在某个时间点上的数据快照。非常适用于备份,全量复制等场景。比如每6小时执行bgsave备份,并把RDB文件拷贝到远程机器或者文件系统中(如hdfs),用于灾难恢复。
  • Redis加载RDB恢复数据远远快于AOF的方式

RDB的缺点:

  • RDB方式数据没办法做到实时持久化/秒级持久化。因为bgsave每次运行都要执行fork操作创建子进程,属于重量级操作,频繁执行成本过高。
  • RDB文件使用特定二进制格式保存,Redis版本演进过程中有多个格式的RDB版本,存在老版本Redis服务无法兼容新版RDB格式的问题。

 5.2 AOF(日志方式)

   AOF(append only file)持久化:以独立日志的方式记录每次写命令,重启时再重新执行AOF文件中的命令达到恢复数据的目的AOF的主要作用是解决了数据持久化的实时性,目前已经是Redis持久化的主流方式。

   AOF默认是默认不开启的,开启AOF功能需要设置配置:appendonly yes。

   AOF工作流程:

    

  1. 所有的写入命令会追加到aof_buf(缓冲区)中。
  2. AOF缓冲区根据对应的策略向硬盘做同步操作。
  3. 随着AOF文件越来越大,需要定期对AOF文件进行重写,达到压缩的目的。
  4. 当Redis服务器重启时,可以加载AOF文件进行数据恢复。

 注:

1. AOF为什么把命令追加到aof_buf中?

  Redis使用单线程响应命令,如果每次写AOF文件命令都直接追加到硬盘,那么性能完全取决于当前硬盘负载。先写入缓冲区aof_buf中,还有另一个好处,Redis可以提供多种缓冲区同步硬盘的策略,在性能和安全性方面做出平衡。

 2. AOF缓冲区同步文件策略,由参数appendfsync控制:

appendfsync always    #每次有数据修改发生时都会写入AOF文件,这样会严重降低Redis的速度
appendfsync everysec  #每秒钟同步一次,显示地将多个写命令同步到硬盘
appendfsync no        #让操作系统决定何时进行同步

3. AOF文件重写是把Redis进程内的数据转化为写命令同步到新AOF文件的过程。重写后的AOF文件为什么可以变小?

1)进程内已经超时的数据不再写入文件。
2)旧的AOF文件含有无效命令,重写使用进程内数据直接生成,这样新的AOF文件只保留最终数据的写入命令。
3)多条写命令可以合并为一个,如:lpush list a、lpush list b、lpush list c可以转化为:lpush list a b c。为了防止单条命令过大造成客户端缓冲区溢出,对于list、set、hash、zset等类型操作,以64个元素为界拆分为多条。

AOF重写降低了文件占用空间,除此之外,另一个目的是:更小的AOF文件可以更快地被Redis加载。

 

【注】如果同时配了RDB和AOF,优先加载AOF。

第6章 复制

 

posted on 2019-08-17 16:55  Toria  阅读(4253)  评论(3编辑  收藏  举报

导航