zoukankan      html  css  js  c++  java
  • 《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的标准,客户端需要将其封装为如下格式(每行用 分隔):

      

      这样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章 复制

  • 相关阅读:
    android中requestFocus
    @SuppressLint("NewApi")和@TargetApi()的区别
    Gradle基础
    Gradle build-info.xml not found for module app.Please make sure that you are using gradle plugin '2.0.0-alpha4' or higher.
    Duplicate files copied in APK META-INF/DEPENDENCIES
    解决Gradle DSL method not found: ‘android()’
    SSL peer shut down incorrectly
    如何测试远端TCP和UDP端口是否开放
    方法总比困难多
    图灵简传
  • 原文地址:https://www.cnblogs.com/toria/p/11343259.html
Copyright © 2011-2022 走看看