zoukankan      html  css  js  c++  java
  • Redis的数据类型以及应用场景

    1. Redis的作用

    1.1 Redis可以做什么

      1.缓存:缓存机制几乎在所有的大型网站都有使用,合理地使用缓存不仅可以加快数据的访问速度,而且能够有效地降低后端数据源的压力。Redis提供了键值过期时间设置,并且也提供了灵活控制最大内存和内存溢出后的淘汰策略。可以这么说,一个合理的缓存设计能够为一个网站的稳定保驾护航。

      2.排行榜系统:排行榜系统几乎存在于所有的网站,例如按照热度排名的排行榜,按照发布时间的排行榜,按照各种复杂维度计算出的排行榜,Redis提供了列表和有序集合数据结构,合理地使用这些数据结构可以很方便地构建各种排行榜系统。

      3.计数器应用:计数器在网站中的作用至关重要,例如视频网站有播放数、电商网站有浏览数,为了保证数据的实时性,每一次播放和浏览都要做加1的操作,如果并发量很大对于传统关系型数据的性能是一种挑战Redis天然支持计数功能而且计数的性能也非常好,可以说是计数器系统的重要选择。

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

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

     

    1.2 Redis不可以做什么

      实际上和任何一门技术一样,每个技术都有自己的应用场景和边界,也就是说Redis并不是万金油,有很多适合它解决的问题,但是也有很多不合适它解决的问题 我们可以站在数据规模和数据冷热的角度来进行分析。站在数据规模的角度看,数据可以分为大规模数据和小规模数据,我们知道Redis的数据是存放在内存中的,虽然现在内存已经足够便宜,但是如果数据量非常大,例如每天有几亿的用户行为数据,使用Redis来存储的话,基本上是个无底洞,经济成本相当的高。

      站在数据冷热的角度看,数据分为热数据和冷数据,热数据通常是指需要频繁操作的数据,反之为冷数据,例如对于视频网站来说,视频基本信息基本上在各个业务线都是经常要操作的数据,而用户的观看记录不一定是经常需要访问的数据,这里暂且不讨论两者数据规模的差异,单纯站在数据冷热的角度上看,视频信息属于热数据,用户观看记录属于冷数据。 如果将这些冷数据放在Redis中,基本上是对于内存的一种浪费,但是对于一再热数据可以放在Redis中加速读写,也可以减轻后端存储的负载,可以说是事半功倍。

      所以,Redis并不是万金油,相信随着我们对Redis的逐步学习,能够清楚Redis真正的使用场景。

     

    1.3 用好Redis的建议

      切勿当作黑盒使用,开发与运维同样重要。很多使用Redis的开发者认为只要会用API开发相应的功能就可以,更有甚者认为Redis就是get、set、del,不需要知道Redis的原理:但是在我们实际运维和使用Redis的过程中发现,很多线上的故障和问题都是由于完全把Redis当做黑盒造成的,如果不了解Redis的单线程模型,有些开发者会在有上千万个键的Redis上执行keys *操作,如果不了解持久化的相关原理,会在一个写操作量很大的Redis上配置自动保存RDB而且在很多公司内只有专职的关系型数据库DBA,并没有NoSQL的相关运维人员,也就是说开发者很有可能会自己运维Redis,对于Redis的开发者来说既是好事又是坏事:站在好的方面看,开发人员可以通过运维Redis真正了解Redis的一些原理,不单纯停留在开发上。站在坏的方面看,Redis的开发人员不仅要支持开发,还要承担运维的责任,而且由于运维经验不足可能会造成线上故障。但是从实际经验来看,运维足够规模的Redis会对用好Redis更加有帮助。

     

    2.Redis数据结构和内部编码

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

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

      可以看到每种数据结构都有两种以上的内部编码实现,例如list数据结构包含了linkedlist和ziplist两种内部编码。同时有些内部编码,例如ziplist,可以作为多种外部数据结构的内部实现,可以通过object encoding命令查询内部编码:

    127.0.0.1:6379> object encoding hello
     "embstr"
    127.0.0.1:6379> object encoding mylist
     "ziplist"

      可以看到键hello对应值的内部编码是embstr,键mylist对应值的内部编码是Ziplist.

      Redis这样设计有两个好处:第一,可以改进内部编码,而对外的数据结构和命令没有影响,这样一旦开发出更优秀的内部编码,无需改动外部数据结构和命令,例如Redis 3.2提供了quicklist,结合了 ziplist和linkedlist两者的优势,为列表类型提供了一种为优秀的内部编码实现,而对外部用户来说基本感知不到。第二,多种内部编码实现可以在不同场景下发挥各自的优势,例如ziplist比较节省内存,但是在列表元素比较多的情况下,性能会有所下降,这时候Redis会根据配置选项将列表类型的内部实现转换为Linkedlist。

      接下来会讲解每个字符串类型的应用场景以及详细的字符编码。

     

    2.1 字符串

      字符串类型是Redis最基础的数据结构。首先键都是字符串类型,而且其他几种数据结构都是在字符串类型基础上构建的,所以字符串类型能为其他四种数据结构的学习奠定基础。如图2.7所示,字符串类型的值实际可以是字符串(简单的字符串、复杂的字符串(例如JSON、XML)、数字(整数、浮点数),甚至是二进制(图片、音频、视频),但是值最大不能超过512MB。

      

    2.1.1 命令

      字符串类型的命令比较多,本小节将按照常用和不常用两个维度进行说明,但是这里常用和不常用是相对的,尽可能都去了解和掌握。

    1.常用命令

    (1) 设置值

    127.0.0.1:6379> object encoding hello
     "embstr"
    127.0.0.1:6379> object encoding mylist
     "ziplist"

    下面操作设置键为hello,值为world的键值对,返回结果为OK代表设置成功:

    127.0.0.1:6379> set hello world
    OK

    set命令有几个选项:

    □ ex seconds:为键设置秒级过期时间。

    □ px milliseconds:为键设置毫秒级过期时间。

    □ nx:键必须不存在,才可以设置成功,用于添加。

    □ xx:与nx相反,键必须存在,才可以设置成功,用于更新。

    除了set选项,Redis还提供了setex和setnx两个命令

    setex key seconds value
    setnx key value

    它们的作用和ex和nx选项是一样的。下面的例子说明了set、setnx、setxx的区别。

    当前键hello不存在:

    127.0.0.1:6379> exists hello
    (integer) 0  

    设置键为hello, 值为world的键值对:

    127.0.0.1:6379> set hello world
    OK 

    因为键hello已存在,所以setnx失败,返回结果为0:

    127.0.0.1:6379> setnx hello redis
    (integer) 0

      因为键hello已存在,所以set xx成功,返回结果为OK:

    127.0.0.1:6379> set hello jedis xx
    OK

    setnx和setxx在实际使用中有什么应用场景吗?以setnx命令为例子,由于Redis的单线程命令处理机制,如果有多个客户端同时执行 setnx key value,根据setnx的特性只有一个客户端能设置成功,setnx可以作为分布式锁的一种实现方案,Redis官方给出了使用 setnx 实现分布式锁的方法:http://redis.io/topics/distlock。

    (2) 获取值

     get key

    下面操作获取键hello的值:

    127.0.0.1:6379> get hello
    "world"

    如果要获取的键不存在,则返回nil(空):

    127.0.0.1:6379> get not_exist_key
    (nil)

    (3) 批量设置值

    mset key value [key value ...]

    下面操作通过mset命令一次性设置4个键值对:

    127.0.0.1:6379> mset a1 b2 c3 d4
    OK

    (4) 批量获取值

    mget key [key … ]

    下面操作批量获取了键a、b、c 、d 的值:

    127.0.0.1:6379> mget a b e d
    1 )  "1"
    2 )  "2"
    3 )  "3"
    4 )  "4"

    如果有些键不存在,那么它的值为nil(空),结果是按照传人键的顺序返回:

    127.0.0.1:6379> mget a b c f
    1 )  " 1 "
    2 )  " 2 "
    3 )   "3"
    4 )  (nil)

    批量操作命令可以有效提高开发效率,假如没有mget这样的命令,要执行0次get命令需要按照图2-8的方式来执行,具体耗时如下:

    N次get时间= N次网络时间+ N次命令时间

    使用mget命令后,要执行n次get命令操作只需要按照图2-9的方式来完成,具体耗时如下:

    n次get时间 = 1次网络时间+ n次命令时间

    Redis可以支撑每秒数万的读写操作,但是这指的是Redis服务端的处理能力,对于客户端来说,一次命令除了命令时间还是有网络时间,假设网络时间为1毫秒,命令时间为0.1毫秒(按照每秒处理1万条命令算),那么执行1000次 get命令和1次mget命令的区别如表2-1,因为Redis的处理能力已经足够高,对于开发人员来说,网络可能会成为性能的瓶颈。

    表2-1 1000次get和1次get对比表

    操  作

    时  间

    1000次 get

    1000 x 1 + 1000 x 0.1 = 1100 毫秒= 1.1 秒

    1次met(组装了 1000个键值对)

    1  x 1 + 1000  x 0.1 = 101 毫秒= 0.101 秒

    学会使用批量操作,有助于提高业务处理效率,但是要注意的是每次批量操作所发送的命令数不是无节制的,如果数量过多可能造成Redis阻塞或者网络拥塞。

    (5) 计数   

    incr key

    incr命令用于对值做自增操作,返回结果分为三种情况:

    □ 值不是整数,返回错误。

    □ 值是整数,返回自增后的结果。

    □ 键不存在,按照值为〇自增,返回结果为1。

    例如对一个不存在的键执行incr操作后,返回结果是1:

    127.0.0.1:6379> exists key
    (integer) 0
    127.0.0.1:6379> incr key
    (integer) 1

    再次对键执行incr命令,返回结果是2:

    127.0.0.1:6379> incr key
    (integer) 2

    如果值不是整数,那么会返回错误:

    127.0.0.1:6379>set hello world
    OK
    127.0.0.1:6379> incr hello
    (error) ERR value is not an integer or out of range

    除了incr命令,Redis提供了deer(自减)、incrby(自增指定数字)、decrby(自减指定数字)、incrbyfloat (自增浮点数):

    deer key
    inerby key increment
    deerby key decrement
    incrby float key increment

    很多存储系统和编程语言内部使用CAS机制实现计数功能,会有一定的CPU开销,但在Redis中完全不存在这个问题,因为Redis是单线程架构,任何命令到了Redis服务端都要顺序执行

     

    2.不常用命令

    (1) 追加值

    append key value

    append可以向字符串尾部追加值,例如:

    127.0.0.1:6379> get key
    "redis "
    127.0.0.1:6379> append key world
    ( integer) 10
    127.0.0.1:6379> get key
    " redisworld "

    (2) 字符串长

    strlen key

    例如,当前值为redisworld, 所以返回值为10:

    127.0.0.1:6379> get key
    "redisworld "
    127.0.0.1:6379> strlen key
    ( integer) 10

    下面操作返回结果为6 , 因为每个中文占用3 个字节:

    127.0.0.1:6379> set hello "世界 "
    OK
    127.0.0.1:6379> strlen hello
    ( integer) 6

    (3) 设置并返回原值

    getset key value

    getset和set—样会设置值,但是不同的是,它同时会返回键原来的值,例如:

    127.0.0.1:6379> getset hello world
    (nil)
    127.0.0.1:6379> getset hello redis
    "world"

    (4) 设置指定位置的字符

    setrange key offeset value

    下面操作将值由pest变为了best:

    127.0.0.1:6379> set redis pest
    OK
    127.0.0.1:6379> setrange redis 0 b
    (integer) 4
    127.0.0.1:6379> get redis
    "best"

    (5) 获取部分字符串

    getrange key start end

    start和end分别是开始和结束的偏移量,偏移量从0 开始计算,例如下面操作获取了值best的前两个字符。

    127.0.0.1:6379> getrange redis 0 1
    "be"

    表2-2是字符串类型命令的时间复杂度,开发人员可以参考此表,结合自身业务需求和数据大小选择适合的命令。

    表 2-2 字符串类型命令时间复杂度
    命令 时间复杂度
    set key value O(1)
    get key O(1)
    del key [key ...] O(k):k是键的个数
    mset key value [key value ... ] O(k):k是键的个数
    mget key [key ... ] O(k):k是键的个数
    incr key O(1)
    deer key O(1)
    inerby key increment O(1)
    deerby key decrement O(1)
    incrbyfloat key increment O(1)
    append key value O(1)
    strlen key O(1)
    setrange key offset value O(1)
    getrange key start end O(n),n是字符串长度,由于获取字符串非常快,所以如果字符串不是很长,可以视同为O(1)

     

    2.1.2 字符串的内部编码

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

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

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

    整数类型示例如下:

     127.0.0.1:6379> set key 8653
      OK
     127.0.0.1:6379> object encoding key
     "int"

    短字符串示例如下:

    #小于等于39个字节的字符串:embstr
    127.0.0.1:6379> set key "hello,world"
    OK
    127.0.0.1:6379> object encoding key
    "embstr"

    长字符串示例如下:

     #大于39个字节的字符串:raw
    127.0.0.1:6379> set key "one string greater than 39 byte......."
     OK
    127.0.0.1:6379> object encoding key
    127.0.0.1:6379> strlen key
    (integer) 40

    有关字符串类型的内存优化技巧将在文章8.3理解内存介绍。

     

    2.1.3 使用场景

    1.缓存功能

      图2-10是比较典型的缓存使用场景,其中Redis作为缓存层,MySQL作为存储层.绝大部分请求的数据都是从Redis中获取。由于Redis具有支持高并发的特性,所以缓存通常能起到加速读写和降低后端压力的作用。下面伪代码模拟了图2-10的访问过程:

    1)该函数用于获取用户的基础信息:

    Userinfo getUserInfo(lony id){ 
    ...
    }

    2)首先从Redis获取用户信息

     //定义键
    userRedisKey = "user:info:" + id;
     //从Redis获取值
     value = redis.get(userRedisKey);
     if (value != null) {
           //将值进行反序列化为Userlnfo并返回结果
           userInfo = deserialize(value);
           return userInfo;
     }

      小提示: 与MySQL等关系型数据库不同的是,Redis没有命令空间,而且也没有对键名有强制要求(除了不能使用一些特殊字符)。但设计合理的键名,有利于防止键冲突和项目的可维护性,比较推荐的方式是使用“业务名:对象名:id:[属性]”作为键名(也可以不是分号)。例如MySQL的数据库名为vs,用户表名为user,那么对应的键可以用"vs:user:1","vs:user:1:name"来表示,如果当前Redis只被一个业务使用,甚至可以去掉“vs:"。如果键名比较长,例如“user:{uid): friends:messages :{mid}”,可以在能描述键含义的前提下适当减少键的长度,例如变为"u:{uid}:fr:m:{mid}“,从而减少由于键过长的内存浪费。

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

    //从MySQL获取用户信息
    userinfo = mysql.get(id);
    //将userInfo 序列化,并存入Redis
    redis.setex(userRedisKey, 3600, serialize(userInfo));
    //返回结果
    return userInfo
     
    整个功能的伪代码如下:
     
    Userinfo getUserlnfo(long id){
           userRedisKey = "user:info:" + id
           value = redis.get(userRedisKey);
           Userinfo userinfo;
           if (value != null) {
                 userInfo = deserialize(value);
           } else {
                 userinfo = mysql.get(id);
                 if (userinfo != null)
                      redis.setex(userRedisKey, 3600, serialize(userInfo));
           }
          return userInfo;
    )

      2.计数

      许多应用都会使用Redis作为计数的基础工具,它可以实现快速计数、查询缓存的功能。同时数据可以异步落地到其他数据源。例如视频播放数系统就是使用Redis作为视频播放数计数的基础组件,用户每播放一次视频,相应的视频播放数就会自增1:

    long incrVideoCouncer(long id)(
          key = "video:playCount:" + id;
          return redis.incr(key);
    }

     

      3.共享Session

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

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

     

      4.限速

      很多应用出于安全的考虑,会在每次进行登录时,让用户输入手机验证码,从而确定是否是用户本人。但是为了短信接口不被频繁访问,会限制用户每分钟获取验证码的频率,例如一分钟不能超过5次,如图2-13所示。

      此功能可以使用Redis来实现,下面的伪代码给出了基本实现思路:

     phoneNum = "138xxxxxxxx';
     key = 'shortMsg:limit:" + phoneNum;
     //SET key value EX 60 NX
     isExists = redis.set(key,1,"EX 60","NX");
     if(isExists != null I I redis.incr(key) <=5)(
     //通过
          }else(
            //限速
     }

      上述就是利用Redis实现了限速功能,例如一些网站限制一个IP地址不能在一秒钟之内访问超过n次也可以采用类似的思路。

      除了上面介绍的几种使用场景,字符串还有非常多的适用场景,开发人员可以结合字符串提供的相应命令充分发挥自己的想象力。

     

    2.2 哈希

      几乎所有的编程语言都提供了哈希(hash)类型,它们的叫法可能是哈希、字典、关联数组。在Redis中,哈希类型是指键值本身又是一个键值对结构,形如valuer {fieldl,valuel), ... {fieldN, valueN}}, Redis键值对和哈希类型二者的关系可以用图2-14来表示。

     

    2.2.1 命令

    (1) 设置值

    hset key field value

    下面为user:1添加一对 field-value:

    127.0.0.1:6379> hset user:1 name tom
    (integer) 1

    如果设置成功会返回1,反之会返回0。此外Redis提供了 hsetnx命令,它们的关系就像set 和 setnx 命令一样,只不过作用域由键变为field。

    (2) 获取值

    hget key field

    例如,下面操作获取user:l 的 name域 (属性)对应的值:

    127.0.0.1:6379> hget user:1 name
    "tom"

    如果键或field不存在,会返回nil:

    127.0.0.1:6379> hget user:2 name
    (nil)
    127.0.0.1:6379> hget user:1 age
    (nil)

    (3) 删除 field

    hdel key field [ field … ]

    hdel会删除一个或多个field , 返回结果为成功删除field 的个数,例如:

    127.0.0.1:6379> hdel user:1 name
    ( integer) 1
    127.0.0.1:6379> hdel user:1 age
    (integer) 0

    (4) 计算field 个数

    hlen key

    例如 user:1 有 3个field:

    127.0.0.1:6379> hset user:1 name tom
    (integer) 1
    127.0.0.1:6379> hset user:1 age 23
    (integer)1
    127.0.0.1:6379> hset user:1 city tianjin
    (integer) 1
    127.0.0.1 :6379> hlen user:1
    ( integer) 3

    (5) 批量设置或获取field-value

    hmget key field [ field … ]
    hmset key field value [ field value … ]

    hmset和hmget分别是批量设置和获取field-value, hmset需要的参数是key和多对field-value, hmget需要的参数是key和多个field。例如:

    127.0.0.1:6379> hmset user:1 name mike age 12 city tianjin
    OK
    127.0.0.1:6379> hmget user:1 name city
    1) "mike"
    2) "tianjin "

    (6) 判断field是否存在

    hexists key field

    例如,user:1包含name域,所以返回结果为1,不包含时返回0:

    127.0.0.1:6379> hexists user:1 name
    (integer) 1

    (7) 获取所有field

    hkeys key

    hkeys命令应该叫hfields更为恰当,它返回指定哈希键所有的field , 例如:

    127.0.0.1:6379> hkeys user:1
    1)  "name"
    2)  "age"
    3) " city"

    (8) 获取所有value

    hvals key

    下面操作获取user:1 全部value:

    127.0.0.1:6379 > hvals user:1
    1)  "mike"
    2)  "12 "
    3)  "tian jin "

    (9) 获取所有的field-value

    hgetall key

    下面操作获取user:1 所有的field-value:

    127.0.0.1:6379> hgetall user:1
    1)  "name"
    2)  "mike"
    3)  "age"
    4)  "12"
    5)  "city "
    6)  "tian jin"

    在使用hgetall时,如果哈希元素个数比较多,会存在阻塞Redis的可能。如果开发人员只需要获取部分field , 可以使用hmget, 如果一定要获取全部field-value, 可以使用hscan命令,该命令会渐进式遍历哈希类型

    (10) hincrby hincrbyfloat

    hincrby key field
    hincrby float key field

    hincrby和 hincrbyfloat, 就像incrby和 incrbyfloat命令一样,但是它们的作用域是filed。

    (11) 计算value 的字符串长度(需要Redis 3.2以上)

    hstrlen key field

    例如hget user:1 name的 value是 tom, 那么hstrlen 的返回结果是3:

    127.0.0.1:6379> hstrlen user:1 name
    ( integer) 3
    表 2-3 哈希类型命令的时间复杂度
    命令 时间复杂度
    hset key field value O(1)
    hget key field O(1)
    hdel key field [field ... ] O(k),k是field个数
    hlen key O(1)
    hgetall key O(n),n是field个数
    hmget field [field ... ] O(k),k是field个数
    hmset field value [field value .. ] O(k),k是field个数
    hexists key field O(1)
    hkeys key O(n),n是field个数
    hvals key O(n),n是field个数
    hsetnx key field value O(1)
    hincrby key field increment O(1)
    hincrbyfloat key field increment O(1)
    hstrlen key field O(1)

    2.2.2 哈希的内部编码

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

      • ziplist (压缩列表):当哈希类型元素个数小于hash-max-ziplist-entries配置(默认512个)、同时所有值都小于hash-max-ziplist -value 配置(默认64字节)时,Redis会使用ziplist 作为哈希的内部实现,ziplist 使用更加紧凑的结构实现多个元素的连续存储,所以在节省内存方面比hashtable更加优秀。
      • hashtable(哈希表):当哈希类型无法满足ziplist 的条件时,Redis会使用hashtable作为哈希的内部实现,因为此时ziplist 的读写效率会下降,而hashtable的读写时间复杂度为0(1)。

    下面的示例演示了哈希类型的内部编码,以及相应的变化。

    1)当field个数比较少且没有大的value时,内部编码为ziplist:

    127.0.0.1:6379> hmset hashkey f1 v1 f2 v2
    OK
    127.0.0.1:6379> object encoding hashkey
    "ziplist"

    2.1)当有value大于64字节,内部编码会由ziplist变为hashtable:

    127.0.0.1:6379> hset hashkey f3 "one string is bigger than 64 byte  ..忽 略 .."
    OK
    127.0.0.1:6379> object encoding hashkey
    "hashtable"

    2.2)当field个数超过512,内部编码也会由ziplist 变为hashtable:

    127.0.0.1:6379 > hmset hashkey f1 v1 f2 v2 f3 v3 . . . 忽 略 . . . f513 v513
    OK
    127.0.0.1:6379 > object encoding hashkey
    "hashtable"

     

    2.2.3 使用场景

    图2-15为关系型数据表记录的两条用户信息,用户的属性作为表的列,每条用户信息作为行。

    如果将其用哈希类型存储,如图2-16所示。

    相比于使用字符串序列化缓存用户信息,哈希类型变得更加直观,并且在更新操作上会更加便捷。可以将每个用户的id定义为键后缀,多对field-value对应每个用户的属性,类似如下伪代码:

    Userlnfo getUserlnfo (longid ) {
           //用户id作为key后缀
           userRedisKey = "user:info:" + id;
           //使用 hgetall获取所有用户信息映射关系
           userlnfoMap = redis.hgetAll(userRedisKey) ;
           Userlnfouserlnfo;
            if (userlnfoMap != null)  {
                 //将映射关系转换为 U serln fo
                 userlnfo = transferMapToUserlnfo(userlnfoMap) ;
             } else {
                   //从 MySQL 中获取用户信息
                   userlnfo = mysql.get(id );
                   //将 userlnfo变为映射关系使用 hmset 保存到 Redis 中
                   redis.hmset(userRedisKey, transferUserlnfoToMap(userlnfo));
                   //添加过期时间
                  redis.expire(userRedisKey, 3600);
            }
            return userlnfo;

    但是需要注意的是哈希类型和关系型数据库有两点不同之处:

      • 哈希类型是稀疏的,而关系型数据库是完全结构化的,例如哈希类型每个键可以有不同的field, 而关系型数据库一旦添加新的列,所有行都要为其设置值(即使为NULL),如图2-17所示。
      • 关系型数据库可以做复杂的关系查询,而Redis去模拟关系型复杂查询开发困难,维护成本高。

    开发人员需要将两者的特点搞清楚,才能在适合的场景使用适合的技术。到目前为止,我们已经能够用三种方法缓存用户信息,下面给出三种方案的实现方法和优缺点分析。

    1)原生字符串类型:每个属性一个键。

    set user:1:name tom
    set user:1:age 23
    set user:1:city beijing

    优点:简单直观,每个属性都支持更新操作。

    缺点:占用过多的键,内存占用量较大,同时用户信息内聚性比较差,所以此种方案一般不会在生产环境使用。

    2) 序列化字符串类型:将用户信息序列化后用一个键保存。

    user:1 serialize (userlnfo)

    优点:简化编程,如果合理的使用序列化可以提高内存的使用效率。

    缺点:序列化和反序列化有一定的开销,同时每次更新属性都需要把全部数据取出进行反序列化,更新后再序列化到Redis中。

    3) 哈希类型:每个用户属性使用一对field-value,但是只用一个键保存.

    hmset user:1 name tom age 23 city beijing

    优点:简单直观,如果使用合理可以减少内存空间的使用。

    缺点:要控制哈希在ziplist和 hashtable两种内部编码的转换,hashtable会消耗更多内存。

     

    2.3 列表

      列表(list) 类型是用来存储多个有序的字符串,如图2-18所示,a、b、c、d、e 五个元素从左到右组成了一个有序的列表,列表中的每个字符串称为元素(element), —个列表最多可以存储232-1个元素。在 Redis中,可以对列表两端插入(push) 和弹出(pop),还可以获取指定范围的元素列表、获取指定索引下标的元素等(如图2-18和图2-19所示)。列表是一种比较灵活的数据结构,它可以充当栈和队列的角色,在实际开发上有很多应用场景。

      列表类型有两个特点:第一、列表中的元素是有序的,这就意味着可以通过索引下标获取某个元素或者某个范围内的元素列表,例如要获取图2-19的第5 个元素,可以执行lindex user:1:message4 (索引从0算起)就可以得到元素e。第二、列表中的元素可以是重复的,例如图2-20所示列表中包含了两个字符串a。

      这两个特点在后面介绍集合和有序集合后,会显得更加突出,因此在考虑是否使用该数据结构前,首先需要弄清楚列表数据结构的特点。

    2.3.1 命令

      下面将按照对列表的5 种操作类型对命令进行介绍,命令如表2-4所示。

    表 2-4 列表的四种操作类型
    操作类型 操 作
    添加 rpush lpush linsert
    lrange lindex llen
    删除 lpop rpop lrem ltrim
    修改 lset
    阻塞操作 blpop brpop

     

    1.添加操作

    (1) 从右边插入元素

    rpush key value [value ...]

      下面代码从右向左插人元素c、b、a:

    127.0.0.1:6379> rpush listkey c b a
    (integer) 3

    lrange 0 -1命令可以从左到右获取列表的所有元素:

    127.0.0.1:6379> lrange listkey 0 -1
    1) "c"
    2) "b"
    3) "a"

    (2) 从左边插入元素

    lpush key value [value …]

    使用方法和rpush相同,只不过从左侧插人,这里不再赘述。

    (3) 向某个元素前或者后插人元素

    linsert key before|after pivot value

    linsert 命令会从列表中找到等于pivot的元素,在其前(before) 或者后(after) 插入一个新的元素value,例如下面操作会在列表的元素b前插入java:

    127.0.0.1:6379> linsert listkey before b java
    (integer) 4

    返回结果为4,代表当前命令的长度,当前列表变为:

    127.0.0.1:6379> lrange listkey 0 -1
    1) "c"
    2) "java"
    3) "b"
    4) "a"

    2.查找

    (1) 获取指定范围内的元素列表

    lrange key start end

    lrange操作会获取列表指定索引范围所有的元素。索引下标有两个特点:第一,索引下标从左到右分别是0到 N-1,但是从右到左分别是-1到 -N。第二,lrange中的end选项包含了自身,这个和很多编程语言不包含end不太相同,例如想获取列表的第2 到第4 个元素,可以执行如下操作:

    127.0.0.1:6379> lrange listkey 1 3
    1)  "java"
    2)  "b"
    3)  "a"

    (2) 获取列表指定索引下标的元素

    lindex key index

    例如当前列表最后一个元素为a:

    127.0.0.1:6379> lindex listkey -1
    "a"

    (3) 获取列表长度

    llen key

    例如,下面示例当前列表长度为4:

    127.0.0.1:6379> llen listkey
    (integer) 4

    3.删除

    (1) 从列表左侧弹出元素

    lpop key

    如下操作将列表最左侧的元素c会被弹出,弹出后列表变为java、b、a :

    127.0.0.1:6379> t lpop listkey
    "c"
    127.0.0.1:6379> lrange listkey 0 -1
    1)  "java"
    2)  "b"
    3)  "a"

    (2) 从列表右侧弹出

    rpop key

    它的使用方法和lpop是一样的,只不过从列表右侧弹出,这里不再赘述。

    (3) 删除指定元素

    lrem key count, value

    lrem命令会从列表中找到等于value的元素进行删除,根据count的不同分为三种情况:

    □ count>0,从左到右,删除最多count个元素。

    □ count<0,从右到左,删除最多count绝对值个元素。

    □ count=0,删除所有。

    例如向列表从左向右插人5 个 a, 那么当前列表变为“aaaaajavaba”,下面操作将从列表左边开始删除4 个为a 的元素:

    127.0.0.1:6379> lrem listkey 4 a
    (integer) 4
    127.0.0.1:6379> lrange listkey 0 -1
    1) "a"
    2) "java"
    3) "b"
    4) "a"

    (4) 按照索引范围修剪列表

    ltrim key start end

    例如,下面操作会只保留列表listkey第2个到第4个元素

    127.0.0.1:6379> ltrim listkey 1 3
    OK
    127.0.0.1:6379> lrange listkey 0 -1
    1) "java"
    2) "b"
    3) "a"

    4.修改

    修改指定索引下标的元素:

    lset key index newValue

    下面操作会将列表listkey中的第3个元素设置为python:

    127.0.0.1:6379> lset listkey 2 python
    OK
    127.0.0.1:6379> lrange listkey 0 -1
    1)  "java"
    2)  "b"
    3)  "python"

    5.阻塞操作

    阻塞式弹出如下:

    blpop key [key … ] timeout
    brpop key [key … ] timeout

    blpop和 brpop是lpop和rpop的阻塞版本,它们除了弹出方向不同,使用方法基本相同,所以下面以brpop命令进行说明,brpop命令包含两个参数:

    □ key [key …]:多个列表的键。

    □ timeout: 阻塞时间(单位:秒)。

     

    (1) 列表为空:如果timeout=3 , 那么客户端要等到3 秒后返回,如果timeout=0, 那么客户端一直阻塞等下去:

    127.0.0.1:6379> brpop list:test 3
    (nil)
    (3.10s)
    127.0.0.1:6379 > brpop list:test 0

    如果此期间添加了数据elementl, 客户端立即返回

    127.0.0.1:6379> brpop list:test 3
    1)  "list:test"
    2)  "elementl"
    (2.06s)

    (2) 列表不为空:客户端会立即返回

    127.0.0.1:6379> brpop list:test 0
    1)  "list:test"
    2)  "elementl"

    在使用brpop时,有两点需要注意。

    第一点,如果是多个键,那么brpop会从左至右遍历键,一旦有一个键能弹出元素 ,客户端立即返回

    127.0.0.1:6379> brpop list : 1 list:2 list:3 0
    .. 阻 塞 ..

    此时另一个客户端分别向list:2和list:3插人元素:

    client-lpush > lpush list:2 element2
    (integer) 1
    client-lpush > lpush list:3 element3
    (integer) 1

    客户端会立即返回list:2 中的element2, 因为list:2 最先有可以弹出的元素:

    127.0.0.1:6379> brpop list:1 list:2 list:3 0
    1) "list:2"
    2) "element2_l "

    第二点,如果多个客户端对同一个键执行brpop, 那么最先执行brpop命令的客户端可以获取到弹出的值.

    客户端1:
    client-1 > brpop list:test 0
    ... 阻 塞 ...
    客户端2:
    client-2 > brpop list:test 0
    .. . 阻 塞 ...
    客户端3:
    client-3 > brpop list:test 0
    ...阻 塞 ...

    此时另一个客户端lpush—个元素到list:test列表中:

    client-lpush > lpush list:test element
    (integer) 1

    那么客户端1最会获取到元素,因为客户端1最先执行brpop, 而客户端2 和客户端3继续阻塞:

    client> brpop list:test 0
    1) "list:test "
    2) "element"

    有关列表的基础命令已经介绍完了,表 2-5是这些命令的时间复杂度,开发人员可以参考此表选择适合的命令。

    表 2-5 列表命令时间复杂度
    操作类型 命令 时间复杂度
    添加 rpush key value [value ... ] O(k),k是元素个数
    lpush key value [value ... ] O(k),k是元素个数
    linsert key before|after pivot value O(n),n是pivot距离列表头或尾的距离
    查找 lrange key start end O(s+n),s是start偏移量,n是start 到 end的范围
    lindex key index O(n),n是索引的偏移量
    llen key O(1)
    删除 lpop key O(1)
    rpop key O(1)
    lrem count value O(n),n是列表长度
    ltrim key start end O(n),n是要裁剪的元素总数
    修改 lset key index value O(n), n是索引的偏移量
      blpop brpop O(1)

     

    2.3.2 列表的内部编码

    列表类型的内部编码有两种。

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

    下面的示例演示了列表类型的内部编码,以及相应的变化。

    1)当元素个数较少且没有大元素时,内部编码为ziplist:

    127.0.0.1:6379> rpush listkey el e2 e3
    (integer) 3
    127.0.0.1:6379> object encoding listkey
    "ziplist"

    2.1)当元素个数超过512个,内部编码变为linkedlist:

    127.0.0.1:6379> rpush listkey e4 e5 ... 忽 略 . . . e512 e513
    ( integer) 513
    127.0.0.1:6379> object encoding listkey
    "linkedlist"

    2.2 ) 或者当某个元素超过64字节,内部编码也会变为linkedlist:

    127.0.0.1:6379> rpush listkey "one string is bigger than 64 byte ...........................
    ( integer) 4
    127.0.0.1:6379> object encoding listkey
    "linkedlist"

    2.3.3 使用场景

      1.消息队列

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

     

      2.文章列表

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

    1)每篇文章使用哈希结构存储,例如每篇文章有3 个属性title 、timestamp、content:

    hmset acticle:1 title xx timestamp 1476536196 content xxxx
    ...
    hmset acticle:k title yy timestamp 1476512536 content yyyy
    ...

    2)向用户文章列表添加文章,user:{id}:articles作为用户文章列表的键:

    lpush user:1:acticles article:1 article 3
    ....
    lpush user:k:acticles article:5

    3)分页获取用户文章列表,例如下面伪代码获取用户id=l的前10篇文章:

    articles = lrange user:1:articles 0 9
    for article in {articles}
          hgetall { article}

    使用列表类型保存和获取文章列表会存在两个问题。第一,如果每次分页获取的文章个数较多,需要执行多次hgetall操作,此时可以考虑使用Pipeline)批量获取,或者考虑将文章数据序列化为字符串类型,使用mget批量获取。第二,分页获敢文章列表时,lrange命令在列表两端性能较好,但是如果列表较大,获取列表中间范围的元素性能会变差,此时可以考虑将列表做二级拆分,或者使用Redis3.2的quicklist内部编码实现,它结合ziplist和 linkedlist的特点,获取列表中间范围的元素时也可以高效完成。

     

    2.4 集合

      集合(set)类型也是用来保存多个的字符串元素,但和列表类型不一样的是,集合中不允许有重复元素,并且集合中的元素是无序的,不能通过索引下标获取元素。如图2-22所示,集合user:1:follow 包含着"it"、"music"、"his”、"sports" 四个元素,一个集合最多可以存储232-1个元素。Redis除了支持集合内的增删改查,同时还支持多个集合取交集、并集、差集,合理地使用好集合类型,能在实际开发中解决很多实际问题。

     

    2.4.1 命令

    下面将按照集合内和集合间两个维度对集合的常用命令进行介绍。

    1.集合内操作

    (1) 添加元素

    sadd key element [element ...]

    返回结果为添加成功的元素个数,例如:

    127.0.0.1:6379> exists myset
    (integer) 0
    127.0.0.1:6379> sadd myset a b c
    (integer) 3
    127.0.0.1:6379> sadd myset a b
    (integer) 0

    (2) 删除元素

    srem key element [element ...]

    返回结果为成功删除元素个数,例如:

    127.0. 0.1:6379> srem myset a b
    (integer) 2
    127.0. 0.1:6379> srem myset hello
    (integer) 0

    (3) 计算元素个数

    scard key

    scard的时间复杂度为0(1),它不会遍历集合所有元素,而是直接用Redis内部的变量,例如:

    127.0.0.1:6379> scard myset
    (integer) 1

    (4) 判断元素是否在集合中

    sismember key element

    如果给定元素element在集合内返回1,反之返回0, 例如:

    127.0.0.1:6379> sismember myset c
    (integer) 1

    (5) 随机从集合返回指定个数元素

    srandmember key [count]

    [count]是可选参数,如果不写默认为1, 例如:

    127.0.0.1:6379> srandmember myset 2
    1) "a"
    2) "c"
    127.0.0.1:6379> srandmember myset
    "d"

    (6) 从集合随机弹出元素

    spop key

    spop操作可以从集合中随机弹出一个元素,例如下面代码是一次 spop 后,集合元素变为"d b a":

    127.0.0.1:6379> spop myset
    "c"
    127.0.0.1:6379> smembers myset
    1)  "d"
    2)  "b"
    3)  "a"

    需要注意的是Redis从3.2版本开始,spop也支持[count] 参数。

    srandmember和spop都是随机从集合选出元素,两者不同的是spop命令执行后,元素会从集合中删除,而srandmember不会。

    (7) 获取所有元素

    smembers key

    下面代码获取集合myset所有元素,并且返回结果是无序的:

    127.0.0.1:6379> smembers myset
    1)  "d"
    2)  "b"
    3)  "a"

    smembers和lrange、hgetall都属于比较重的命令,如果元素过多存在阻塞Redis的可能性,这时候可以使用sscan来完成

     

    2.集合间操作

    现在有两个集合,它们分别是user:1:follow 和 user:2:follow:

    127.0.0.1:6379> sadd user:1:follow it music his sports
    ( integer) 4
    127.0.0.1:6379> sadd user:2:follow it news ent sports
    ( integer) 4

    (1) 求多个集合的交集

    sinter key [key …]

    例如下面代码是求user:1:follow和user:2:follow两个集合的交集,返回结果是 sports、it :

    127.0.0.1:6379> sinter user:1:follow user:2:follow
    1)  "sports"
    2)  "it"

    (2) 求多个集合的并集

    suinon key [key … ]

    例如下面代码是求user:1:follow和user:2:follow两个集合的并集,返回结果是sports、it、his、news、music、ent:

    127.0.0.1:6379> sunion user:1:follow user:2:follow
    1) "sports"
    2) "it"
    3) "his"
    4) "news"
    5) "music"
    6) "ent"

    (3) 求多个集合的差集

    sdiff key [key … ]

    例如下面代码是求user:1:follow和user:2:follow两个集合的差集,返回结果是music和his:

    127.0.0.1:6379> sdiff user:1:follow user:2:follow
    1) "music"
    2)  "his

    前面三个命令如图2-23所示

    (4) 将交集、并集、差集的结果保存

    sinterstore destination key [key . . . ]
    suionstore destination key [key . . . ]
    sdiffstore  destination key [key . . . ]

    集合间的运算在元素较多的情况下会比较耗时,所以Redis提供了上面三个命(原命令 +  store) 将集合间交集、并集、差集的结果保存在 destination key 中,例如下面操作将 user:1:follow和 user:2:follow两个集合的交集结果保存在user:1_2: inter中,user:1_2:inter本身也是集合类型:

    127.0.0.1:6379> sinterstore user:1_2:inter user:1:follow user:2:follow
    (integer) 2
    127.0.0.1:6379> type user:1_2:inter
    set
    127.0.0.1:6379> smembers user:1_2:inter
    1)  "it"
    2)  "sports"

    至此有关集合的命令基本已经介绍完了,表 2-6给出集合常用命令的时间复杂度,开发人员可以根据自身需求进行选择。

    表 2-6 集合常用命令时间复杂度
    命 令 时间复杂度
    sadd key element [element ...]  O(k),k是元素个数
    srem key element [element ...]  O(k),k是元素个数
    scard key  O(1)
    sismember key element  O(1)
    srandmember key [count]  O(count)
    spop key  O(1)
    smembers key  O(n),n是元素总数
    sinter key [key ...]或者sinterstore  O(m*k),k是多个集合中元素最少的个数,是键个数
    suinon key [key ...] 或者 suionstore  O(k),k是多个集合元素个数和
    sdiffkey [key ...] 或者 sdiffstore  O(k),k是多个集合元素个数和

     

    2.4.2 集合的内部编码

    集合类型的内部编码有两种:

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

    下面用示例来说明:

    1)当元素个数较少且都为整数时,内部编码为intset:

    127.0.0.1:6379> sadd setkey 1 2 3 4
    (integer) 4
    127.0.0.1:6379> object encoding setkey
    "intset"

    2.1)当元素个数超过 512 个,内部编码变为 hashtable:

    127.0.0.1:6379> sadd setkey 1 2 3 4 5 6 . . . 512 513
    (integer) 509
    127.0.0.1:6379> scard setkey
    (integer) 513
    127.0.0.1:6379> object encoding listkey
    "hashtable"

    2.2)当某个元素不为整数时,内部编码也会变为hashtable:

    127.0.0.1:6379> sadd setkey a
    (integer) 1
    127.0.0.1:6379> object encoding setkey
    "hashtable"

    2.4.3 使用场景

      集合类型比较典型的使用场景是标签(tag)。例如一个用户可能对娱乐、体育比较感兴趣,另一个用户可能对历史、新闻比较感兴趣,这些兴趣点就是标签。有了这些数据就可以得到喜欢同一个标签的人,以及用户的共同喜好的标签,这些数据对于用户体验以及增强用户黏度比较重要。例如一个电子商务的网站会对不同标签的用户做不同类型的推荐,比如对数码产品比较感兴趣的人,在各个页面或者通过邮件的形式给他们推荐最新的数码产品,通常会为网站带来更多的利益。

    下面使用集合类型实现标签功能的若干功能。

    (1) 给用户添加标签

    sadd user:1:tags tag1 tag2 tag5
    sadd user:2:tags tag2 tag3 tag5
    sadd user:k:tags tag1 tag2 tag4

    (2) 给标签添加用户

    sadd tag1:users user:1 user:3
    sadd tag2:users user:1 user:2 user:3
    sadd tagk:users user:1 user:2

    (3) 删除用户下的标签

    srem user:1:tags tag1 tag5

    (4) 删除标签下的用户

    srem tag1:users user:1
    srem tag5:users user:1

    (3) 和(4) 也是尽量放在一个事务执行。

    (5) 计算用户共同感兴趣的标签

    可以使用sinter命令,来计算用户共同感兴趣的标签,如下代码所示:

    sinter user:1:tags user:2:tags

    前面只是给出了使用Redis集合类型实现标签的基本思路,实际上一个标签系统远比这个要复杂得多,不过集合类型的应用场景通常为以下几种:

    □ sadd = Tagging(标签)

    □ spop/srandmember = Random item( 生成随机数,比如抽奖)

    □ sadd + sinter = Social Graph( 社交需求)

     

    2.5 有序集合

      有序集合相对于哈希、列表、集合 来说会有一点点陌生,但既然叫有序集合,那么它和集合必然有着联系,它保留了集合不能有重复成员的特性,但不同的是,有序集合中的元素可以排序。但是它和列表使用索引下标作为排序依据不同的是,它给每个元素设置一个分数(score)作为排序的依据。如图2-24所示,该有序集合包含kris、mike、frank、tim 、martin、tom,它们的分数分别是1、91、200、220、250、251,有序集合提供了获取指定分数和元素范围查询、计算成员排名等功能,合理的利用有序集合,能帮助我们在实际开发中解决很多问题。

      有序集合中的元素不能重复,但是score可以重复,就和一个班里的同学学号不能重复,但是考试成绩可以相同。

      表2-7给出了列表、集合、有序集合三者的异同点。

     表 2-7 给出了列表、集合和有序集合三者的异同点
     数据结构  是否予许重复元素 是否有序  有序实现方式  应用场景 
     列表  是  是  索引下标  时间轴、消息队列等
     集合  否  否  无  标签、社交等
     有序集合  否  是  分值  排行榜系统、社交等

      

    2.5.1 命令

    本节依旧按照集合内和集合外两个维度对有序集合的命令进行介绍。

    1.集合内

    (1) 添加成员

    zadd key score member [score member ... ]

    下面操作向有序集合u ser: ranking添加用户tom和他的分数251:

    127.0.0.1:6379> zadd user:ranking 251 tom
    (integer) 1

    返回结果代表成功添加成员的个数:

    127.0.0.1:6379> zadd user:ranking 1 kris 91 mike 200 frank 220 tim 250 martin
    (integer) 5

    有关zadd命令有两点需要注意 :

    口 Redis3.2为zadd命令添加了nx、xx、ch、incr四个选项:

    • nx: member必须不存在,才可以设置成功,用于添加。

    • xx: member必须存在,才可以设置成功,用于更新。

    • ch: 返回此次操作后,有序集合元素和分数发生变化的个数

    • incr:对 score做增加,相当于后面介绍的zincrby。

    □ 有序集合相比集合提供了排序字段,但是也产生了代价,zadd的时间复杂度为O(log(n)),sadd的时间复杂度为0(1)。

    (2) 计算成员个数

    zcard key

    例如下面操作返回有序集合user:ranking的成员数为5,和集合类塾的scard命令一样,zcard的时间复杂度为0(1)。

    127.0.0.1:6379> zcard user:ranking
    (integer) 5

    (3) 计算某个成员的分数

    zscore key member

    tom的分数为251,如果成员不存在则返回nil:

    127.0.0.1:6379> zscore user:ranking tom
    "251"
    127.0.0.1:6379> zscore user:ranking test
    (nil)

    (4) 计算成员的排名

    zrank key member
    zrevrank key member

    zrank是从分数从低到高返回排名,zrevrank反之。例如下面操作中,tom在zrank和zrevrank分另排名第5和第0(排名从0开始计算)。

    127.0.0.1:6379> zrank user:ranking tom
    (integer) 5
    127.0.0.1:6379> zrevrank user:ranking tom
    (integer)  0

    (5) 删除成员

    zrem key member [member …]

    下面操作将成员mike从有序集合user:ranking中删除

    127.0.0.1:6379> zrem user:ranking mike
    ( integer) 1

    返回结果为成功删除的个数。

    (6) 增加成员的分数

    zincrby key increment member

    下面操作给tom增加了9分,分数变为了260分:

    127.0.0.1:6379> zincrby user:ranking 9 tom
    "260"

    (7) 返回指定排名范围的成员

    zrange  key start end [withscores]
    zrevrange key start end [withscores]

    有序集合是按照分值排名的,zrange是从低到高返回,zrevrange反之。下面代码返回排名最低的是三个成员,如果加上withscores选项,同时会返回成员的分数:

    127.0.0.1:6379> zrange user:ranking 0 2 withscores
    1) "kris "
    2) "1"
    3) "frank"
    4) "200"
    5) "tim"
    6 ) "220 "
    127.0.0.1:6379> zrevrange user:ranking 0 2 withscores
    1) "tom"
    2) "260"
    3) "martin"
    4) "250"
    5) "tim"
    6) "220"

    (8) 返回指定分数范围的成员

    zrangebyscore  key min max [withscores]  [limit offset count]
    zrevrangebyscore key max min [withscores] [limit offset count]

    其中zrangebyscore 按照分数从低到高返回,zrevrangebyscore反之。例如下面操作从低到高返回200到221分的成员,withscores 选项会同时返回每个成员的分数。

    [limit offset count] 选项可以限制输出的起始位置和个数:

    127.0.0.1:6379>zrangebyscore user:ranking 200 tinf withscores
    1)  "frank"
    2 ) "200 "
    3)  "tim"
    4)  "220"
    127.0.0.1:6379> zrevrangebyscore user:ranking 221 200 withscores
    1)  "tim"
    2) "220 "
    3) "frank"
    4)  "200"

    同时min和max还支持开区间(小括号)和闭区间(中括号),-inf和+inf分别代表无限小和无限大:

    127.0.0.1:6379> zrangebyscore user:ranking (200 + inf withscores
    1)  "tim"
    2)  "220 "
    3)  "martin"
    4)  "250"
    5)  "tom"
    6)  "260"

    (9) 返回指定分数范围成员个数

    zcount key min max

    下面操作返回200到221分的成员的个数:

    127.0.0.1:6379> zcount user:ranking 200 221
    (integer) 2

    (10) 删除指定排名内的升序元素

    zremrangebyrank key start end

    下面操作删除第start到第end名的成员:

    127.0.0.1:6379> zremrangebyrank user:ranking 0 2
    (integer) 3

    (11) 删除指定分数范围的成员

    remrangebyscore key min max

    下面操作将250分以上的成员全部删除,返回结果为成功删除的个数:

    127.0.0.1:6379> zremrangebyscore user:ranking (250 + inf
    (integer) 2

      2.集合间的操作

    将图2-25的两个有序集合导入到Redis中。

    127.0.0.1:6379> zadd user:ranking:1 1 kris 91 mike 200 frank 220 tim 250 martin 251 tom
    (integer) 6
    127.0.0.1:6379> zadd user:ranking:2 8 james 77 mike 625 martin 888 tom
    (integer) 4

    (1) 交集

    zinterstore destination numkeys key [key …]  [weights weight  [weight …]]
    [aggregate sumIminImax]

    这个命令参数较多,下面分别进行说明:

    □ destination:交集计算结果保存到这个键。

    □ numkeys:需要做交集计算键的个数。

    □ key [key:需要做交集计算的键。

    □ weights weight [weight …]:每个键的权重,在做交集计算时,每个键中的每个member会将自己分数乘以这个权重,每个键的权重默认是1。

    □ aggregate sum|min|max :计算成员交集后,分值可以按照sum (和)、min(最小值)、max(最大值)做汇总,默认值是sum。

    下面操作对user:ranking:1和user:ranking:2做交集,weights和 aggregate使用了默认配置,可以看到目标键user:ranking:l_inter_2对分值做了sum操作:

    127.0.0.1:6379 > zinterstore user:ranking:1_inter_2 2 user:ranking:1 user:ranking:2
    (integer) 3
    127.0.0.1:6379> zrange user:ranking:1_inter_2 0 -1 withscores
    1) "mike"
    2) "168"
    3) "martin"
    4) "875"
    5) "tom"
    6) "1139"

    如果想让user:ranking:2的权重变为0.5,并且聚合效果使用max,可以执行如下操作:

    127.0.0.1:6379> zinterstore user:ranking:1_inter_2  2 user:ranking:1 user:ranking:2 weights 1 0.5 aggregate max
    (integer) 3
    127.0.0.1:6379> zrange user:ranking:1_inter_2 0 -1 withscores
    1) "mike"
    2) "91"
    3) "martin"
    4) "312.5"
    5) "tom"
    6) "444"

    (2) 并集

    zunionstore destination numkeys key [key ...]  [weights weight [weight ...]]
    [aggregate sum|min|max]

    该命令的所有参数和zinterstore是一致的,只不过是做并集计算,例如下面操作是计算 user:ranking:1 和 user:ranking:2 的并集,weights和aggregate使用了默认配置,可以看到目标键user:ranking:l_union_2对分值做了sum操作:

    127.0.0.2:6379> zunionstore user:ranking:1_union_2  2  user:ranking:1 user:ranking:2
    (integer) 7
    127.0.0.1:6379> zrange user:ranking:1_union_2 0 -1 withscores
    1) "kris"
    2) "1"
    3) "james"
    4) "8"
    5) "mike"
    6) "168"
    7) "frank"
    8) "200"
    9) "tim"
    10) "220"
    11) "martin"
    12) "875"
    13) "tom"
    14) "1139"

    至此有序集合的命令基本介绍完了,表 2-8是这些命令的时间复杂度,开发人员在使用对应的命令进行开发时,不仅要考虑功能性,还要了解相应的时间复杂度,防止由于使用不当造成应用方效率下降以及Redis阻塞。

    表 2-8 有序集合命令的时间复杂度
    命令 时间复杂度
    zadd key score member [score member … ] O(k X log(n))),k是添加成员的个数,n是当前有序集合成员个数
    zcard key O(1)
    score key member O(1)

    zrank key member

    zrevrank key member

    O(log(n)),n是当前有序集合成员个数
    zrem key member [member ... ] O(k*log(n)),k是删除成员的个数,n是当前有序集合成员个数
    zincrby key increment member O(log(n)),n是当前有序集合成员个数

    zrange  key start end [withscores]

    zrevrange key start end [withscores]

    O(log(n) +k),k是要获取的成员个数,n是当前有序集合成员个数

    zrangebyscore key min max [withscores]

    zrevrangebyscore key max min [withscores]

    O(log(n) +k),k是要获取的成员个数,n是当前有序集合成员个数
    zcount O(log(n)),n是当前有序集合成员个数
    zremrangebyrank key start end O(log(n) + k),k是要删除的成员个数,n是当前有序集合成员个数
    zremrangebyscore key min max O(log(n) + k),k是要删除的成员个数,n是当前有序集合成员个数
    zinterstore destination numkeys key [key ...] O(n*k)+O(m*log(m)), n是成员数最小的有序集合成员个数,k是有序集合的个数,m是结果集中成员个数
    zunionstore destination numkeys key [key ... ] O(n)+O(m*log(m)),n是所有有序集合成员个数和,m 是结果集中成员个数

     

    2.5.2 有序集合内部编码

    有序集合类型的内部编码有两种:

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

    下面用示例来说明:

    1)当元素个数较少且每个元素较小时,内部编码为skiplist:

    127.0.0.1:6379> zadd zsetkey 50 e1 60 e2 30 e3
    (integer) 3
    127.0.0.1:6379> object encoding zsetkey
    "ziplist"

    2.1)当元素个数超过128个,内部编码变为ziplist:

    127.0.0.1:6379> zadd zsetkey 50 e1 60 e2 30 e3 12 e4 ...忽 略 . . . 84 e129
    (integer) 129
    127.0.0.1:6379> object encoding zsetkey
    "skiplist"

    2.2)当某个元素大于64字节时,内部编码也会变为hashtable:

    127.0.0.1:6379> zadd zsetkey 20 "one string is bigger than 64 byte .......................
    (integer) 1
    127.0.0.1:6379> object encoding zsetkey
    "skiplist"

    2.5.3 使用场景

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

    (1) 添加用户赞数

    例如用户mike上传了一个视频,并获得了3个赞,可以使用有序集合的zadd和zincrby 功能:

    zadd user:ranking:2016_03_15 mike 3

    如果之后再获得一个赞,可以使用zincrby:

    zincrby user:ranking:2016_03_15 mike 1

    (2) 取消用户赞数

    由于各种原因(例如用户注销、用户作弊)需要将用户删除,此时需要将用户从榜单中删除掉,可以使用zrem。例如删除成员tom:

    zrem user:ranking:2016_03_15 mike

    (3) 展示获取赞数最多的十个用户

    此功能使用zrevrange命令实现:

    zrevrangebyrank user:ranking:2016_03_15 0 9

    (4) 展示用户信息以及用户分数

    此功能将用户名作为键后缀,将用户信息保存在哈希类型中,至于用户的分数和排名可

    以使用zscore和 zrank两个功能:

    hgetall user:info:tom
    zscore user:ranking:2016_03_15 mike
    zrank user:ranking:2016_03_15 mike

    3. 键管理

    本节将按照单个键、遍历键、数据库管理三个维度对一些通用命令进行介绍。

    3.1 单个键管理

    针对单个键的命令,例如 type、del、object、exists、expire等就不做讲解,下面将介绍剩余的几个重要命令。

    1.键重命名

    rename key newkey

    例如现有一个键值对,键为python, 值为jedis:

    127.0.0.1:6379> get python
    "jedis"

    下面操作将键python重命名为java:

    127.0.0.1:6379> set python jedis
    OK
    127.0.0.1:6379> rename python java
    OK
    127.0.0.1:6379> get python
    127.0.0.1:6379> get java
    "jedis"

    如果在 rename之前,键java已经存在,那么它的值也将被覆盖,如下所示:

    127.0.0.1:6379> set a b
    OK
    127.0.0.1:6379> set c d
    OK
    127.0.0.1:6379> rename a c
    OK
    127.0.0.1 :6379> get a
    (nil)
    127.0.0.1:6379> get c
    "b"

    为了防止被强行 rename,Redis提供了renamenx命令,确保只有newKey不存在时候才被覆盖,例如下面操作 renamenx时,newkey = python 已经存在,返回结果是 0 代表没有完成重命名,所以键 java 和 python的值没变:

    127.0.0.1:6379> set java jedis
    OK
    127.0.0.1:6379> set python redis-py
    OK
    127.0.0.1:6379> renamenx java python
    (integer) 0
    127.0.0.1:6379> get java
    "jedis"
    127.0.0.1:6379> get python
    "redis-py"

    在使用重命名命令时,有两点需要注意:

    □ 由于重命名键期间会执行del命令删除旧的键,如果键对应的值比较大,会存在阻塞Redis的可能性,这点不要忽视。

    □ 如果 rename和renamenx中的key和 newkey如果是相同的,在 Redis 3.2和之前版本返回结果略有不同。

    Redis 3.2中会返回OK:

    127.0.0.1:6379> rename key key
    OK

    Redis 3.2之前的版本会提示错误:

    127.0.0.1:6379> rename key key
    (error) ERR source and destination objects are the same

    2.随机返回一个键

    randomkey

    下面示例中,当前数据库有1000个键值对,randomkey命令会随机从中挑选一个键

    127.0.0.1:6379> dbsize
    1000
    127.0.0.1:6379> randomkey
    "hello"
    127.0.0.1:6379> randomkey
    "jedis"

    3.键过期

    除了expire、ttl命令以外,Redis还提供了expireat、pexpire、pexpireat、pttl、persist等一系列命令,下面分别进行说明:

    □ expire key seconds:键在 seconds 秒后过期0

    □ expireat key timestamp:键在秒级时间戳 timestamp 后过期。

    下面为键hello设置了10秒的过期时间,然后通过ttl观察它的过期剩余时间(单位:秒),随着时间的推移,ttl逐渐变小,最终变为-2:

    127.0.0.1:6379> set hello world
    OK
    127.0.0.1:6379> expire hello 10
    (integer) 1
    #还剩7秒
    127.0.0.1:6379> ttl hello
    (integer) 7
    #还剩0秒
    127.0.0.1:6379> ttl hello
    (integer) 0
    #返回结果为-2,说明键hello已经被删除
    127.0.0.1:6379> ttl hello
    (integer) -2

    ttl命令和pttl都可以查询键的剩余过期时间,但是pttl精度更高可以达到毫秒级别,有3种返回值:

    □ 大于等于0的整数:键剩余的过期时间(ttl是秒,pttl是毫秒)。

    □ -1 : 键没有设置过期时间。

    □ -2 : 键不存在。

    expireat命令可以设置键的秒级过期时间戳,例如如果需要将键hello在2016-08-01 00:00:00 (秒级时间戳为1469980800)过期,可以执行如下操作:

    127.0.0.1:6379> expireat hello 1469980800
    (integer) 1

    除此之外,Redis 2.6版本后提供了毫秒级的过期方案:

    □ pexpire key milliseconds: 键在milliseconds毫秒后过期。

    □ pexpireat key milliseconds-timestamp 键在毫秒级时间戳timestamp后过期。

    但无论是使用过期时间还是时间戳,秒级还是毫秒级,在 Redis内部最终使用的都是pexpireat 。

    在使用Redis相关过期命令时,需要注意以下几点。

    (1)如果expire key的键不存在,返回结果为0:

    127.0.0.1:6379> expire not_exist_key 30
    (integer) 0

    (2)如果过期时间为负值,键会立即被删除,犹如使用del命令一样:

    127.0.0.1:6379> set hello world
    OK
    127.0.0.1:6379> expire hello -2
    (integer) 1
    127.0.0.1:6379> get hello
    (nil)

    (3)persist命令可以将键的过期时间清除:

    127.0.0.1:6379> hset key f1 v1
    (integer) 1
    127.0.0.1:6379> expire key 50
    ( integer) 1
    127.0.0.1:6379> ttl key
    (integer) 46
    127.0.0.1:6379> persist key
    (integer) 1
    127.0.0.1:6379> ttl key
    (integer) -1

    (4)对于字符串类型键,执行set命令会去掉过期时间,这个问题很容易在开发中被忽视。如下是Redis源码中,set命令的函数setKey,可以看到最后执行了  removeExpire(db, key) 函数去掉了过期时间:

    void setKey(redisDb *db, robj *key, robj * val)  {
           if (lookupKeyWrite(db,key) == NULL)  {
               dbAdd(db, key ,val);
            } else {
                dbOverwrite(db,key,val);
            }
            incrRefCount(val);
             //去掉过期时间
           removeExpire(db,key);
            signalModifiedKey(db ,key);
     }

    下面的例子证实了set会导致过期时间失效,因ttl变为-1:

    127.0.0.1:6379> expire hello 50
    (integer) 1
    127.0.0.1:6379> ttl hello
    (integer) 46
    127.0.0.1:6379> set hello world
    OK
    127.0.0.1:6379> ttl hello
    (integer) -1

    (5)Redis不支持二级数据结构(例如哈希、列表)内部元素的过期功能,例如不能对列表类型的一个元素做过期时间设置。

    (6)setex 命令作为set + expire的组合,不但是原子执行,同时减少了一次网络通讯的时间。

    有关Redis键过期的详细原理。Redis内存文章会讲解。

     

      4.迁移键

      迁移键功能非常重要,因为有时候我们只想把部分数据由一个Redis迁移到另一个Redis (例如从生产环境迁移到测试环境),Redis发展历程中提供了move、dump + restore 、migrate三组迁移键的方法,它们的实现方式以及使用的场景不太相同,下面分别介绍。

    (1) move

    move key db

    如图 2-26所示,move命令用于在Redis内部进行数据迁移,Redis内部可以有多个数据库,由于多个数据库功能后面会进行介绍,这里只需要知道Redis内部可以有多个数据库,彼此在数据上是相互隔离的,move key db就是把指定的键从源数据库移动到目标数据库中,但笔者认为多数据库功能不建议在生产环境使用,所以这个命令读者知道即可。

    (2) dump + restore

    dump key
    restore key ttl value

    dump + restore可以实现在不同的Redis实例之间进行数据迁移的功能,整个迁移的过程分为两步:

    1) 在源Redis上,dump命令会将键值序列化,格式采用的是RDB格式。

    2) 在目标Redis上,restore命令将上面序列化的值进行复原,其中ttl参数代表过期时间,如果ttl = 0 代表没有过期时间。

    整个过程如图2-27所示。

    有关dump + restore有两点需要注意:第一,整个迁移过程并非原子性的,而是通过客户端分步完成的。第二,迁移过程是开启了两个客户端连接,所以dump的结果不是在源Redis和目标Redis之间进行传输,下面用一个例子演示完整过程。

    1) 在源Redis上执行dump:

    redis-source> set hello world
    OK 
    redis-source> dump hello
    "x00x05worldx06x00x8f<Tx04%xfcNQ

    2) 在目标Redis上执行restore:

    redis-target> get hello
    (nil)
    redis -target> restore hello 0 "x00x05worldx06x00x8f<Tx04%xfcNQ"
    OK
    redis -target> get hello
    "world"

    上面2步对应的伪代码如下:

    Redis sourceRedis = new Redis ( "sourceMachine", 6379);
    Redis targetRedis = new Redis ( "targetMachine", 6379);
    targetRedis.restore("hello", 0, sourceRedis.dump(key));

    3) migrate

    migrate host port key I "" destination-db timeout [copy]  [replace]  [keys key [key . . . ] ]

    migrate命令也是用于在Redis实例间进行数据迁移的,实际上migrate命令就是将dump、restore、del三个命令进行组合,从而简化了操作流程。migrate命令具有原子性,而且从Redis 3.0.6版本以后已经支持迁移多个键的功能,有效地提高了迁移效率,migrate在文章10.4水平扩容中起到重要作用。

    整个过程如图2-28所示,实现过程和dump + restore基本类似,但是有3点不太相同:第一,整个过程是原子执行的,不需要在多个Redis实例上开启客户端的,只需要在源Redis上执行migrate命令即可。第二,migrate命令的数据传输直接在源Redis和目标Redis上完成的。第三,目标Redis完成restore后会发送OK给源Redis,源Redis接收后会根据migrate对应的选项来决定是否在源Redis上删除对应的键。

    下面对migrate的参数进行逐个说明:

    □ host:目标Redis的IP地址。

    □ port:目标Redis的端口。

    □ key丨"":在 Redis 3.0.6版本之前,migrate只支持迁移一个键,所以此处是要迁移的键,但 Redis 3.0.6版本之后支持迁移多个键,如果当前需要迁移多个键,此处为空字符串””。

    □ destination-db:目标 Redis 的数据库索引,例如要迁移到0号数据库,这里就写0。

    □ timeout:迁移的超时时间(单位为毫秒)。

    □ [copy]:如果添加此选项,迁移后并不删除源键。

    □ [replace]: 如果添加此选项,migrate木管目标Redis是否存在该键都会正常迁移进行数据覆盖。

    □ [keys key [key •••]]:迁移多个键,例如要迁移keyl、key2、key3 , 此处填写“keys keyl key2 key3 ”。

    下面用示例演示migrate命令,为了方便演示源Redis使用6379端口,目标Redis使用6380端口,现要将源Redis的键hello迁移到目标Redis中,会分为如下几种情况:

    情况1:源 Redis有键hello,目标Redis没有:

    127.0.0.1:6379> migrate 127.0.0.1 6380 hello 0 1000
    OK

    情况2:源 Redis和目标Redis都有键hello:

    127.0.0.1:6379> get hello
    "world"
    127.0.0.1:6380> get hello
    "redis "

    如果migrate命令没有加replace选项会收到错误提示,如果加了replace会返回OK表明迁移成功:

    127.0.0.1:6379> migrate 127.0.0.1 6379 hello 0 1000
    (error) ERR Target instance replied with error:BUSYKEY Target key name already exists.
    127.0.0.1:6379> migrate 127.0.0.1 6379 hello 0 1000 replace
    OK

    情况3: 源Redis没有键hello。如下所示,此种情况会收到nokey的提示

    127.0.0.1:6379> migrate 127.0.0.1 6380 hello 0 1000
    NOKEY

    下面演示一下Redis3.0.6版本以后迁移多个键的功能。

    □ 源Redis批量添加多个键

    127.0.0.1:6379> mset key1 value1 key2 value2 key3 value3
    OK

    □ 源Redis执行如下命令完成多个键的迁移:

    127.0. 0.1:6379> migrate 127.0.0.1 6380 "" 0 5000 keys key1 key2 key3
    OK

    使用表2-9总结一下move、dump + restore、migrate三种迁移方式的异同点,笔者建议使用migrate命令进行键值迁移。

     2-9 move、dump + restore、migrate 三个命令比较
    命令 作用域 原子性 支持多个键
    move Redis实例内部
    dump + restore Redis实例之间
    migrate Redis实例之间

     

    3.2 遍历键

    Redis提供了两个命令遍历所有的键,分别是keys和scan, 本节将对它们介绍并简要分析。

    1.全量遍历键

    实际上keys命令是支持pattern匹配的,例如向一个空的Redis插入4个字符串类型的键值对。

    127.0.0.1:6379> dbsize
    ( integer) 0
    127.0.0.1:6379> mset hello world redis best jedis best hill high
    OK

    如果要获取所有的键,可以使用keys pattern 命令:

    127.0.0.1:6379> keys *
    1) "hill"
    2) "jedis "
    3) "redis "
    4) "hello "

    上面为了遍历所有的键,pattern直接使用星号,这是因为pattern使用的是glob风格的通配符:

    □ * 代表匹配任意字符。

    □ ? 代表匹配一个字符。

    口 []代表匹配部分字符,例如[1,3]代表匹配1,3,[1-10]代表匹配1到 10的任意数字。

    □ x 用来做转义,例如要匹配星号、问号需要进行转义。

    下面操作匹配以j , r 开头,紧跟edis字符串的所有键:

    127.0.0.1:6379> keys [j,r]edis
    1)  "jedis"
    2)  "redis"

    例如下面操作会匹配到hello和hill这两个键:

    127.0.0.1:6379> keys h?ll*
    1) "hill"
    2) "hello"

    当需要遍历所有键时(例如检测过期或闲置时间、寻找大对象等),keys是一个很有帮助的命令,例如想删除所有以video字符串开头尚键,可以执行如下操作

    redis-cli keys video * | xargs redis-cli del

    但是如果考虑到Redis的单线程架构就不那么美妙了,如果Redis包含了大量的键,执行keys命令很可能会造成Redis阻塞,所以一般建议不要在生产环境下使用keys命令。但有时候确实有遍历键的需求该怎么办,可以在以下三种情况使用:

    □在一个不对外提供服务的Redis从节点上执行,这样不会阻塞到客户端的请求,但是会影响到主从复制.

    □如果确认键值总数确实比较少,可以执行该命令。

    □使用下面要介绍的scan 命令渐进式的遍历所有键,可以有效防止阻塞。

     

      2.渐进式遍历

      Redis从2.8版本后,提供了一个新的命令 scan, 它能有效的解决 keys 命令存在的问题。和keys命令执行时会遍历所有键不同,scan 采用渐进式遍历的方式来解决keys命令可能带来的阻塞问题,每次scan命令的时间复杂度是0(1),但是要真正实现keys的功能,需要执行多次scan。Redis 存储键值对实际使用的是 hashtable 的数据结构,其简化模型如图2-29所示。那么每次执行 scan,可以想象成只扫描一个字典中的一部分键,直到将字典中的所有键遍历完毕。scan的使用方法如下:

    scan cursor [match pattern]  [count number]

    □ cursor是必需参数,实际上cursor 是一个游标,第一次遍历从 0 开始,每次 scan遍历完都会返回当前游标的值,直到游标值为0,表示遍历结束。

    □ match pattern是可选参数,它的作用的是做模式的匹配,这点和keys的模式匹配很像。

    □ count number 是可选参数,它的作用是表明每次要遍历的键个数,默认值是10,此参数可以适当增大。

      现有一个Redis有26个键(英文26个字母),现在要遍历所有的键,使用scan 命令效果的操作如下。第一次执行scan 0,返回结果分为两个部分:第一个部分6就是下次scan 需要的cursor, 第二个部分是10个键:

    127.0.0.1:6379> scan 0
    1)  "6"
    2)  1)  "w"
         2)  "i"
         3)  "e"
         4)  "x"
         5)  "j"
         6)  "q"
         7)  "y"
         8)  "u"
         9)  "b"
        10) "o"
    使用新的cursor="6",执行scan 6127.0.0.1:6379> scan 6
    1)  "11"
    2)  1)  "h"
         2)  "n"
         3)  "m"
         4)  "t"
         5)  "c"
         6)  "d"
         7)  "g"
         8)  "p"
         9)  "z"
        10) "a"

      这次得到的cursor="11",继续执行scan 11 得到结果cursor变为0,说明所有的键已经被遍历过了.

    127.0.0.1:6379> scan 11
    1)  "0"
    2)   1)  "s"
         2)  "f"
         3)  "r"
         4)  "v"
         5)  "k"
         6)  "l"  

      除了scan以外,Redis提供了面向哈希类型、集合类型、有序集合的扫描遍历命令,解决诸如hgetall、smembers、zrange可能产生的阻塞问题,对应的命令分别是hscan、sscan、zscan, 它们的用法和scan 基本类似。

      渐进式遍历可以有效的解决keys命令可能产生的阻塞问题,但是scan 并非完美无瑕,如果在scan 的过程中如果有键的变化(增加、删除、修改),那么遍历效果可能会碰到如下问题:新增的键可能没有遍历到,遍历出了重复的键等情况,也就是说scan 并不能保证完整的遍历出来所有的键,这些是我们在开发时需要考虑的。

     

    3.3 数据库管理

      Redis提供了几个面Redis数据库的操作,它们分别是dbsize、select、flushdb/flushall命令,本节将通过具体的使用场景介绍这些命令。

    1.切换数据库

    select dblndex

      Redis有16个数据库,默认为0号。那么能不能像使用测试数据库和正式数据库一样,把正式的数据放在0号数据库,测试的数据库放在1号数据库,那么两者在数据上就不会彼此受影响了。事实真有那么好吗?Redis3.0中已经逐渐弱化这个功能,例如Redis的分布式实现Redis Cluster只允许使用0号数据库,只不过为了向下兼容老版本的数据库功能,该功能没有完全废弃掉,下面分析一下为什么要废弃掉这个“优秀”的功能呢?总结起来有三点:

    □ Redis是单线程的。如果使用多个数据库,那么这些数据库仍然是使用一个CPU,彼此之间还是会受到影响的。

    □ 多数据库的使用方式,会让调试和运维不同业务的数据库变的困难,假如有一个慢查询存在,依然会影响其他数据库,这样会使得别的业务方定位问题非常的困难。

    □ 部分Redis的客户端根本就不支持这种方式。即使支持,在开发的时候来回切换数字形式的数据库,很容易弄乱。

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

     

    2. flushdb/flushall

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

    例如当前0号数据库有四个键值对、1号数据库有三个键值对:

    127.0.0.1:6379> dbsize
    (integer) 4
    127.0.0.1:6379> select 1
    OK
    127.0.0.1:6379 [1]> dbsize
    (integer) 3

    如果在0号数据库执行flushdb,1号数据库的数据依然还在:

    127.0.0.1:6379> flushdb
    OK
    127.0.0.1:6379> dbsize
    (integer) 0
    127.0.0.1:6379> select 1
    OK
    127.0.0.1:6379[1]> dbsize
    (integer) 3

    在任意数据库执行flushall会将所有数据库清除:

    127.0.0.1:6379> flushall
    OK
    127.0.0.1:6379> dbsize
    (integer) 0
    127.0.0.1:6379> select 1
    OK
    127.0.0.1:6379[1]> dbsize
    (integer) 0

      flushdb/flushall命令可以非常方便的清理数据,但是也带来两个问题:

    □ flushdb/flushall命令会将所有数据清除,一旦误操作后果不堪设想.

    □ 如果当前数据库键值数量比较多,flushdb/flushall存在阻塞Redis的可能性。所以在使用flushdb/flushall—定要小心谨慎。

    作者:小家电维修

    相见有时,后会无期。

  • 相关阅读:
    java 字节流与字符流的区别
    什么是缓冲区
    java流输入输出
    Apache安装配置
    Maven学习
    Redis
    数据结构与算法
    pig ERROR 2997: Encountered IOException. File or directory null does not exist.
    hadoop学习路线(转)
    86标准SQL与92标准SQL用法区别
  • 原文地址:https://www.cnblogs.com/lizexiong/p/14691248.html
Copyright © 2011-2022 走看看