Redis(二)数据结构与键管理

  一、基础知识

  1.全局命令

  • keys *   :查看所有键
  • dbsize:返回当前数据库中键的总数
  • exists key:检查键是否存在
  • del key ... :删除键
  • expire key seconds:对键添加过期时间
  • ttl key:返回键的剩余过期时间(-1键没设置过期时间,-2键不存在)
  • type key:返回键的数据结构类型
bigjun@myubuntu:/$ redis-cli -h 127.0.0.1 -p 6379
127.0.0.1:6379> set hello world
OK
127.0.0.1:6379> set java jedis
OK
127.0.0.1:6379> set python redis-py
OK
127.0.0.1:6379> keys *
1) "hello"
2) "python"
3) "java"
4) "myname"
127.0.0.1:6379> dbsize
(integer) 4
127.0.0.1:6379> exists java
(integer) 1
127.0.0.1:6379> exists not_exist_key
(integer) 0
127.0.0.1:6379> del java
(integer) 1
127.0.0.1:6379> exists java
(integer) 0
127.0.0.1:6379> keys *
1) "hello"
2) "python"
3) "myname"
127.0.0.1:6379> del python myname
(integer) 2
127.0.0.1:6379> keys *
1) "hello"
127.0.0.1:6379> get hello
"world"
127.0.0.1:6379> expire hello 10
(integer) 1
127.0.0.1:6379> ttl hello
(integer) 5
127.0.0.1:6379> ttl hello
(integer) 3
127.0.0.1:6379> ttl hello
(integer) 2
127.0.0.1:6379> ttl hello
(integer) 1
127.0.0.1:6379> ttl hello
(integer) 0
127.0.0.1:6379> ttl hello
(integer) -2
127.0.0.1:6379> ttl hello
(integer) -2
127.0.0.1:6379> get hello
(nil)
127.0.0.1:6379> del hello
(integer) 0
127.0.0.1:6379> keys *
(empty list or set)
127.0.0.1:6379> set a b
OK
127.0.0.1:6379> type a
string
127.0.0.1:6379> rpush mylist a b c d e f g
(integer) 7
127.0.0.1:6379> type mylist
list
127.0.0.1:6379> type not_exsit_key
none
127.0.0.1:6379> shutdown nosave
not connected> 

  2.数据结构和内部编码

  使用type key命令可以返回当前键的数据结构类型,分别包括:string(字符串)、hash(哈希)、list(列表)、set(集合)、zset(有序集合)。

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

  

  可以通过object encoding命令查询内部编码:

127.0.0.1:6379> keys *
1) "a"
127.0.0.1:6379> rpush mylist a b c
(integer) 3
127.0.0.1:6379> object encoding a
"embstr"
127.0.0.1:6379> object encoding mylist
"quicklist"

  Redis这样设计有两个好处:

  • 可以改进内部编码,而对外的数据结构和命令没有影响。
  • 多种内部编码实现可以在不同场景下发挥各自的优势。

  3.单线程架构

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

  Redis客户端调用都经历了发送命令、执行命令、返回结果三个过程。

  

  因为Redis是单线程来处理命令的,所以一条命令从客户端到服务器端不会立刻被执行,所有命令都会进入一个队列中,然后逐个被执行,可以确定不会有两条命令被同时执行,不会产生并发问题,这就是Redis单线程的基本模型。

  为什么Redis使用单线程模型会达到每秒万级别的处理能力呢:

  • 纯内存访问,Redis将所有数据放在内存中,内存的相应时长大约为100纳秒,这是Redis达到每秒万级别访问的重要基础。
  • 非阻塞I/O,Redis使用epoll作为I/O多路复用技术的实现,在加上Redis自身的事件处理模型将epoll中的连接、读写、关闭都转换为事件,不在网络I/O上浪费过多的时间。
  • 单线程避免了线程切换和竟态产生的消耗。

  单线程能带来几个好处:

  • 单线程可以简化数据结构和算法的实现,并发数据结构实现不但困难而且开发测试比较麻烦。
  • 单线程避免了线程切换和竟态产生的消耗,对于服务端开发来说,锁和线程切换通常是性能杀手。

  单线程会有一个问题:

  对于每个命令的执行时间是有要求的,如果某个命令执行过长,会造成其他命令的阻塞,对于Redis这种高性能的服务来说是致命的,所以Redis是面向快速执行场景的数据库。

  二、字符串

  字符串类型是Redis最基础的数据结构。首先键都是字符串类型,值可以是字符串(简单的字符串、复杂的字符串(JSON、XML))、数字(整数、浮点数)、甚至是二进制(图片、音频、视频),但是值最大不能超多512MB。

  

  1.命令

  (1)常用命令

  • 设置值
