《Redis开发与运维》

第1章 初识Redis

1. Redis介绍:

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

  与不少键值对数据库不一样的是,Redis中的值能够是由string(字符串)、hash(哈希)、list(列表)、set(集合)、zset(有序集合等多种数据结构和算法组成,所以Redis能够知足不少的应用场景。linux

  并且由于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。

一、命令

  • 设置值: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(自减指定数字)

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

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

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

三、典型使用场景

(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

  

 一、命令:

  • 设置值: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

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

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

  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)类型是用来存储多个有序的字符串.

一、命令:

  • 从右边插入元素: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

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

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

 三、使用场景

 (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除了支持集合内的增删改查,同时还支持多个集合取交集、并集、差集,合理地使用好集合类型,能在实际开发中解决不少实际问题。

一、命令:

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

二、内部编码:

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

三、使用场景:

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

  给用户添加标签:

  

2.2.5  有序集合zset

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

一、命令:

  • 添加成员: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选项,同时会返回成员的分数)

 二、内部编码:

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

三、使用场景:

  有序集合比较典型的使用场景就是排行榜系统。例如视频网站须要对用户上传的视频作排行榜,榜单的维度多是多个方面的:按照时间、按照播放数量、按照得到的赞数。本节使用赞数这个维度,记录天天用户上传视频的排行榜。主要须要实现如下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章 复制

相关文章
相关标签/搜索