Redis03-API的理解和使用
1、Redis基础
1.1、redis-cli操作redis的方式
- redis-cli有两种方式连接Redis服务器,并进行数据操作。
- 第一种方式是将命令作为redis-cli的参数执行(命令行方式)。
- 第二种方式是不带参数运行redis-cli,进入交互模式(交互式方式)。
1 2 3 4 | redis-cli [OPTIONS] [cmd [arg [arg ...]]] -h <hostname> 服务器IP( default : 127.0.0.1). -p <port> 服务端口( default : 6379). -a <password> 客户端连接redis服务的密码 |
示例1:交互式方式
1 2 3 4 5 6 | //等价于:redis-cli -h 127.0.0.1 -p 6379 ]# redis-cli //远程连接redis ]# redis-cli -h 10.1.1.11 -p 6379 //远程连接redis,且redis有密码 ]# redis-cli -h 10.1.1.11 -p 6379 -a hengha123 |
示例2:命令行方式
1 2 3 4 5 6 | //等价于:redis-cli -h 127.0.0.1 -p 6379 PING ]# redis-cli PING //远程连接redis ]# redis-cli -h 10.1.1.11 -p 6379 PING //远程连接redis,且redis有密码 ]# redis-cli -h 10.1.1.11 -p 6379 -a hengha123 PING |
1.2、redis-cli全局命令
- Redis有5种数据结构,它们是键值对中的值,对于键来说有一些通用的命令。
- Redis命令不区分大小写。
1、授权认证
1 | AUTH requirepass_value |
示例:配置文件中requirepass指令的值
1 2 3 | ]# redis-cli -h 10.1.1.11 -p 6379 10.1.1.11:6001> AUTH hengha123 OK |
2、查看符合模式的键名
1 | KEYS pattern |
- pattern支持glob风格通配符格式:
-
注意,keys命令会遍历所有键,它的时间复杂度是O(n),当Redis保存了大量的键时,不建议在生产环境中使用。
示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 | //面插入了三个字符串类型的键值对 10.1.1.11:6379> SET hello world OK 10.1.1.11:6379> SET java jedis OK 10.1.1.11:6379> SET python redis-py OK //查看所有键 10.1.1.11:6379> KEYS * 1) "python" 2) "java" 3) "hello" |
3、查看键的总数量
- dbsize命令会返回当前数据库中键的总数。
1 | DBSIZE |
- dbsize命令在计算键总数时不会遍历所有键,而是直接获取Redis内置的键总数变量,所以dbsize命令的时间复杂度是O(1)。
示例:
1 2 3 | //当前数据库有3个键,分别是hello、java、python,所以dbsize的结果是3 10.1.1.11:6379> DBSIZE (integer) 3 |
4、检查键是否存在
- 如果键存在则返回1,不存在则返回0。
1 | EXISTS key |
示例:
1 2 3 4 5 6 7 8 9 | 10.1.1.11:6379> KEYS * 1) "python" 2) "java" 3) "hello" 10.1.1.11:6379> EXISTS hello (integer) 1 10.1.1.11:6379> EXISTS hello3 (integer) 0 |
5、删除键
- 删除一个或多个键,返回值是删除的键的个数。
1 | DEL key1 [key2 ...] |
示例:
- 第二次执行DEL命令时,因为hello键已经被删除了,实际上并没有删除任何键,所以返回0。
1 2 3 4 | 10.1.1.11:6379> DEL hello (integer) 1 10.1.1.11:6379> DEL hello (integer) 0 |
- DEL命令的参数不支持通配符,但可以结合Linux的管道和xargs命令实现删除所有符合规则的键。
1 2 3 4 | //比如要删除所有以“user:”开头的键 redis-cli KEYS "user:*" | xargs redis-cli DEL //由于DEL命令支持多个键作为参数,所以还可以执行来达到同样的效果,但是性能更好。 redis-cli DEL $(redis-cli KEYS "user:*" ) |
6、查看键值的数据类型
- TYPE命令用来获得键值的数据类型,返回值可能是string(字符串类型)、hash(散列类型)、list(列表类型)、set(集合类型)、zset(有序集合类型)。
1 | TYPE key |
示例:
1 2 3 4 5 6 7 | 10.1.1.11:6379> KEYS * 1) "python" 2) "java" //查看键值的数据类型 10.1.1.11:6379> TYPE python string |
7、设置键的过期时间
- Redis可以对键添加过期时间。当超过过期时间后,会自动删除键
1 | EXPIRE key seconds |
- ttl命令会返回键的剩余过期时间,它有3种返回值:
- 大于等于0的整数:键剩余的过期时间。
- -1:键没设置过期时间。
- -2:键不存在
示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | 10.1.1.11:6379> KEYS * 1) "java" 2) "python" //查看键的过期时间,java键没有设置过期时间 10.1.1.11:6379> ttl java (integer) -1 //设置java键过期时间为15秒 10.1.1.11:6379> EXPIRE java 15 (integer) 1 //查看键的过期时间,java键还可以存在10秒 10.1.1.11:6379> ttl java (integer) 10 //查看键的过期时间,java键不存在 10.1.1.11:6379> ttl java (integer) -2 |
8、设置和查看配置参数(配置文件)
1 2 3 4 | CONFIG GET <pattern> #返回与全局参数匹配的参数和其值 CONFIG SET <parameter> <value> #设置参数的值为value CONFIG REWRITE #将配置持久化到本地配置文件 CONFIG RESETSTAT #清除INFO上报的统计信息 |
1.3、Redis数据结构和内部编码
- type命令实际返回的就是当前键的数据结构类型,它们分别是:string(字符串)、hash(哈希)、list(列表)、set(集合)、zset(有序集合),但这些只是Redis对外的数据结构,如图2-1所示。
- 实际上每种数据结构都有自己底层的内部编码实现,而且是多种实现,这样Redis会在合适的场景选择合适的内部编码,如图2-2所示。
- 每种数据结构都有两种以上的内部编码实现,例如list数据结构包含了linkedlist和ziplist两种内部编码。同时有些内部编码可以作为多种外部数据结构的内部实现,如ziplist。
- 可以通过object encoding命令查询键的内部编码。
- Redis这样设计有两个好处:
- (1)可以改进内部编码,而对外的数据结构和命令没有影响,这样一旦开发出更优秀的内部编码,无需改动外部数据结构和命令,例如Redis3.2提供了quicklist,结合了ziplist和linkedlist两者的优势,为列表类型提供了一种更为优秀的内部编码实现,而对外部用户来说基本感知不到。
- (2)多种内部编码实现可以在不同场景下发挥各自的优势,例如ziplist比较节省内存,但是在列表元素比较多的情况下,性能会有所下降,这时候Redis会根据配置选项将列表类型的内部实现转换为linkedlist。
示例:
1 2 3 4 5 6 7 8 9 | 10.1.1.11:6379> KEYS * 1) "python" //查看键值的数据类型 10.1.1.11:6379> TYPE python string //查看键的内部编码 10.1.1.11:6379> OBJECT ENCODING python "embstr" |
1.4、Redis使用单线程架构
- Redis使用了单线程架构和I/O多路复用模型来实现高性能的内存数据库服务。
1、Redis单线程基础原理
- Redis客户端与服务端的交互过程可以简化成图2-3,每次客户端调用都经历了发送命令、执行命令、返回结果三个过程。
- Redis是使用单线程处理请求的,所以一个命令从客户端达到服务端时不会立刻被执行,而是进入一个队列中,然后排队等待被执行。如果有两个客户端在同一时刻向一个服务端发送两条命令,这两条命令的执行顺序是不确定的,但一定不会同时被执行(如图2-4所示)。
2、为什么使用单线程还能这么快
- 通常来讲,单线程处理能力要比多线程差。
- 那么为什么Redis使用单线程模型会达到每秒万级别的处理能力呢?可以将其归结为三点:
- (1)纯内存存储,Redis将所有数据放在内存中,内存的响应时长大约为100纳秒,这是Redis达到每秒万级别访问的重要基础。
- (2)非阻塞I/O,Redis使用epoll作为I/O多路复用技术的实现,再加上Redis自身的事件处理模型将epoll中的连接、读写、关闭都转换为事件,不在网络I/O上浪费过多的时间,如图2-6所示。
- (3)单线程避免了线程切换和竞态产生的消耗。
- 既然采用单线程就能达到如此高的性能,那么也不失为一种不错的选择,因为单线程能带来几个好处:
- (1)单线程可以简化数据结构和算法的实现。如果对高级编程语言熟悉的读者应该了解并发数据结构实现不但困难而且开发测试比较麻烦。
- (2)单线程避免了线程切换和竞态产生的消耗,对于服务端开发来说,锁和线程切换通常是性能杀手。
- 但是单线程会有一个问题:对于每个命令的执行时间是有要求的。如果某个命令执行过长,会造成其他命令的阻塞,对于Redis这种高性能的服务来说是致命的,所以Redis是面向快速执行场景的数据库。
- 单线程机制很容易被初学者忽视,但笔者认为Redis单线程机制是开发和运维人员使用和理解Redis的核心之一。
2、字符串类型
- 字符串类型是Redis最基础的数据类型。
- 首先键都是字符串类型,而且其他几种数据结构都是在字符串类型基础上构建的,所以字符串类型能为其他四种数据结构的学习奠定基础。
- 如图2-7所示,字符串类型的值实际可以是字符串(简单的字符串、复杂的字符串(例如JSON、XML))、数字(整数、浮点数),甚至是二进制(图片、音频、视频),但是值最大不能超过512MB。
2.1、命令
- 表2-2是字符串类型命令及其时间复杂度,开发人员可以参考此表,结合自身业务需求和数据大小选择适合的命令。
2.1.1、常用命令
1、设置值
1 2 3 4 5 6 7 8 | SET value [EX seconds|PX milliseconds|KEEPTTL] [NX|XX] ex seconds:为键设置秒级过期时间。 px milliseconds:为键设置毫秒级过期时间。 nx:键必须不存在,才可以设置成功,用于添加。 xx:与nx相反,键必须存在,才可以设置成功,用于更新。 SETEX key seconds value #和ex选项是一样 SETNX key value #和nx选项是一样 |
- setnx和set xx在实际使用中有什么应用场景吗?
- 以setnx命令为例,由于Redis的单线程命令处理机制,如果有多个客户端同时执行setnx key value,根据setnx的特性只有一个客户端能设置成功。
- setnx可以作为分布式锁的一种实现方案,Redis官方给出了使用setnx实现分布式锁的方法:http://redis.io/topics/distlock。
示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | 10.1.1.11:6379> keys * ( empty array ) //键必须存在,才可以设置成功,用于更新。此时hello键不存在,因此设置不成功 10.1.1.11:6379> set hello world xx (nil) //键必须不存在,才可以设置成功,用于添加。 10.1.1.11:6379> set hello world nx OK 10.1.1.11:6379> set hello world1 xx OK 10.1.1.11:6379> set hello world2 nx (nil) |
2、获取值
1 | GET key |
示例:
1 2 3 4 5 6 7 8 9 | 10.1.1.11:6379> keys * 1) "hello" //如果要获取的键存在,则返回其值 10.1.1.11:6379> get hello "world1" //如果要获取的键不存在,则返回nil(空) 10.1.1.11:6379> get hello1 (nil) |
3、批量设置值
1 | MSET key value [key value ...] |
示例:
1 2 | 10.1.1.11:6379> mset a 1 b 2 c 3 OK |
4、批量获取值
1 | MGET key [key ...] |
- 批量操作命令可以有效提高开发效率:
1 2 3 4 5 | //执行n次get命令需要按照图2-8的方式来执行,具体耗时如下 n次get时间 = n次网络时间 + n次命令时间 //使用mget命令后,要执行n次get命令操作只需要按照图2-9的方式来完成,具体耗时如下 n次get时间 = 1次网络时间 + n次命令时间 |
- 使用批量操作,有助于提高业务处理效率,但是要注意的是每次批量操作所发送的命令数不是无节制的,如果数量过多可能造成Redis阻塞或者网络拥塞。
示例:
1 2 3 4 5 6 7 8 9 10 11 12 | 10.1.1.11:6379> keys * 1) "b" 2) "a" 3) "c" 4) "hello" //如果有些键不存在,那么它的值为nil(空),结果是按照传入键的顺序返回。 10.1.1.11:6379> mget a b c e 1) "1" 2) "2" 3) "3" 4) (nil) |
5、计数
1 2 3 4 5 6 | INCR key #自增1 INCRBY key increment #自增指定数字 INCRBYFLOAT key increment #自增浮点数 DECR key #自减1 DECRBY key decrement #自减指定数字 |
- incr命令用于对值做自增操作,返回结果有三种情况:
- 值不是整数,返回错误。
- 值是整数,返回自增后的结果。
- 键不存在,按照值为0自增,返回结果为1。
- 很多存储系统和编程语言内部使用CAS机制实现计数功能,会有一定的CPU开销,但在Redis中完全不存在这个问题,因为Redis是单线程架构,任何命令到了Redis服务端都要顺序执行。
示例:
1 2 3 4 5 6 7 8 9 | 10.1.1.11:6379> exists int (integer) 0 10.1.1.11:6379> incr int (integer) 1 10.1.1.11:6379> incr int (integer) 2 10.1.1.11:6379> get int "2" |
2.1.2、不常用命令
1、追加值
1 | APPEND key value |
示例:
1 2 3 4 5 6 7 | 10.1.1.11:6379> get hello "world1" 10.1.1.11:6379> append hello 23 (integer) 8 10.1.1.11:6379> get hello "world123" |
2、字符串长度
1 | STRLEN key |
示例:
1 2 3 4 5 6 7 8 9 10 | 10.1.1.11:6379> get hello "world123" 10.1.1.11:6379> strlen hello (integer) 8 //每个中文占用3个字节 10.1.1.11:6379> set hello "世界" OK 10.1.1.11:6379> strlen hello (integer) 6 |
3、设置并返回原值
1 | GETSET key value |
- getset和set一样会设置值,但是不同的是,它同时会返回键原来的值。
示例:
1 2 3 4 5 6 7 | 10.1.1.11:6379> get hello "world" 10.1.1.11:6379> getset hello hi "world" 10.1.1.11:6379> get hello "hi" |
4、设置指定位置的字符
- 从指定的位置开始修改指定的字节。
1 | SETRANGE key offset value |
示例:
1 2 3 4 5 6 7 8 | 10.1.1.11:6379> get hello "world" //从0开始修改三个字符 10.1.1.11:6379> setrange hello 0 WOR (integer) 5 10.1.1.11:6379> get hello "WORld" |
5、获取部分字符串
1 | GETRANGE key start end |
示例:
1 2 3 4 | 10.1.1.11:6379> get hello "WORld" 10.1.1.11:6379> getrange hello 2 3 "Rl" |
2.2、内部编码
- 字符串类型的内部编码有3种:
- int:8个字节的长整型。
- embstr:小于等于44个字节的字符串。
- raw:大于44个字节的字符串。
- Redis会根据当前值的类型和长度决定使用哪种内部编码实现。
示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | //整数类型 10.1.1.11:6379> set key 123 OK 10.1.1.11:6379> object encoding key "int" //短字符串 10.1.1.11:6379> set key abc OK 10.1.1.11:6379> object encoding key "embstr" //长字符串 10.1.1.11:6379> set key "one string greater than 45 byte............." OK 10.1.1.11:6379> strlen key (integer) 45 10.1.1.11:6379> object encoding key "raw" |
2.3、典型使用场景
1、缓存功能
- 图2-10是比较典型的缓存使用场景,其中Redis作为缓存层,MySQL作为存储层,绝大部分请求的数据都是从Redis中获取。由于Redis具有支撑高并发的特性,所以缓存通常能起到加速读写和降低后端压力的作用。
- 与MySQL等关系型数据库不同的是,Redis没有命令空间,而且也没有对键名有强制要求(除了不能使用一些特殊字符)。但设计合理的键名,有利于防止键冲突和项目的可维护性,比较推荐的方式是使用“业务名:对象名:id:[属性]”作为键名(也可以不是分号)。例如MySQL的数据库名为vs,用户表名为user,那么对应的键可以用"vs:user:1"。
- 下面伪代码模拟了图2-10的访问过程:
- (1)该函数用于获取用户的基础信息
1 2 3 | UserInfo getUserInfo(long id){ ... } |
-
- (2)首先从Redis获取用户信息
1 2 3 4 5 6 7 8 9 | //定义键 userRedisKey = "user:info:" + id; //从Redis获取值 value = redis.get(userRedisKey); if (value != null) { //将值进行反序列化为UserInfo并返回结果 userInfo = deserialize(value); return userInfo; } |
-
- (3)如果没有从Redis获取到用户信息,需要从MySQL中进行获取,并将结果回写到Redis,添加1小时(3600秒)过期时间
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | //从MySQL获取用户信息 userInfo = mysql.get(id); //将userInfo序列化,并存入Redis redis.setex(userRedisKey, 3600, serialize(userInfo)); //返回结果 return userInfo UserInfo getUserInfo(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:
1 2 3 4 | long incrVideoCounter(long id) { key = "video:playCount:" + id; return redis.incr(key); } |
- 开发提示
- 实际上一个真实的计数系统要考虑的问题会很多:防作弊、按照不同维度计数,数据持久化到底层数据源等。
3、共享Session
- 如图2-11所示,一个分布式Web服务将用户的Session信息(例如用户登录信息)保存在各自服务器中,这样会造成一个问题,出于负载均衡的考虑,分布式服务会将用户的访问均衡到不同服务器上,用户刷新一次访问可能会发现需要重新登录,这个问题是用户无法容忍的。
- 为了解决这个问题,可以使用Redis将用户的Session进行集中管理,如图2-12所示,在这种模式下只要保证Redis是高可用和扩展性的,每次用户更新或者查询登录信息都直接从Redis中集中获取。
4、限速
- 很多应用出于安全的考虑,会在每次进行登录时,让用户输入手机验证码,从而确定是否是用户本人。但是为了短信接口不被频繁访问,会限制用户每分钟获取验证码的频率,例如一分钟不能超过5次。
- 此功能可以使用Redis来实现,下面的伪代码给出了基本实现思路:
1 2 3 4 5 6 7 8 9 | 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次也可以采用类似的思路。
3、哈希
- 几乎所有的编程语言都提供了哈希(hash)类型,它们的叫法可能是哈希、字典、关联数组。
- 在Redis中,哈希类型的值本身又是键值对,形如value={{field1,value1},...{fieldN,valueN}},Redis键值对和哈希类型二者的关系可以用图2-14来表示。
- 哈希类型中的映射关系叫作field-value,注意这里的value是指field对应的值,不是键对应的值,请注意value在不同上下文的作用。
3.1、命令
- 表2-3是哈希类型命令及其时间复杂度,开发人员可以参考此表选择适合的命令。
1、设置值
1 2 3 | HSET key field value [field value ...] HSETNX key field value #field必须不存在,才可以设置成功,用于添加。 |
示例:
1 2 | 10.1.1.11:6379> hset user:1 name tom age 18 (integer) 2 |
2、获取所有field
- hkeys命令应该叫hfields更为恰当,它返回指定哈希键所有的field。
1 | HKEYS key |
示例:
1 2 3 | 10.1.1.11:6379> hkeys user:1 1) "name" 2) "age" |
3、获取所有value
1 | HVALS key |
示例:
1 2 3 | 10.1.1.11:6379> hvals user:1 1) "tom" 2) "18" |
4、获取所有的field-value
1 | HGETALL key |
- 在使用hgetall时,如果哈希元素个数比较多,会存在阻塞Redis的可能。
- 如果只需要获取部分field,可以使用hmget。
- 如果一定要获取全部field-value,可以使用hscan命令,该命令会渐进式遍历哈希类型。
示例:
1 2 3 4 5 | 10.1.1.11:6379> hgetall user:1 1) "name" 2) "tom" 3) "age" 4) "18" |
5、获取指定field的值
1 | HGET key field |
示例:
1 2 3 4 5 6 7 8 | 10.1.1.11:6379> HGET user:1 name "tom" 10.1.1.11:6379> HGET user:1 age "18" //如果键或field不存在,会返回nil 10.1.1.11:6379> HGET user:1 age1 (nil) |
6、删除field
- hdel会删除一个或多个field,返回结果为成功删除field的个数。
1 | HDEL key field [field ...] |
示例:
1 2 | 10.1.1.11:6379> hdel user:1 age (integer) 1 |
7、查看field的个数
1 | HLEN key |
示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | 10.1.1.11:6379> hkeys user:1 1) "name" 10.1.1.11:6379> hset user:1 age 18 (integer) 1 10.1.1.11:6379> hset user:1 city tianjin (integer) 1 10.1.1.11:6379> hlen user:1 (integer) 3 10.1.1.11:6379> hkeys user:1 1) "name" 2) "age" 3) "city" |
8、批量设置或获取field-value
1 2 | HMSET key field value [field value ...] HMGET key field [field ...] |
9、判断field是否存在
1 | HEXISTS key field |
10、计数
1 2 | HINCRBY key field increment #自增指定数字 HINCRBYFLOAT key field increment #自增浮点数 |
- hincrby和hincrbyfloat,就像incrby和incrbyfloat命令一样,但是它们的作用域是filed。
11、查询value的字符串长度(需要Redis3.2以上)
1 | HSTRLEN key field |
3.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 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | //当field个数比较少且没有大的value时,内部编码为ziplist: 10.1.1.11:6379> hmset hashkey f1 v1 f2 v2 OK 10.1.1.11:6379> object encoding hashkey "ziplist" //当有value大于64字节,内部编码会由ziplist变为hashtable: 10.1.1.11:6379> hset hashkey f3 "one string is bigger than 64 byte...忽略..." OK 10.1.1.11:6379> object encoding hashkey "hashtable" //当field个数超过512,内部编码也会由ziplist变为hashtable: 10.1.1.11:6379> hmset hashkey f1 v1 f2 v2 f3 v3 ...忽略... f513 v513 OK 10.1.1.11:6379> object encoding hashkey "hashtable" |
3.3、使用场景
- 相比于使用字符串序列化缓存用户信息,哈希类型变得更加直观,并且在更新操作上会更加便捷
- 但需要注意的是哈希类型和关系型数据库有两点不同之处:
- 哈希类型是稀疏的,而关系型数据库是完全结构化的,例如哈希类型每个键可以有不同的field,而关系型数据库一旦添加新的列,所有行都要为其设置值(即使为NULL),如图2-17所示。
- 关系型数据库可以做复杂的关系查询,而Redis去模拟关系型复杂查询开发困难,维护成本高。
- 到目前为止,已经能够用三种方法缓存用户信息,下面给出三种方案的实现方法和优缺点分析。
- (1)原生字符串类型:每个属性一个键。
- 优点:简单直观,每个属性都支持更新操作。
- 缺点:占用过多的键,内存占用量较大,同时用户信息内聚性比较差,所以此种方案一般不会在生产环境使用。
- (1)原生字符串类型:每个属性一个键。
1 2 3 | set user:1:name tom set user:1:age 23 set user:1:city beijing |
-
- (2)序列化字符串类型:将用户信息序列化后用一个键保存。
- 优点:简化编程,如果合理的使用序列化可以提高内存的使用效率。
- 缺点:序列化和反序列化有一定的开销,同时每次更新属性都需要把全部数据取出进行反序列化,更新后再序列化到Redis中。
- (2)序列化字符串类型:将用户信息序列化后用一个键保存。
1 | set user:1 serialize(userInfo) |
-
- (3)哈希类型:每个用户属性使用一对field-value,但是只用一个键保存。
- 优点:简单直观,如果使用合理可以减少内存空间的使用。
- 缺点:要控制哈希在ziplist和hashtable两种内部编码的转换,hashtable会消耗更多内存。
- (3)哈希类型:每个用户属性使用一对field-value,但是只用一个键保存。
1 | hmset user:1 name tomage 23 city beijing |
4、列表
- 列表(list)类型是用来存储多个有序的字符串,列表中的每个字符串都被称为元素(element),一个列表最多可以存储2^32-1个元素。
- 在Redis中,可以对列表两端插入(push)和弹出(pop),也可以获取指定范围的子列表、索引下标的元素等(如图2-18和图2-19所示)。
- 列表是一种比较灵活的数据结构,它可以充当栈和队列的角色,在实际开发上有很多应用场景。
- 列表类型有两个特点:
- (1)列表中的元素是有序的,这就意味着可以通过索引下标获取某个元素或者某个范围内的子列表。
- (2)列表中的元素可以是重复的。
4.1、命令
- 列表类型有5种操作命令,如表2-4所示。
- 表2-5是列表类型命令及其时间复杂度,开发人员可以参考此表选择适合的命令。
4.1.1、添加操作
1、从右边插入元素
1 | RPUSH key element [element ...] |
示例:
1 2 3 4 5 6 7 | //从右边依次插入元素b、a 10.1.1.11:6379> rpush listkey b a (integer) 2 //从左到右获取列表的所有元素 10.1.1.11:6379> lrange listkey 0 -1 1) "b" 2) "a" |
2、从左边插入元素
1 | LPUSH key element [element ...] |
示例:
1 2 3 4 5 6 7 8 | //从左边插入元素c 10.1.1.11:6379> lpush listkey c (integer) 3 //从左到右获取列表的所有元素 10.1.1.11:6379> lrange listkey 0 -1 1) "c" 2) "b" 3) "a" |
3、向某个元素前或后插入元素
- linsert命令会从列表中找到等于pivot的元素,在其前(before)或后(after)插入一个新的元素value。
1 | LINSERT key BEFORE|AFTER pivot element |
示例:
1 2 3 4 5 6 7 8 9 | //在列表的元素b前插入java 10.1.1.11:6379> linsert listkey before b java (integer) 4 //从左到右获取列表的所有元素 10.1.1.11:6379> lrange listkey 0 -1 1) "c" 2) "java" 3) "b" 4) "a" |
4.1.2、查找
1、获取指定范围内的元素列表
- lrange会获取列表指定索引范围所有的元素。
1 | LRANGE key start stop |
- 索引下标有两个特点:
- (1)索引下标从左到右分别是0到N-1,但是从右到左分别是-1到-N。
- (2)lrange中的stop选项包含自身,这个和很多编程语言不包含end不太相同。
示例:
1 2 3 4 5 6 7 8 9 10 | //从左到右获取列表的所有元素 10.1.1.11:6379> lrange listkey 0 -1 1) "c" 2) "java" 3) "b" 4) "a" 10.1.1.11:6379> lrange listkey 1 2 1) "java" 2) "b" |
2、获取列表指定索引下标的元素
1 | LINDEX key index |
示例:
1 2 | 10.1.1.11:6379> lindex listkey 1 "java" |
3、获取列表长度
1 | LLEN key |
示例:
1 2 | 10.1.1.11:6379> llen listkey (integer) 4 |
4.1.3、删除
1、从列表左侧弹出元素
1 | LPOP key |
示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | //从左到右获取列表的所有元素 10.1.1.11:6379> lrange listkey 0 -1 1) "c" 2) "java" 3) "b" 4) "a" //从列表左侧弹出元素 10.1.1.11:6379> lpop listkey "c" //从左到右获取列表的所有元素 10.1.1.11:6379> lrange listkey 0 -1 1) "java" 2) "b" 3) "a" |
2、从列表右侧弹出
1 | RPOP key |
示例:
1 2 3 4 5 6 7 | //从列表右侧弹出 10.1.1.11:6379> rpop listkey "a" //从左到右获取列表的所有元素 10.1.1.11:6379> lrange listkey 0 -1 1) "java" 2) "b" |
3、删除指定元素
1 | LREM key count element |
- lrem命令会从列表中找到等于element的元素进行删除,根据count的不同分为三种情况:
- count>0,从左到右,删除最多count个元素。
- count<0,从右到左,删除最多count绝对值个元素。
- count=0,删除所有。
示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | //删除listkey键 10.1.1.11:6379> del listkey //重新创建listkey键 10.1.1.11:6379> rpush listkey a a a a a java b a (integer) 8 //从左向右删除4个a元素 10.1.1.11:6379> lrem listkey 4 a (integer) 4 //从左到右获取列表的所有元素 10.1.1.11:6379> lrange listkey 0 -1 1) "a" 2) "java" 3) "b" 4) "a" |
4、按照索引范围修剪列表
1 | LTRIM key start stop |
示例:
1 2 3 4 5 6 7 8 | //只保留列表listkey第2个到第4个元素 10.1.1.11:6379> ltrim listkey 1 3 OK //从左到右获取列表的所有元素 10.1.1.11:6379> lrange listkey 0 -1 1) "java" 2) "b" 3) "a" |
4.1.4、修改
- 修改指定索引下标的元素
1 | LSET key index new_value |
示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 | //从左到右获取列表的所有元素 10.1.1.11:6379> lrange listkey 0 -1 1) "java" 2) "b" 3) "a" //将列表listkey中的第2个元素修改为python 10.1.1.11:6379> lset listkey 1 python OK 10.1.1.11:6379> lrange listkey 0 -1 1) "java" 2) "python" 3) "a" |
4.1.5、阻塞操作
1 2 | BLPOP key [key ...] timeout BRPOP key [key ...] timeout |
- brpop命令包含两个参数:
- key [key ...]:多个列表的键。
- timeout:阻塞时间(单位:秒)。
- 在使用brpop时,有两点需要注意。
- (1)如果是多个键,那么brpop会从左至右遍历键,一旦有一个键能弹出元素,客户端立即返回。
- (2)如果多个客户端对同一个键执行brpop,那么最先执行brpop命令的客户端可以获取到弹出的值。
示例1:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | //列表为空:如果timeout=3,那么客户端要等到3秒后返回,如果timeout=0,那么客户端一直阻塞等下去 10.1.1.11:6379> lrange listkey 0 -1 ( empty array ) 10.1.1.11:6379> brpop listkey 3 (nil) (3.01s) 10.1.1.11:6379> brpop listkey 0 ...阻塞... //如果此期间添加了新元素,则立即返回(另个窗口执行rpush listkey hh) 10.1.1.11:6379> brpop listkey 0 1) "listkey" 2) "hh" |
示例2:
1 2 3 4 5 6 7 | //列表不为空:客户端会立即返回 10.1.1.11:6379> lrange listkey 0 -1 1) "hh123" 10.1.1.11:6379> brpop listkey 0 1) "listkey" 2) "hh123" |
4.2、内部编码
- 列表类型的内部编码有两种。
- ziplist(压缩列表):当列表的元素个数小于list-max-ziplist-entries配置(默认512个),同时列表中每个元素的值都小于list-max-ziplist-value配置时(默认64字节),Redis会选用ziplist来作为列表的内部实现来减少内存的使用。
- linkedlist(链表):当列表类型无法满足ziplist的条件时,Redis会使用linkedlist作为列表的内部实现。
- Redis3.2版本提供了quicklist内部编码,简单地说它是以一个ziplist为节点的linkedlist,它结合了ziplist和linkedlist两者的优势,为列表类型提供了一种更为优秀的内部编码实现,它的设计原理可以参考Redis的另一个作者Matt Stancliff的博客:https://matt.sh/redis-quicklist。
示例:
1 2 3 4 5 6 7 8 9 10 | //当元素个数较少且没有大元素时,内部编码为ziplist: //当某个元素超过64字节,内部编码会变为linkedlist //当元素个数超过512个,内部编码变为linkedlist 10.1.1.11:6379> lrange listkey 0 -1 ( empty array ) 10.1.1.11:6379> rpush listkey e1 e2 e3 (integer) 3 10.1.1.11:6379> object encoding listkey "quicklist" |
4.3、使用场景
1、消息队列
- 如图2-21所示,Redis的lpush+brpop命令组合即可实现阻塞队列,生产者客户端使用lrpush从列表左侧插入元素,多个消费者客户端使用brpop命令阻塞式的“抢”列表尾部的元素,多个客户端保证了消费的负载均衡和高可用性。
2、文章列表
- 每个用户有属于自己的文章列表,现需要分页展示文章列表。此时可以考虑使用列表,因为列表不但是有序的,同时支持按照索引范围获取元素。
- (1)每篇文章使用哈希结构存储,假如每篇文章有3个属性title、timestamp、content
1 2 3 | hmset acticle:1 title xx timestamp 1476536196 content xxxx ... hmset acticle:k title yy timestamp 1476512536 content yyyy |
- (2)向用户文章列表添加文章,user:{id}:articles作为用户文章列表的键
1 2 3 | lpush user:1:acticles article:1 article3 ... lpush user:k:acticles article:5 |
- (3)分页获取用户文章列表,例如下面伪代码获取用户id=1的前10篇文章
1 2 3 | articles = lrange user:1:articles 0 9 for article in {articles} hgetall {article} |
- 使用列表类型保存和获取文章列表会存在两个问题。
- (1)如果每次分页获取的文章个数较多,需要执行多次hgetall操作,此时可以考虑使用Pipeline批量获取,或者考虑将文章数据序列化为字符串类型,使用mget批量获取。
- (2)分页获取文章列表时,lrange命令在列表两端性能较好,但是如果列表较大,获取列表中间范围的元素性能会变差,此时可以考虑将列表做二级拆分,或者使用Redis3.2的quicklist内部编码实现,它结合ziplist和linkedlist的特点,获取列表中间范围的元素时也可以高效完成。
- 实际上列表的使用场景很多,在选择时可以参考以下口诀:
- lpush+lpop=Stack(栈)
- lpush+rpop=Queue(队列)
- lpsh+ltrim=Capped Collection(有限集合)
- lpush+brpop=Message Queue(消息队列)
5、集合
- 集合(set)类型也是用来保存多个的字符串元素,但是和列表类型不一样,集合中不允许有重复元素,并且集合中的元素是无序的,不能通过索引下标获取元素。
- 一个集合最多可以存储2^32-1个元素。Redis除了支持集合内的增删改查,同时还支持多个集合取交集、并集、差集。
- 如图2-22所示,集合user:1:follow包含"it"、"music"、"his"、"sports"四个元素。
5.1、命令
- 集合类型有2种操作命令:集合内和集合间。
- 表2-6是集合类型命令及其时间复杂度,开发人员可以参考此表选择适合的命令。
5.1.1、集合内操作
1、添加元素
1 | SADD key member [member ...] |
示例:
1 2 3 4 5 6 7 8 | 10.1.1.11:6379> exists myset (integer) 0 //返回结果为添加成功的元素个数 10.1.1.11:6379> sadd myset a b c (integer) 3 10.1.1.11:6379> sadd myset a b (integer) 0 |
2、计算元素个数
1 | SCARD key |
- scard的时间复杂度为O(1),它不会遍历集合所有元素,而是直接用Redis内部的变量。
示例:
1 2 | 10.1.1.11:6379> scard myset (integer) 3 |
3、获取所有元素
1 | SMEMBERS key |
- smembers和lrange、hgetall都属于比较重的命令,如果元素过多存在阻塞Redis的可能性,这时候可以使用sscan来完成。
示例:
1 2 3 4 | 10.1.1.11:6379> smembers myset 1) "b" 2) "a" 3) "c" |
4、随机从集合返回指定个数元素
1 2 | SRANDMEMBER key [ count ] [ count ]是可选参数,默认为1 |
示例:
1 2 3 | 10.1.1.11:6379> srandmember myset 2 1) "b" 2) "a" |
5、判断元素是否在集合中
1 | SISMEMBER key member |
示例:
1 2 3 4 5 | //如果给定元素element在集合内返回1,反之返回0。 10.1.1.11:6379> sismember myset a (integer) 1 10.1.1.11:6379> sismember myset e (integer) 0 |
6、从集合随机弹出元素
1 2 | SPOP key [ count ] [ count ]是可选参数,默认为1 |
- 从3.2版本开始,spop也支持[count]参数。srandmember和spop都是随机从集合选出元素,两者不同的是spop命令执行后,元素会从集合中删除,而srandmember不会。
示例:
1 2 3 4 5 6 7 | //从集合随机弹出元素(删除并返回元素) 10.1.1.11:6379> spop myset "b" 10.1.1.11:6379> smembers myset 1) "a" 2) "c" |
7、删除元素
1 | SREM key member [member ...] |
示例:
1 2 3 4 5 | //返回结果为成功删除元素个数 10.1.1.11:6379> srem myset a b (integer) 1 10.1.1.11:6379> srem myset hello (integer) 0 |
5.1.2、集合间操作
- 现在有两个集合,它们分别是myset1和myset2。
1 2 3 4 | 10.1.1.11:6379> sadd myset1 it music his sports (integer) 4 10.1.1.11:6379> sadd myset2 it news ent sports (integer) 4 |
1、求多个集合的交集
1 | SINTER key [key ...] |
示例:
1 2 3 | 10.1.1.11:6379> sinter myset1 myset2 1) "sports" 2) "it" |
2、求多个集合的并集
1 | SUNION key [key ...] |
示例:
1 2 3 4 5 6 7 | 10.1.1.11:6379> sunion myset1 myset2 1) "news" 2) "his" 3) "it" 4) "sports" 5) "ent" 6) "music" |
3、求多个集合的差集
1 | SDIFF key [key ...] |
示例:
1 2 3 | 10.1.1.11:6379> sdiff myset1 myset2 1) "his" 2) "music" |
4、将交集、并集、差集的结果保存
- 集合间的运算在元素较多的情况下会比较耗时,所以Redis提供了下面三个命令(原命令+store)将集合间交集、并集、差集的结果保存在destination中。
1 2 3 | SINTERSTORE destination key [key ...] SUNIONSTORE destination key [key ...] SDIFFSTORE destination key [key ...] |
示例:
1 2 3 4 5 6 7 8 9 | //将myset1和myset2的交集,保存到myset:1_2:inter集合中 10.1.1.11:6379> sinterstore myset:1_2:inter myset1 myset2 (integer) 2 10.1.1.11:6379> type myset:1_2:inter set 10.1.1.11:6379> smembers myset:1_2:inter 1) "sports" 2) "it" |
5.2、内部编码
- 集合类型的内部编码有两种:
- intset(整数集合):当集合中的元素都是整数(小于2^64)且元素个数小于set-maxintset-entries配置(默认512个)时,Redis会选用intset来作为集合的内部实现,从而减少内存的使用。
- hashtable(哈希表):当集合类型无法满足intset的条件时,Redis会使用hashtable作为集合的内部实现。(存在不是整数的值、或整数值超过2^64、或元素的个数超过512)
示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 | //当元素个数较少且都为整数时,内部编码为intset 10.1.1.11:6379> keys * ( empty array ) 10.1.1.11:6379> sadd setkey 1 2 3 4 (integer) 4 10.1.1.11:6379> object encoding setkey "intset" //当某个元素不为整数(浮点数或字符串)时,内部编码会变为hashtable 10.1.1.11:6379> sadd setkey 5.5 (integer) 1 10.1.1.11:6379> object encoding setkey "hashtable" //当元素个数超过512个,内部编码变为hashtable |
5.3、使用场景
- 集合类型比较典型的使用场景是标签(tag)。
- 例如一个用户可能对娱乐、体育比较感兴趣,另一个用户可能对历史、新闻比较感兴趣,这些兴趣点就是标签。有了这些数据就可以得到喜欢同一个标签的人,以及用户的共同喜好的标签,这些数据对于用户体验以及增强用户黏度比较重要。
- 例如一个电子商务的网站会对不同标签的用户做不同类型的推荐,比如对数码产品比较感兴趣的人,在各个页面或者通过邮件的形式给他们推荐最新的数码产品,通常会为网站带来更多的利益。
- 下面使用集合类型实现标签功能的若干功能。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | //(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 //(5)计算用户共同感兴趣的标签 sinter user:1:tags user:2:tags |
- 用户和标签的关系维护应该在一个事务内执行,防止部分命令失败造成的数据不一致。(1)和(2)、(3)和(4)尽量放在一个事务执行。
- 集合类型的应用场景通常为以下几种:
- sadd=Tagging(标签)
- spop/srandmember=Random item(生成随机数,比如抽奖)
- sadd+sinter=Social Graph(社交需求)
6、有序集合
- 有序集合和集合一样不能有重复的元素,不同的是,有序集合中的元素可以排序,但是它和列表使用索引下标作为排序依据不同的是,它给每个元素设置一个分数(score)作为排序的依据(score可以重复)。
- 如图2-24所示,该有序集合包含kris、mike、frank、tim、martin、tom,它们的分数分别是1、91、200、220、250、251,有序集合提供了获取指定分数和元素范围查询、计算成员排名等功能。
- 表2-7给出了列表、集合、有序集合三者的异同点。
6.1、命令
- 有序集合类型有2种操作命令:集合内和集合间。
- 表2-8是有序集合类型命令及其时间复杂度,开发人员可以参考此表选择适合的命令。
6.1.1、集合内操作
1、添加成员
1 | ZADD key [NX|XX] [CH] [INCR] score member [score member ...] |
- 有关zadd命令有两点需要注意:
- Redis3.2为zadd命令添加了nx、xx、ch、incr四个选项:
- nx:member必须不存在,才可以设置成功,用于添加。
- xx:member必须存在,才可以设置成功,用于更新。
- ch:返回此次操作后,有序集合元素和分数发生变化的个数
- incr:对score做增加,相当于后面介绍的zincrby。
- 有序集合相比集合提供了排序字段,但是也产生了代价,zadd的时间复杂度为O(long(n)),sadd的时间复杂度为O(1)。
- Redis3.2为zadd命令添加了nx、xx、ch、incr四个选项:
示例:
1 2 3 4 | 10.1.1.11:6379> zadd myzset 251 tom (integer) 1 10.1.1.11:6379> zadd myzset 1 kris 91 mike 200 frank 220 tim 250 martin (integer) 5 |
2、返回所有成员个数
1 | ZCARD key |
- 和集合类型的scard命令一样,zcard的时间复杂度为O(1)。
示例:
1 2 | 10.1.1.11:6379> zcard myzset (integer) 6 |
3、返回指定分数范围成员个数
1 | ZCOUNT key min max |
示例:
1 2 | 10.1.1.11:6379> ZCOUNT myzset 200 300 (integer) 4 |
4、返回指定分数范围的成员
1 2 3 4 5 | ZRANGEBYSCORE key min max [WITHSCORES] [LIMIT offset count ] #按照分数从低到高返回 ZREVRANGEBYSCORE key max min [WITHSCORES] [LIMIT offset count ] #按照分数从高到低返回 min和max支持开区间(小括号)和闭区间(中括号),-inf和+inf分别代表无限小和无限大 withscores会同时返回每个成员的分数 [limit offset count ]选项可以限制输出的起始位置和个数 |
示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | //-inf和+inf分别代表无限小和无限大 10.1.1.11:6379> zrevrangebyscore myzset +inf -inf withscores 1) "tom" 2) "251" 3) "martin" 4) "250" 5) "tim" 6) "220" 7) "frank" 8) "200" 9) "mike" 10) "91" 11) "kris" 12) "1" 10.1.1.11:6379> zrevrangebyscore myzset 300 250 withscores 1) "tom" 2) "251" 3) "martin" 4) "250" |
5、返回指定排名范围的成员
1 2 | ZRANGE key start stop [WITHSCORES] #按分数排名从低到高返回 ZREVRANGE key start stop [WITHSCORES] #按分数排名从高到低返回 |
示例:
1 2 3 4 5 6 7 | 10.1.1.11:6379> zrevrange myzset 0 2 withscores 1) "tom" 2) "251" 3) "martin" 4) "250" 5) "tim" 6) "220" |
6、返回某个成员的分数
1 | ZSCORE key member |
示例:
1 2 | 10.1.1.11:6379> zscore myzset tom "251" |
7、增加成员的分数
1 | ZINCRBY key increment member |
示例:
1 2 3 | //给tom增加了9分,分数变为了260分,返回增加后的分数 10.1.1.11:6379> zincrby myzset 9 tom "260" |
8、计算成员的排名
1 2 | ZRANK key member #按分数从低到高返回排名(排名从0开始计算) ZREVRANK key member #按分数从高到低返回排名(排名从0开始计算) |
示例:
1 2 | 10.1.1.11:6379> zrevrank myzset tom (integer) 0 |
9、删除成员
1 | ZREM key member [member ...] |
示例:
1 2 3 | //将成员mike从有序集合user:ranking中删除。 10.1.1.11:6379> zrem myzset mike (integer) 1 |
10、删除指定排名内的升序元素
1 | ZREMRANGEBYRANK key start stop |
示例:
1 2 | 10.1.1.11:6379> zremrangebyrank myzset 0 2 (integer) 3 |
11、删除指定分数范围的成员
1 | ZREMRANGEBYSCORE key min max |
示例:
1 2 | 10.1.1.11:6379> zremrangebyscore myzset (250 +inf (integer) 1 |
6.1.2、集合间操作
- 将图2-25的两个有序集合导入到Redis中。
- 现在有两个集合,它们分别是myzset1和myzset2。
1 2 3 4 | 10.1.1.11:6379> zadd myzset1 1 kris 91 mike 200 frank 220 tim 250 martin 251 tom (integer) 6 10.1.1.11:6379> zadd myzset2 8 james 77 mike 625 martin 888 tom (integer) 4 |
1、交集
1 2 3 4 5 6 | ZINTERSTORE destination numkeys key [key ...] [WEIGHTS weight] [AGGREGATE SUM|MIN|MAX] destination:交集计算结果保存到这个键 numkeys:需要做交集计算键的个数 key [key...]:需要做交集计算的键 [WEIGHTS weight [weight ...]]:每个键的权重,在做交集计算时,每个键中的每个member会将自己分数乘以这个权重,每个键的权重默认是1 aggregate sum|min|max:计算成员交集后,分值可以按照sum(和)、min(最小值)、max(最大值)做汇总,默认值是sum |
示例:
1 2 3 4 5 6 7 8 9 10 | //对myzset1和myzset2做交集,weights和aggregate使用了默认配置,可以看到目标键user:ranking:1_inter_2对分值做了sum操作 10.1.1.11:6379> zinterstore myzset:1_inter_2 2 myzset1 myzset2 (integer) 3 10.1.1.11:6379> zrange myzset:1_inter_2 0 -1 withscores 1) "mike" 2) "168" 3) "martin" 4) "875" 5) "tom" 6) "1139" |
2、并集
1 | ZUNIONSTORE destination numkeys key [key ...] [WEIGHTS weight] [AGGREGATE SUM|MIN|MAX] |
6.2、内部编码
- 有序集合类型的内部编码有两种:
- ziplist(压缩列表):当有序集合的元素个数小于zset-max-ziplistentries配置(默认128个),同时每个元素的值都小于zset-max-ziplist-value配置(默认64字节)时,Redis会用ziplist来作为有序集合的内部实现,ziplist可以有效减少内存的使用。
- skiplist(跳跃表):当ziplist条件不满足时,有序集合会使用skiplist作为内部实现,因为此时ziplist的读写效率会下降。
示例:
1 2 3 4 5 6 7 8 9 10 11 | //当元素个数较少且每个元素较小时,内部编码为skiplist 10.1.1.11:6379> zadd zsetkey 50 e1 60 e2 30 e3 (integer) 3 10.1.1.11:6379> object encoding zsetkey "ziplist" //当某个元素大于64字节时,内部编码也会变为hashtable 10.1.1.11:6379> zadd zsetkey 20 "one string is bigger than 64 byte................................" (integer) 1 10.1.1.11:6379> object encoding zsetkey "skiplist" //当元素个数超过128个,内部编码变为ziplist |
6.3、使用场景
- 有序集合比较典型的使用场景就是排行榜系统。例如视频网站需要对用户上传的视频做排行榜,榜单的维度可能是多个方面的:按照时间、按照播放数量、按照获得的赞数。本节使用赞数这个维度,记录每天用户上传视频的排行榜。主要需要实现以下4个功能。
- (1)添加用户赞数
1 2 3 4 | //用户mike上传了一个视频,并获得了3个赞,可以使用有序集合的zadd和zincrby功能 zadd user:ranking:2016_03_15 mike 3 //如果之后再获得一个赞,可以使用zincrby zincrby user:ranking:2016_03_15 mike 1 |
- (2)取消用户赞数
1 2 | //由于各种原因(例如用户注销、用户作弊)需要将用户删除,此时需要将用户从榜单中删除掉,可以使用zrem。例如删除成员tom zrem user:ranking:2016_03_15 mike |
- (3)展示获取赞数最多的十个用户
1 2 | //此功能使用zrevrange命令实现: zrevrangebyrank user:ranking:2016_03_15 0 9 |
- (4)展示用户信息以及用户分数
1 2 3 4 | //此功能将用户名作为键后缀,将用户信息保存在哈希类型中,至于用户的分数和排名可以使用zscore和zrank两个功能: hgetall user:info:tom zscore user:ranking:2016_03_15 mike zrank user:ranking:2016_03_15 mike |
7、键管理
7.1、单个键管理
- 针对单个键的命令,前面已经介绍过一部分了,例如type、del、object、exists、expire等,下面将介绍剩余的几个重要命令。
7.1.1、单个键管理
1 2 | RENAME key newkey #如果newkey已经存在,那么newkey值将被覆盖 RENAMENX key newkey #只有newkey不存在时,才会重命名成功 |
- 在使用重命名命令时,有两点需要注意:
- 由于重命名键期间会执行del命令删除旧的键,如果键对应的值比较大,会存在阻塞Redis的可能性,这点不要忽视。
- 如果rename和renamenx中的key和newkey如果是相同的,在Redis3.2和之前版本返回结果略有不同(Redis3.2之前会提示错误,Redis3.2(含)之后会返回OK)。
示例:
1 2 3 4 5 6 7 8 9 | //将键hello,重命名为hi 10.1.1.11:6379> get hello "wrold" 10.1.1.11:6379> rename hello hi OK 10.1.1.11:6379> get hello (nil) 10.1.1.11:6379> get hi "wrold" |
7.1.2、随机返回一个键名
1 | RANDOMKEY |
示例:
1 2 3 4 5 6 7 8 9 | 10.1.1.11:6379> keys * 1) "java" 2) "hi" 3) "hello" 10.1.1.11:6379> randomkey "hi" 10.1.1.11:6379> randomkey "hello" |
7.1.3、键过期
- 除了expire、ttl命令以外,Redis还提供了expireat、pexpire、pexpireat、pttl、persist等一系列命令,下面分别进行说明:
1 2 3 4 5 6 7 8 | EXPIRE key seconds #设置键在seconds秒后过期 EXPIREAT key timestamp #设置键在到时间戳timestamp后过期(秒) PEXPIRE key milliseconds #设置键在milliseconds毫秒后过期 PEXPIREAT key milliseconds-timestamp #设置键在到时间戳timestamp后过期(毫秒) TTL key #查询键的剩余过期时间(秒) PTTL key #查询键的剩余过期时间(毫秒) |
- ttl和pttl命令有3种返回值:
- 大于等于0的整数:键剩余的过期时间
- -1:键没有设置过期时间
- -2:键不存在
- 无论是使用过期时间还是时间戳,秒级还是毫秒级,在Redis内部最终使用的都是pexpireat。
- 在使用Redis相关过期命令时,需要注意以下几点。
- (1)如果expire key的键不存在,返回结果为0。
- (2)如果过期时间为负值,键会立即被删除,犹如使用del命令一样。
- (3)persist命令可以将键的过期时间清除。
- (4)对于字符串类型键,执行set命令会去掉过期时间。
- (5)Redis不支持二级数据结构(例如哈希、列表)内部元素的过期功能,例如不能对列表类型的一个元素做过期时间设置。
- (6)setex命令作为set+expire的组合,不但是原子执行,同时减少了一次网络通讯的时间。
示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | 10.1.1.11:6379> keys * ( empty array ) 10.1.1.11:6379> set hello world OK //设置键过期时间为60秒 10.1.1.11:6379> expire hello 60 (integer) 1 //键过期时间还剩56秒 10.1.1.11:6379> ttl hello (integer) 56 //键过期时间还剩54575毫秒 10.1.1.11:6379> pttl hello (integer) 54575 //返回结果为-2,说明键hello已经被删除 10.1.1.11:6379> ttl hello (integer) -2 |
7.1.4、迁移键
- 迁移键功能非常重要,因为有时候我们只想把部分数据由一个Redis迁移到另一个Redis(例如从生产环境迁移到测试环境),Redis发展历程中提供了move、dump+restore、migrate三组迁移键的方法,它们的实现方式以及使用的场景不太相同,如表2-9所示(笔者建议使用migrate)。
1、move
- 一个Redis实例内部可以有多个数据库,多个数据库之间在数据上是相互隔离的(笔者不建议在生产环境中使用多数据库功能)。
- move命令用于在Redis实例内部进行数据迁移,把指定的键从源数据库迁移到目标数据库中,如图2-26所示。
1 | MOVE key db |
2、dump+restore
1 2 | DUMP key RESTORE key ttl serialized-value [REPLACE] [ABSTTL] [IDLETIME seconds] [FREQ frequency] |
- 如图2-27所示,dump+restore在不同的Redis实例之间进行数据迁移,整个迁移的过程分为两步:
- (1)在源Redis上,使用dump命令将键值序列化,格式采用的是RDB格式。
- (2)在目标Redis上,使用restore命令将上面序列化的值进行复原,其中ttl参数代表过期时间,如果ttl=0代表没有过期时间。
- dump+restore需要注意两点:
- (1)整个迁移过程并非原子性的,而是通过客户端分步完成的。
- (2)迁移过程是开启了两个客户端连接,所以dump的结果不是在源Redis和目标Redis之间进行传输。
实例:
1 2 3 4 5 6 7 8 9 | //在一个redis实例中序列化键值 10.1.1.11:6379> dump hello "\x00\x05world\t\x00\xc9#mH\x84/\x11s" //在另一个redis实例中将序列化的值进行复原 10.1.1.11:6380> restore hello 0 "\x00\x05world\t\x00\xc9#mH\x84/\x11s" OK 10.1.1.11:6380> get hello "world" |
3、migrate
- migrate命令在不同的Redis实例之间进行数据迁移。实际上migrate命令就是将dump、restore、del三个命令进行组合,从而简化了操作流程。
- migrate命令具有原子性,而且从Redis3.0.6版本以后已经支持迁移多个键的功能,有效地提高了迁移效率。
1 2 3 4 5 6 7 8 9 10 | MIGRATE host port key| "" destination-db timeout [ COPY ] [REPLACE] [AUTH password] [AUTH2 username password] [KEYS key [key …]] host:目标Redis的IP地址 port:目标Redis的端口 key| "" :在Redis3.0.6版本之前,migrate只支持迁移一个键,所以此处是要迁移的键,但Redis3.0.6版本之后支持迁移多个键,如果当前需要迁移多个键,此处为空字符串 "" 。 destination-db:目标Redis的数据库索引,例如要迁移到0号数据库,这里就写0。 timeout:迁移的超时时间(单位为毫秒)。 [ copy ]:如果添加此选项,迁移后并不删除源键。 [replace]:如果添加此选项,migrate不管目标Redis是否存在该键都会正常迁移(可能会有数据覆盖)。 [AUTH password]:目标redis的认证信息。 keys [key...]:迁移多个键,例如要迁移key1、key2、key3,此处填写 "keys key1 key2 key3" 。 |
- 整个过程如图2-28所示,实现过程和dump+restore基本类似,但是有3点不太相同:
- (1)整个过程是原子执行的,不需要在多个Redis实例上开启客户端的,只需要在源Redis上执行migrate命令即可。
- (2)migrate命令的数据传输直接在源Redis和目标Redis上完成的。
- (3)目标Redis完成restore后会发送OK给源Redis,源Redis接收后会根据migrate对应的选项来决定是否在源Redis上删除对应的键。
示例:
- 源Redis使用6379端口,目标Redis使用6380端口,现要将源Redis的键hello迁移到目标Redis中,会分为如下几种情况:
- 情况1:源Redis有键hello,目标Redis没有,会返回OK,表明迁移成功。
1 2 | 10.1.1.11:6379> migrate 10.1.1.11 6380 hello 0 1000 auth hengha123 OK |
-
- 情况2:源Redis和目标Redis都有键hello,会有两种情形。
1 2 3 4 5 6 | //(1)如果migrate命令没有加replace选项会收到错误提示,迁移失败 10.1.1.11:6379> migrate 10.1.1.11 6380 hello 0 1000 auth hengha123 (error) ERR Target instance replied with error: BUSYKEY Target key name already exists. //(2)如果migrate命令加了replace选项,会返回OK表明迁移成功 10.1.1.11:6379> migrate 10.1.1.11 6380 hello 0 1000 auth hengha123 replace OK |
- 情况3:源Redis没有键hello,会返回nokey的提示。
1 2 | 10.1.1.11:6379> migrate 10.1.1.11 6380 hello 0 1000 auth hengha123 NOKEY |
示例2:
- Redis3.0.6版本以后可以迁移多个键。
1 2 3 4 5 6 | //源Redis批量添加多个键: 127.0.0.1:6379> mset key1 value1 key2 value2 key3 value3 OK //源Redis执行如下命令完成多个键的迁移: 10.1.1.11:6379> migrate 10.1.1.11 6380 "" 0 5000 auth hengha123 keys key1 key2 key3 OK |
7.2、遍历键
- Redis提供了两个命令遍历所有的键,分别是keys和scan。
1、全量遍历键
- keys会遍历所有键(例如检测过期或闲置时间、寻找大对象等)。pattern支持glob风格通配符格式。
1 | KEYS pattern |
- 如果Redis包含了大量的键,执行keys命令很可能会造成Redis阻塞(因为Redis是单线程的),所以一般建议不要在生产环境下使用keys命令。
- 确实有遍历键的需求该怎么办,可以在以下三种情况使用:
- 在一个不对外提供服务的Redis从节点上执行,这样不会阻塞到客户端的请求,但是会影响到主从复制。
- 如果确认键值总数确实比较少,可以执行该命令。
- 使用scan命令渐进式的遍历所有键,可以有效防止阻塞。
2、渐进式遍历
- Redis从2.8版本后,提供了一个新的命令scan,它能有效的解决keys命令存在的问题。
- 和keys命令执行时会遍历所有键不同,scan采用渐进式遍历的方式来解决keys命令可能带来的阻塞问题,每次scan命令的时间复杂度是O(1),但是要真正实现keys的功能,需要执行多次scan。
- Redis存储键值对实际使用的是hashtable的数据结构,其简化模型如图2-29所示。
- 那么每次执行scan,可以想象成只扫描一个字典中的一部分键,直到将字典中的所有键遍历完毕。scan的使用方法如下:
1 2 3 4 | SCAN cursor [MATCH pattern] [ COUNT count ] [TYPE type] cursor是必需参数,实际上cursor是一个游标,第一次遍历从0开始,每次scan遍历完都会返回当前游标的值,直到游标值为0,表示遍历结束。 match pattern是可选参数,它的作用的是做模式的匹配,这点和keys的模式匹配很像。 count number是可选参数,它的作用是表明每次要遍历的键个数,默认值是10,此参数可以适当增大。 |
- 除了scan以外,Redis提供了面向哈希类型、集合类型、有序集合的扫描遍历命令,解决诸如hgetall、smembers、zrange可能产生的阻塞问题,对应的命令分别是hscan、sscan、zscan,它们的用法和scan基本类似。
- 渐进式遍历可以有效的解决keys命令可能产生的阻塞问题,但是scan并非完美无瑕,如果在scan的过程中如果有键的变化(增加、删除、修改),那么遍历效果可能会碰到如下问题:新增的键可能没有遍历到,遍历出了重复的键等情况,也就是说scan并不能保证完整的遍历出来所有的键,这些是我们在开发时需要考虑的。
7.3、数据库管理
- Redis提供了几个面向Redis数据库的操作,它们分别是dbsize、select、flushdb/flushall命令。
1、切换数据库
1 | SELECT index |
- Redis和许多关系型数据库一样,在一个实例下可以存在多个数据库,但关系型数据库用字符来区分不同的数据库,而Redis使用数字来区分不同的数据库。Redis默认配置中是有16个数据库(配置文件:databases 16)。
- 使用rediscli -h {ip} -p {port}连接Redis时,默认使用0号数据库。
- Redis3.0中已经逐渐弱化了一个实例可以有多数据库的功能,例如Redis的分布式实现Redis Cluster只允许使用0号数据库。
- 为什么要废弃掉这个“优秀”的功能呢?总结起来有三点:
- Redis是单线程的。如果使用多个数据库,那么这些数据库仍然是使用一个CPU,彼此之间还是会受到影响的。
- 多数据库的使用方式,会让调试和运维不同业务的数据库变的困难,假如有一个慢查询存在,依然会影响其他数据库,这样会使得别的业务方定位问题非常的困难。
- 部分Redis的客户端根本就不支持这种方式。即使支持,在开发的时候来回切换数字形式的数据库,很容易弄乱。
- 笔者建议如果要使用多个数据库功能,完全可以在一台机器上部署多个Redis实例,彼此用端口来做区分,因为现代计算机或者服务器通常是有多个CPU的。这样既保证了业务之间不会受到影响,又合理地使用了CPU资源。
2、flushdb/flushall
- flushdb/flushall命令用于清除数据库。
- flushdb只清除当前数据库。
- flushall会清除所有数据库。
1 2 | FLUSHDB [ASYNC] #如果在0号数据库执行flushdb,1号数据库的数据依然还在 FLUSHALL [ASYNC] #在任意数据库执行flushall会将所有数据库清除 |
- flushdb/flushall命令可以非常方便的清理数据,但是也带来两个问题:
- flushdb/flushall命令会将所有数据清除,一旦误操作后果不堪设想。
- 如果当前数据库键值数量比较多,flushdb/flushall存在阻塞Redis的可能性。
1 | # # |
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· winform 绘制太阳,地球,月球 运作规律
· 震惊!C++程序真的从main开始吗?99%的程序员都答错了
· AI与.NET技术实操系列(五):向量存储与相似性搜索在 .NET 中的实现
· 【硬核科普】Trae如何「偷看」你的代码?零基础破解AI编程运行原理
· 超详细:普通电脑也行Windows部署deepseek R1训练数据并当服务器共享给他人
2021-08-04 python网络编程Twisted02 Twisted基础和Reactor方法