set key [ex seconds] [px milliseconds] [nx|xx]
ex seconds:为键设置秒级过期时间
px milliseconds:为键设置毫秒级过期时间
nx:键必须不存在,才可以设置成功,用于添加
xx:与nx相反,键必须存在,才可以设置成功,用于更新

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

setex key seconds value:作用和ex选项一样
setnx key value:作用和nx选项一样

  先来验证nx和xx两个选项:

127.0.0.1:6379> exists hello
(integer) 0
127.0.0.1:6379> set hello world 
OK
127.0.0.1:6379> setnx hello redis
(integer) 0
127.0.0.1:6379> get hello
"world"
127.0.0.1:6379> set hello jedis xx
OK
127.0.0.1:6379> get hello
"jedis"

  由于键hello已存在,所以setnx失败,返回结果为0,而set xx成功,返回结果为OK。

  由于Redis的单线程命令处理机制,如果有多个客户端同时执行setnx key value,根据setnx特性只有一个客户端能设置成功,setnx可以作为分布式锁的一种实现方案。

  • 获取值(如果要获取的值不存在,则返回nil(空))
127.0.0.1:6379> get hello
"jedis"
127.0.0.1:6379> get not_exist_key
(nil)
  • 批量设置值
mset key value [key value...]

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

127.0.0.1:6379> mset a 1 b 2 c 3 d 4
OK
  • 批量获取值
127.0.0.1:6379> mget a b c d
1) "1"
2) "2"
3) "3"
4) "4"

  如果某些键不存在,则它的值为空:

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

  执行n次get命令的时间:n次get时间=n次网络时间+n次命令时间

  

  执行1次mget命令的时间:1次mget时间=1次网络时间+n次命令时间

  

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

  • 计数
incr key
用于对值做自增操作,返回结果分为三种情况:
1.值不是整数,返回错误。
2.值是整数,返回自增后的结果。
3.键不存在,按照值为0自增,返回结果为1。

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

(integer) 0
127.0.0.1:6379> incr key
(integer) 1
127.0.0.1:6379> incr key
(integer) 2
127.0.0.1:6379> incr hello
(error) ERR value is not an integer or out of range

  (2)不常用命令

  • 追加值:append key value
127.0.0.1:6379> get hello
"jedis"
127.0.0.1:6379> append hello Java
(integer) 9
127.0.0.1:6379> get hello
"jedisJava"
  • 字符串长度:strlen key
127.0.0.1:6379> get hello
"jedisJava"
127.0.0.1:6379> strlen hello
(integer) 9
  • 设置并返回原值:getset key value
127.0.0.1:6379> exists hello
(integer) 0
127.0.0.1:6379> getset hello world
(nil)
127.0.0.1:6379> getset hello redis
"world"
  • 设置指定位置的字符:setrange key offset value
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"
  • 获取部分字符串:getrange key start end
127.0.0.1:6379> getrange redis 0 1
"be"

  2.内部编码

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

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

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

  (1)整数类型:

127.0.0.1:6379> set int 8023
OK
127.0.0.1:6379> object encoding int
"int"

  (2)短字符串类型:

127.0.0.1:6379> set shortstring "hello world"
OK
127.0.0.1:6379> object encoding shortstring
"embstr"

  (3)场字符串类型:  

127.0.0.1:6379> set longstring "I am a string which has greater than 39 byte, you know?"
OK
127.0.0.1:6379> object encoding longstring
"raw"
127.0.0.1:6379> strlen longstring
(integer) 55

  3.使用场景

  (1)缓存功能

  比较典型的缓存使用场景是,Redis作为缓存层,MySQL作为存储层,绝大部分请求的数据都是从Redis中获取。

  

  由于Redis具有支撑高并发的特性,所以缓存通常能起到加速读写和降低后端压力的作用。

  例如这么一个场景:用户想要获取用户信息的话,首先需要根据用户提供的id,先去Redis中寻找用户信息,如果没有从Redis中获取到用户信息,那就需要从MySQL中进行获取,并将结果回写到Redis,添加1小时过期时间,如果这一个小时以内用户再次想获取信息的话,就直接从Redis中去获取到信息而不用再返回到MySQL中了。

UserInfo getUserInfo(long id){
  // 根据用户提供的id,定义Redis中键key的值   userRedisKey
= "user:info:" + id
  // 根据键key的值,从Redis中获取到对应的value值
  value = redis.get(userRedisKey);
  // 声明UserInfo类的对象变量   UserInfo userInfo;
  // 如果从Redis中获取到了value值   
if (value != null) {
    // 将Redis中存的value值反序列化为Java对象userInfo     userInfo
= deserialize(value);   } else {
    // 如果没有从Redis中获取到值,那么就从MySQL中去寻找     userInfo
= mysql.get(id);
    // 如果从MySQL中获取到了值     
if (userInfo != null)
      // 将从MySQL中获取到的值序列化并回写到Redis中并设置过期时间1小时       redis.setex(userRedisKey,
3600, serialize(userInfo));   }   return userInfo; }

  (2)计数

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

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

  (3)共享Session

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

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

  

  (4)限速

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

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

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

  三、哈希

  Redis中,哈希类型是指键值本身又是一个键值对结构,形如value={{field1,value1},...{fieldN,valueN}}。

  

  1.命令

  (1)设置值:hset key field value (设置成功返回1,反之返回0。和setnx命令一样有hsetnx命令)

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

  (2)获取值:hget key field

127.0.0.1:6379> hget user:1 name
"tom"
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...](返回成功删除field的个数)

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

  (4)计算field个数:hlen key

127.0.0.1:6379> hset user:1 name LianJiang
(integer) 1
127.0.0.1:6379> hset user:1 age 23
(integer) 1
127.0.0.1:6379> hset user:1 sex boy
(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 ...]

127.0.0.1:6379> hmset user:1 name QiaoJiang age 22 sex boy
OK
127.0.0.1:6379> hmget user:1 name age
1) "QiaoJiang"
2) "22"

  (6)判断field是否存在:hexists key field

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

  (7)获取所有field: hkey key

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

  (8)获取所有value:hvals key

127.0.0.1:6379> hvals user:1
1) "QiaoJiang"
2) "22"
3) "boy"

  (9)获取所有的field-value:hgetall key

127.0.0.1:6379> hgetall user:1
1) "name"
2) "QiaoJiang"
3) "age"
4) "22"
5) "sex"
6) "boy"

  (10)自增指定数字,自增指定浮点数:hincrby key field     hincrbyfloat key field

  (11)计算value的字符串长度:hstrlen key field

127.0.0.1:6379> hstrlen user:1 name
(integer) 9

  2.内部编码

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

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

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

  (3)当field个数超过512,内部编码也会有ziplist变为hashtable

  3.使用场景

  将关系型数据库和Redis哈希类型数据库做对比:

  

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

UserInfo getUserInfo(long id){
  // 用户 id 作为 key 后缀
  userRedisKey = "user:info:" + id;
  // 使用 hgetall 获取所有用户信息映射关系
  userInfoMap = redis.hgetAll(userRedisKey);
  UserInfo userInfo;
  if (userInfoMap != null) {
    // 将映射关系转换为 UserInfo
    userInfo = transferMapToUserInfo(userInfoMap);
  } else {
  // 从 MySQL 中获取用户信息
  userInfo = mysql.get(id);
  // 将 userInfo 变为映射关系使用 hmset 保存到 Redis 中
  redis.hmset(userRedisKey, transferUserInfoToMap(userInfo));
  // 添加过期时间
  redis.expire(userRedisKey, 3600);
  }
return userInfo;
}

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

  • 哈希类型是稀疏的,而关系型数据库是完全结构化的,例如哈希类型每个键可以有不同的field,而关系型数据库一旦添加新的列,所有行都要为其设置值(即使为NULL)。

  

  • 关系型数据库可以做复杂的关系查询,而Redis去模拟关系型复杂查询开发困难,维护成本高。

  总结一下三种缓存用户信息的方法并给出三种方案的实现方法和优缺点分析。

  • 原生字符串类型:每个属性一个键。
set user:1:name tom
set user:1:age 23
set user:1:city beijing

  优点:简单直观,每个属性都支持更新操作。
  缺点:占用过多的键,内存占用量较大,同时用户信息内聚性比较差,所以此种方案一般不会在生产环境使用。

  • 序列化字符串类型:将用户信息序列化后用一个键保存。
set user:1 serialize(userInfo)

  优点:简化编程,如果合理的使用序列化可以提高内存的使用效率。
  缺点:序列化和反序列化有一定的开销,同时每次更新属性都需要把全部数据取出进行反序列化,更新后再序列化到Redis中。

  • 哈希类型:每个用户属性使用一对field-value,但是只用一个键保存。
hmset user:1 name tomage 23 city beijing

  优点:简单直观,如果使用合理可以减少内存空间的使用。
  缺点:要控制哈希在ziplist和hashtable两种内部编码的转换,hashtable会消耗更多内存。

  四、列表

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

  

  列表类型有两个特点:

  • 第一,列表中的元素是有序的,这就意味着可以通过索引下标获取某个元素或者某个范围内的元素列表。
  • 第二,列表中的元素可以是重复的。

  1.命令

  (1)添加 rpush  lpush linsert

  • 从右边插入元素:rpush key value [value...](lrange listkey 0 -1命令可以从左到右获取列表的所有元素)
127.0.0.1:6379> rpush listkey c b a
(integer) 3
127.0.0.1:6379> lrange listkey 0 -1
1) "c"
2) "b"
3) "a"
  • 从左边插入元素:lpush key value [value...]
127.0.0.1:6379> lpush listkey d e f
(integer) 6
127.0.0.1:6379> lrange listkey 0 -1
1) "f"
2) "e"
3) "d"
4) "c"
5) "b"
6) "a"
  • 向某个元素前或后插入元素:linsert key before|after pivot value
127.0.0.1:6379> linsert listkey before b java
(integer) 7
127.0.0.1:6379> lrange listkey 0 -1
1) "f"
2) "e"
3) "d"
4) "c"
5) "java"
6) "b"
7) "a"

  (2)查找 lrange lindex lien

  • 获取指定范围内的元素列表:lrange key start end

  索引下标有两个特点:

  ①索引下标从左到右分别是0到N-1,但是从右到左分别是-1和-N。

  ②lrange中的end选项包含了自身,如果想落得第2到第4个元素,end的值就要为3

127.0.0.1:6379> lrange listkey 1 3
1) "e"
2) "d"
3) "c"
127.0.0.1:6379> lrange listkey 0 6
1) "f"
2) "e"
3) "d"
4) "c"
5) "java"
6) "b"
7) "a"
  • 获取列表指定索引下标的元素:lindex key index(-1表示最后一个元素)
127.0.0.1:6379> lindex listkey -1
"a"
  • 获取列表长度:llen key
127.0.0.1:6379> llen listkey
(integer) 7

  (3)删除 lpop rpop lrem ltrim

  • 从列表左侧弹出元素:lpop key
127.0.0.1:6379> lrange listkey 0 -1
1) "f"
2) "e"
3) "d"
4) "c"
5) "java"
6) "b"
7) "a"
127.0.0.1:6379> lpop listkey
"f"
127.0.0.1:6379> lrange listkey 0 -1
1) "e"
2) "d"
3) "c"
4) "java"
5) "b"
6) "a"
  • 从列表右侧弹出元素:rpop key
127.0.0.1:6379> rpop listkey
"a"
127.0.0.1:6379> lrange listkey 0 -1
1) "e"
2) "d"
3) "c"
4) "java"
5) "b"
  • 删除指定元素:lrem key count value
lrem key count value
从列表中找到等于value的元素进行删除,根据count的不同分为三种情况:
count>0,从左到右,删除最多count个元素
count<0,  从右到左,删除最多count绝对值个元素
count=0,删除所有

127.0.0.1:6379> lpush listkey a a a a a 
(integer) 10
127.0.0.1:6379> lrange listkey 0 -1
 1) "a"
 2) "a"
 3) "a"
 4) "a"
 5) "a"
 6) "e"
 7) "d"
 8) "c"
 9) "java"
10) "b"
127.0.0.1:6379> lrem listkey 4 a
(integer) 4
127.0.0.1:6379> lrange listkey 0 -1
1) "a"
2) "e"
3) "d"
4) "c"
5) "java"
6) "b"
  • 按照索引范围修剪列表:ltrim key start end
127.0.0.1:6379> lrange listkey 0 -1
1) "a"
2) "e"
3) "d"
4) "c"
5) "java"
6) "b"
127.0.0.1:6379> ltrim listkey 3 -1
OK
127.0.0.1:6379> lrange listkey 0 -1
1) "c"
2) "java"
3) "b"

  (4)修改 lset key index newValue

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

  (5)阻塞操作(通过timeout定义阻塞时间) blpop key [key ...] timeout      brpop key [key ...] timeout   

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

  在客户端1执行timeout=0时,会一直阻塞下去

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

  这个时候在客户端2添加了数据element1

127.0.0.1:6379> rpush list:test element1
(integer) 1

  客户端2添加完之后客户端1立即有返回:

127.0.0.1:6379> brpop list:test 3
(nil)
(3.06s)
127.0.0.1:6379> brpop list:test 0
1) "list:test"
2) "element1"
(71.97s)
  • 列表不为空,客户端会理解返回:
127.0.0.1:6379> rpush list:test element1
(integer) 1
127.0.0.1:6379> brpop list:test 0
1) "list:test"
2) "element1"

  2.内部编码

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

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

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

  (3)当某个元素超过64字节,内部编码也会变为linkedlist

  3.使用场景

  列表的使用场景很多,可以记住这些公式:

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

  (1)消息队列

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

  

  (2)文章列表

  • 每篇文章使用哈希结构存储,例如每篇文章有3个属性title、timestamp、content:
hmset acticle:1 title xx timestamp 1476536196 content xxxx
...
hmset acticle:k title yy timestamp 1476512536 content yyyy
  • 向用户文章列表添加文章,user:{id}:articles作为用户文章列表的键:
lpush user:1:acticles article:1 article3
...
lpush user:k:acticles article:5
...
  • 分页获取用户文章列表,例如下面伪代码获取用户id=1的前10篇文章:
articles = lrange user:1:articles 0 9
for article in {articles}
  hgetall {article}

  使用列表类型保存和获取文章列表会存在两个问题。

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

  五、集合

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

  

  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> smembers myset
1) "a"
2) "b"
3) "c"
127.0.0.1:6379> sadd myset a b
(integer) 0
  • 删除元素:srem key element [element ...]
127.0.0.1:6379> sadd myset a b
(integer) 0
127.0.0.1:6379> srem myset a b
(integer) 2
127.0.0.1:6379> srem myset a 
(integer) 0
127.0.0.1:6379> smembers myset
1) "c"
  • 计算元素个数:scard key
127.0.0.1:6379> smembers myset
1) "c"
127.0.0.1:6379> scard myset
(integer) 1
  • 判断元素是否在集合中:sismember key element
127.0.0.1:6379> sismember myset c
(integer) 1
127.0.0.1:6379> sismember myset a
(integer) 0
  • 随机从集合返回指定个数元素:srandmember key [count]
127.0.0.1:6379> sadd myset a b d e f g
(integer) 6
127.0.0.1:6379> smembers myset
1) "c"
2) "d"
3) "b"
4) "e"
5) "a"
6) "g"
7) "f"
127.0.0.1:6379> srandmember myset 3
1) "c"
2) "f"
3) "g"
127.0.0.1:6379> srandmember myset 3
1) "a"
2) "c"
3) "g"
  • 从集合随机弹出元素:spop key [count]
127.0.0.1:6379> smembers myset
1) "c"
2) "d"
3) "b"
4) "e"
5) "a"
6) "g"
7) "f"
127.0.0.1:6379> spop myset 1
1) "d"
127.0.0.1:6379> spop myset 2
1) "f"
2) "e"
127.0.0.1:6379> smembers myset
1) "b"
2) "a"
3) "c"
4) "g"
  • 获取所有元素:smembers key
127.0.0.1:6379> smembers myset
1) "b"
2) "a"
3) "c"
4) "g"

  (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
  • 求多个集合的交集:sinter   key [key ...]
127.0.0.1:6379> sinter user:1:follow user:2:follow
1) "it"
2) "sports"
  • 求多个集合的并集:suinon key [key ...]
127.0.0.1:6379> sunion user:1:follow user:2:follow
1) "ent"
2) "his"
3) "it"
4) "news"
5) "music"
6) "sports"
  • 求多个集合的差集:sdiff     key [key ...]
127.0.0.1:6379> sdiff user:1:follow user:2:follow
1) "his"
2) "music"
127.0.0.1:6379> sdiff user:2:follow user:1:follow
1) "ent"
2) "news"
  • 将交集、并集、差集的结果保存 sinsterstore|suionstore|sdiffstore destination key [key ...]
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.内部编码

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

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

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

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

  3.使用场景

  集合类型的应用场景通常分为几下几种:

  • sadd = Tagging(标签)
  • spop/srandmember = Random item(生成随机数,比如抽奖)
  • sadd + sinter = Social Graph(社交需求)

  其中使用最多的场景是标签(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
...

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

sinter user:1:tags user:2:tags

  六、有序集合

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

  

  列表、集合和有序集合三者的异同点:

  

  1.命令

  (1)集合内操作

  • 添加成员:zadd key [NX|XX] [CH] [INCR] score member [score member ...]
zadd key [NX|XX] [CH] [INCR] score member [score member ...]
nx:member必须不存在,才可以设置成功,用于添加
xx:member必须存在,才可以设置成功,用于更新
ch:返回此次操作后,有序集合元素和分数发生变化的个数
incr:对score做增加,相当于zincrby

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
  • 计算成员个数:zcard key
127.0.0.1:6379> zcard user:ranking
(integer) 6
  • 计算某个成员的分数:zscore key member
127.0.0.1:6379> zscore user:ranking tom
"251"
127.0.0.1:6379> zscore user:ranking not_exist_member
(nil)
  • 计算成员的排名:zrank key member(从低到高) | zrevrank key member(从高到低)
127.0.0.1:6379> zrank user:ranking tom
(integer) 5
127.0.0.1:6379> zrevrank user:ranking tom
(integer) 0
  • 删除成员:zrem key member [member ...]
127.0.0.1:6379> zrem user:ranking mike
(integer) 1
  • 增加成员的分数:zincrby key increment member
127.0.0.1:6379> zscore user:ranking tom
"251"
127.0.0.1:6379> zincrby user:ranking 9 tom
"260"
  • 返回指定排名范围的成员:zrange|zrevrange key start end [withscores]
127.0.0.1:6379> zrange user:ranking 0 -1 withscores
 1) "kris"
 2) "1"
 3) "frank"
 4) "200"
 5) "tim"
 6) "220"
 7) "martin"
 8) "250"
 9) "tom"
10) "260"
127.0.0.1:6379> zrevrange user:ranking 0 3 withscores
1) "tom"
2) "260"
3) "martin"
4) "250"
5) "tim"
6) "220"
7) "frank"
8) "200"
127.0.0.1:6379> zrevrange user:ranking 0 3 
1) "tom"
2) "martin"
3) "tim"
4) "frank"
  • 返回指定范围分数的成员:zrangebyscore key min max [withscores] [limit offset count]    |    zrevrange key max min [withscores] [limit offset count] (+inf代表无穷大,-inf代表无穷小)
127.0.0.1:6379> zrangebyscore user:ranking 200 221 withscores
1) "frank"
2) "200"
3) "tim"
4) "220"
127.0.0.1:6379> zrangebyscore user:ranking 200 220 withscores
1) "frank"
2) "200"
3) "tim"
4) "220"
127.0.0.1:6379> zrangebyscore user:ranking 200 219 withscores
1) "frank"
2) "200"
127.0.0.1:6379> zrevrangebyscore user:ranking 260 200 withscores
1) "tom"
2) "260"
3) "martin"
4) "250"
5) "tim"
6) "220"
7) "frank"
8) "200"
127.0.0.1:6379> zrevrangebyscore user:ranking 260 201 withscores
1) "tom"
2) "260"
3) "martin"
4) "250"
5) "tim"
6) "220"
127.0.0.1:6379> zrevrangebyscore user:ranking 260 -inf withscores
 1) "tom"
 2) "260"
 3) "martin"
 4) "250"
 5) "tim"
 6) "220"
 7) "frank"
 8) "200"
 9) "kris"
10) "1"
  • 返回指定分数范围成员个数:zcount key min max
127.0.0.1:6379> zcount user:ranking 200 220
(integer) 2
  • 删除指定排名内的升序元素:zremrangebyrank key start end
127.0.0.1:6379> zrange user:ranking 0 -1 withscores
 1) "kris"
 2) "1"
 3) "frank"
 4) "200"
 5) "tim"
 6) "220"
 7) "martin"
 8) "250"
 9) "tom"
10) "260"
127.0.0.1:6379> zremrangebyrank user:ranking 0 2
(integer) 3
127.0.0.1:6379> zrange user:ranking 0 -1 withscores
1) "martin"
2) "250"
3) "tom"
4) "260"
  • 删除指定分数范围的成员:zremrangebyscore key min max
127.0.0.1:6379> zrange user:ranking 0 -1 withscores
1) "martin"
2) "250"
3) "tom"
4) "260"
127.0.0.1:6379> zremrangebyscore user:ranking 250 +inf
(integer) 2
127.0.0.1:6379> zrange user:ranking 0 -1 withscores
(empty list or set)

  (2)集合间操作

  首先定义两个有序集合:

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
127.0.0.1:6379> zrange user:ranking:1 0 -1 withscores
 1) "kris"
 2) "1"
 3) "mike"
 4) "91"
 5) "frank"
 6) "200"
 7) "tim"
 8) "220"
 9) "martin"
10) "250"
11) "tom"
12) "251"
127.0.0.1:6379> zrange user:ranking:2 0 -1 withscores
1) "james"
2) "8"
3) "mike"
4) "77"
5) "martin"
6) "625"
7) "tom"
8) "888"
  • 交集
zinterstore destination numkeys key [key ...] [weights weight [weight ...]][aggregate sum|min|max]
destination :交集计算结果保存到这个键
numkeys:需要做交集计算键的个数
key [key ...]:需要做交集计算的键
weights weight [weight ...]:每个键的权重,在做交集计算时,每个键中的每个member会将自己分数乘以这个权重,每个键的权重默认是1
aggregate sum|min|max:计算成员交集后,分值可以按照sum(和)、min(最小值)、max(最大值)做汇总,默认值是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"

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"

  • 并集(和交集所有参数一样)
127.0.0.1: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.内部编码

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

  (1)当元素较少且每个元素较小时,内部编码为ziplist

  (2)当元素个数超过128个,内部编码变为skiplist

  (3)当某个元素大于64字节时,内部编码变为hashtable

  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)展示获赞数最多的十个用户

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

  七、键管理

  1.单个键管理

  • 键重命名:rename key newkey
127.0.0.1:6379> get hello
"world"
127.0.0.1:6379> rename hello redis
OK
127.0.0.1:6379> get redis
"world"
127.0.0.1:6379> get hello
(nil)

如果在rename之前,键已经存在,那么它的值将被覆盖:
127.0.0.1:6379> get redis
"world"
127.0.0.1:6379> get java
"jedis"
127.0.0.1:6379> rename redis java
OK
127.0.0.1:6379> get java
"world"
127.0.0.1:6379> gert redis
(error) ERR unknown command 'gert'
127.0.0.1:6379> get redis
(nil)

为了防止被强行rename,可以使用renamenx来确保只有newKey不存在时才被覆盖:
127.0.0.1:6379> get java
"world"
127.0.0.1:6379> get python
"redis-py"
127.0.0.1:6379> renamenx java python
(integer) 0
127.0.0.1:6379> get java
"world"
  • 随机返回一个键:randomkey
127.0.0.1:6379> keys *
1) "python"
2) "java"
127.0.0.1:6379> randomkey
"python"
127.0.0.1:6379> randomkey
"java"
127.0.0.1:6379> randomkey
"python"
127.0.0.1:6379> randomkey
"python"
  • 键过期

  ①expire key seconds:键在seconds秒后过期

  ②expireat key timastamp:键在秒级时间戳timestamp后过期

  ③pexpire key milliseconds:键在milliseconds毫秒后过期

  ④pexpireat key milliseconds-timestamp:键在毫秒时间戳timestamp后过期

  • 键迁移

  ①move key db

  ②dump key + restore key ttl value

  ③migrate host port key| destination-db timeout [COPY] [REPLACE] [KEYS key]

  2.遍历键

  • 全局遍历键:keys pattern
127.0.0.1:6379> dbsize
(integer) 0
127.0.0.1:6379> mset hello world redis test jedis best hill high
OK
127.0.0.1:6379> keys *
1) "redis"
2) "hill"
3) "jedis"
4) "hello"
127.0.0.1:6379> keys [j,r]edis
1) "redis"
2) "jedis"
127.0.0.1:6379> keys h?ll*
1) "hill"
2) "hello"

  考虑到Redis的单线程架构,如果Redis包含了大量的键,执行keys命令很可能会造成Redis阻塞,所以一般建议不要在生产环境中使用keys命令。在一下三种情况下,可以使用:

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

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

  ③可以使用scan命令渐进式地遍历所有键,可以有效防止阻塞。

  • 渐进式遍历:scan cursor [match pattern] [count number]

  Redis存储键值对实际使用的是hashtable的数据结构:

  

  每次执行scan,只扫描一个字典中的一部分键,直到将字典中的所有键遍历完毕:

  

scan cursor [match pattern] [count number]
cursor:必需参数,实际上cursor是一个游标,第一次遍历从0开始,每次scan遍历完都会返回当前游标的值,直到游标值为0,表示遍历结束。
match pattern:可选参数,它的作用的是做模式的匹配,这点和keys的模式匹配很像。
count number:可选参数,它的作用是表明每次要遍历的键个数,默认值是10,此参数可以适当增大。

127.0.0.1:6379> keys *
 1) "s"
 2) "u"
 3) "d"
 4) "h"
 5) "y"
 6) "b"
 7) "q"
 8) "p"
 9) "r"
10) "i"
11) "m"
12) "z"
13) "f"
14) "w"
15) "c"
16) "n"
17) "e"
18) "a"
19) "k"
20) "v"
21) "j"
22) "x"
23) "l"
24) "o"
25) "t"
26) "g"

127.0.0.1:6379> scan 0
1) "17"
2)  1) "s"
    2) "e"
    3) "b"
    4) "q"
    5) "f"
    6) "h"
    7) "m"
    8) "t"
    9) "u"
   10) "d"
127.0.0.1:6379> scan 17
1) "7"
2)  1) "a"
    2) "v"
    3) "j"
    4) "p"
    5) "r"
    6) "l"
    7) "o"
    8) "y"
    9) "z"
   10) "x"
127.0.0.1:6379> scan 7
1) "0"
2) 1) "i"
   2) "k"
   3) "w"
   4) "c"
   5) "n"
   6) "g"

  3.数据库管理

  • 切换数据库:select dbIndex

  MySql这种关系型数据库支持在一个实例下有多个数据库存在的,但是与关系型数据库用字符来区分不同数据库名不同,Redis只是用数字作为多个数据库的实现。

  Redis默认配置中有16个数据库,在redis.conf中有databases 16定义了默认有16个数据库。

  当使用redis-cli -h {ip} -p {port}时默认使用的就是0号数据库,最后一个数据库是15号数据库,不同数据库之间的数据没有任何关联,甚至可以存在相同的键。

127.0.0.1:6379> set hello world
OK
127.0.0.1:6379> get hello
"world"
127.0.0.1:6379> select 15
OK
127.0.0.1:6379[15]> get hello
(nil)

  为什么Redis最好是只使用0号数据库,不提倡使用多个数据库:

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

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

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

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

  • 清除数据库:flushdb(清除当前数据库)/flushall(清除所有数据库)
127.0.0.1:6379> flushdb
OK
127.0.0.1:6379> dbsize
(integer) 0

127.0.0.1:6379> set hello world
OK
127.0.0.1:6379> select 1
OK
127.0.0.1:6379[1]> set redis jedis
OK
127.0.0.1:6379[1]> flushall
OK
127.0.0.1:6379[1]> get redis
(nil)
127.0.0.1:6379[1]> select 0
OK
127.0.0.1:6379> get hello
(nil)

  

原文地址:https://www.cnblogs.com/BigJunOba/p/9115061.html