Redis是一种基于键值对(key-value)的NoSQL数据库,它支持string(字符串)、hash(哈希)、list(列表)、set(集合)、zset(有序集合)、Bitmaps(位图)、HyperLogLog、GEO(地理信息定位)等多种数据结构和算法。因为Redis会将所有数据都存放在内存中,所以它的读写性能非常惊人。不仅如此,Redis还可以将内存的数据利用快照和日志的形式保存到硬盘上,这样在发生类似断电或者机器故障的时候,内存中的数据不会“丢失”。除了上述功能以外,Redis还提供了键过期、发布订阅、事务、流水线、Lua脚本等附加功能。
安装并启动Redis
安装Redis
wget http://download.redis.io/releases/redis-3.0.7.tar.gz
tar xzf redis-3.0.7.tar.gz
cd redis-3.0.7/
make
sudo make install
Redis安装之后,src和/usr/local/bin目录下多了几个以redis开头可执行文件,我们称之为Redis Shell,这些可执行文件可以做很多事情,例如可以启动和停止Redis、可以检测和修复Redis的持久化文件,还可以检测Redis的性能。
| 可执行文件 | 作用 |
|---|---|
| redis-server | 启动 Redis |
| redis-cli | Redis 命令行客户端 |
| redis-benchmark | Redis 基准测试工具 |
| redis-check-aof | Redis AOF 持久化文件检测和修复工具 |
| redis-check-dump | Redis RDB 持久化文件检测和修复工具 |
| redis-sentinel | 启动 Redis Sentinel |
启动Redis
有三种方法启动Redis:默认配置、运行配置、配置文件启动。
默认配置
这种方法会使用Redis的默认配置来启动。
redis-server
因为直接启动无法自定义配置,所以这种方式是不会在生产环境中使用的。
运行启动
redis-server加上要修改配置名和值(可以是多对),没有设置的配置将使用默认配置:
redis-server --configKey1 configValue1 --configKey2 configValue2
如果要用6380作为端口启动Redis,那么可以执行:
redis-server --port 6380
因为直接启动无法自定义配置,所以这种方式是不会在生产环境中使用的。
配置文件启动
将配置写到指定文件里,例如将配置写到了/opt/redis/redis.conf中,那么只需要执行如下命令即可启动Redis:
redis-server /opt/redis/redis.conf
通过配置文件启动的方式提供了更大的灵活性,所以大部分生产环境会使用这种方式启动Redis。
Redis命令行客户端
第一种是交互式方式:通过redis-cli -h {host} -p {port}的方式连接到Redis服务:
[heql@ubuntu ~]$ redis-cli -h 127.0.0.1 -p 6379
127.0.0.1:6379> set hello world
OK
127.0.0.1:6379> get hello
"world"
第二种是命令方式:用redis-cli -h {host} -p {port} {command}就可以直接得到命令的返回结果:
[heql@ubuntu ~]$ redis-cli -h 127.0.0.1 -p 6379 get hello
"world"
如果没有-h参数,那么默认连接127.0.0.1;如果没有-p,那么默认6379端口,也就是说如果-h和-p都没写就是连接127.0.0.1:6379这个Redis实例。
停止Redis服务
redis-cli -h 127.0.0.1 -p 6379 shutdown
注意:
通过
shutdown命令关闭Redis服务: 断开与客户端的连接、持久化文件生成,是一种相对优雅的关闭方式。通过
kill进程号的方式关闭掉Redis: 不会做持久化操作,还会造成缓冲区等资源不能被优雅关闭,极端情况会造成AOF和复制丢失数据的情况。shutdown还有一个参数,代表是否在关闭Redis前,生成持久化文件:redis-cli shutdown nosave|save
API的理解和使用
全局命令
Redis有5种数据结构,它们是键值对中的值,对于键来说有一些通用的命令。
查看所有键(key*)
下面插入了3对字符串类型的键值对:
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
keys*命令会将所有的键输出:
127.0.0.1:6379> keys *
1) "hello"
2) "python"
3) "java"
键总数(dbsize)
下面插入一个列表类型的键值对(值是多个元素组成):
127.0.0.1:6379> rpush mylist a b c d e f g
(integer) 7
dbsize命令会返回当前数据库中键的总数。例如当前数据库有4个键,分别是hello、java、python、mylist,所以dbsize的结果是4:
127.0.0.1:6379> dbsize
(integer) 4
dbsize命令在计算键总数时不会遍历所有键,而是直接获取Redis内置的键总数变量,所以dbsize命令的时间复杂度是O(1)。而keys命令会遍历所有键,所以它的时间复杂度是O(n),当Redis保存了大量键时,线上环境禁止使用。
检查键是否存在(exists)
如果键存在则返回1,不存在则返回0:
127.0.0.1:6379> exists java
(integer) 1
127.0.0.1:6379> exists no_exist_key
(integer) 0
删除键(del)
del是一个通用命令,无论值是什么数据结构类型,del命令都可以将其删除,返回结果为成功删除键的个数,假设删除一个不存在的键,就会返回0:
127.0.0.1:6379> del java
(integer) 1
127.0.0.1:6379> exists java
(integer) 0
127.0.0.1:6379> del mylist
(integer) 1
127.0.0.1:6379> exists mylist
(integer) 0
127.0.0.1:6379> del no_exist_key
(integer) 0
del命令可以支持删除多个键:
127.0.0.1:6379> set a 1
OK
127.0.0.1:6379> set b 2
OK
127.0.0.1:6379> set c 3
OK
127.0.0.1:6379> del a b c
(integer) 3
键过期(expire)
Redis支持对键添加过期时间,当超过过期时间后,会自动删除键,例如为键hello设置了10秒过期时间:
127.0.0.1:6379> set hello world
OK
127.0.0.1:6379> expire hello 10
(integer) 1
ttl命令会返回键的剩余过期时间,它有3种返回值:
- 大于等于0的整数:键剩余的过期时间。
- -1:键没设置过期时间。
- -2:键不存在
通过ttl命令观察键hello的剩余过期时间:
127.0.0.1:6379> ttl hello
(integer) 8
127.0.0.1:6379> ttl hello
(integer) -2
127.0.0.1:6379> get hello
(nil)
键的数据结构类型(type)
127.0.0.1:6379> type a
string
127.0.0.1:6379> rpush mylist a b c d
(integer) 4
127.0.0.1:6379> type mylist
list
如果键不存在,则返回none:
127.0.0.1:6379> type no_exist_key
none
数据结构和内部编码
type命令实际返回的就是当前键的数据结构类型,它们分别是:string(字符串)、hash(哈希)、list(列表)、set(集合)、zset(有序集合),但这些只是Redis对外的数据结构。
实际上每种数据结构都有自己底层的内部编码实现,而且是多种实现,这样Redis会在合适的场景选择合适的内部编码,如图:

可以看到每种数据结构都有两种以上的内部编码实现,例如list数据结构包含了linkedlist和ziplist两种内部编码。同时有些内部编码,例ziplist,可以作为多种外部数据结构的内部实现,可以通过object encoding命令查询内部编码:
127.0.0.1:6379> object encoding python
"embstr"
127.0.0.1:6379> object encoding mylist
"ziplist"
Redis这样设计有两个好处:
- 可以改进内部编码,而对外的数据结构和命令没有影响,这样一旦开发出更优秀的内部编码,无需改动外部数据结构和命令。
- 多种内部编码实现可以在不同场景下发挥各自的优势,例如
ziplist比较节省内存,但是在列表元素比较多的情况下,性能会有所下降,这时候Redis会根据配置选项将列表类型的内部实现转换为linkedlist。
单线程架构
Redis使用了单线程架构和I/O多路复用模型来实现高性能的内存数据库服务。每次客户端调用都经历了发送命令、执行命令、返回结果三个过程。
现在开启了三个redis-cli客户端同时执行命令:
客户端1设置一个字符串键值对:
127.0.0.1:6379> set hello world
客户端2对counter做自增操作:
127.0.0.1:6379> incr counter
客户端3对counter做自增操作:
127.0.0.1:6379> incr counter
因为Redis是单线程来处理命令的,所以一条命令从客户端达到服务端不会立刻被执行,所有命令都会进入一个队列中,然后逐个被执行。所以上面3个客户端命令的执行顺序是不确定的,但是可以确定不会有两条命令被同时执行,所以两条incr命令无论怎么执行最终结果都是2,不会产生并发问题,这就是Redis单线程的基本模型。
为什么单线程还能这么快
- 纯内存访问,
Redis将所有数据放在内存中,内存的响应时长大约为100纳秒,这是Redis达到每秒万级别访问的重要基础。 - 非阻塞I/O,
Redis使用epoll作为I/O多路复用技术的实现,再加上Redis自身的事件处理模型将epoll中的连接、读写、关闭都转换为事件,不在网络I/O上浪费过多的时间。 - 单线程避免了线程切换和竞态产生的消耗。
但是单线程会有一个问题:对于每个命令的执行时间是有要求的。如果某个命令执行过长,会造成其他命令的阻塞,对于Redis这种高性能的服务来说是致命的,所以Redis是面向快速执行场景的数据库。
字符串
字符串类型的值实际可以是字符串(简单的字符串、复杂的字符串(例如JSON、XML))、数字(整数、浮点数),甚至是二进制(图片、音频、视频),但是值最大不能超过512MB。
常用命令
设置值(set)
set key value [ex seconds] [px milliseconds] [nx|xx]
- ex seconds:为键设置秒级过期时间。
- px milliseconds:为键设置毫秒级过期时间。
- nx:键必须不存在,才可以设置成功,用于添加。
- xx:与nx相反,键必须存在,才可以设置成功,用于更新
返回结果为OK代表设置成功:
127.0.0.1:6379> set hello world
OK
除了set选项,Redis还提供了setex和setnx两个命令,它们的作用和ex和nx选项是一样的。
因为键hello已存在,所以setnx失败,返回结果为0:
127.0.0.1:6379> exists hello
(integer) 1
127.0.0.1:6379> setnx hello redis
(integer) 0
setnx和setxx在实际使用中有什么应用场景吗?以setnx命令为例子,由于Redis的单线程命令处理机制,如果有多个客户端同时执行setnx key value,根据setnx的特性只有一个客户端能设置成功,setnx可以作为分布式锁的一种实现方案。
获取值(get)
如果要获取的键不存在,则返回nil(空):
127.0.0.1:6379> get hello
"world"
127.0.0.1:6379> get not_exist_key
(nil)
批量设置值(mset)
下面操作通过mset命令一次性设置4个键值对:
127.0.0.1:6379> mset a 1 b 2 c 3 d 4
OK
批量获取值(mget)
下面操作批量获取了键a、b、c、d的值:
127.0.0.1:6379> mget a b c d
1) "1"
2) "2"
3) "3"
4) "4"
如果有些键不存在,那么它的值为nil(空):
127.0.0.1:6379> mget a b f d
1) "1"
2) "2"
3) (nil)
4) "4"
批量操作命令可以有效提高开发效率,假如没有mget这样的命令,要执行n次get命令:
n次get时间 = n次网络时间 + n次命令时间
计数(incr)
incr命令用于对值做自增操作,返回结果分为三种情况:
- 值不是整数,返回错误。
- 值是整数,返回自增后的结果。
键不存在,按照值为0自增,返回结果为1。
127.0.0.1:6379> exists key (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> set hello world OK 127.0.0.1:6379> incr hello (error) ERR value is not an integer or out of range
除了incr命令,Redis提供了decr(自减)、incrby(自增指定数字)、decrby(自减指定数字)、incrbyfloat(自增浮点数):
decr key
incrby key increment
decrby key decrement
incrbyfloat key increment
不常用命令
追加值(append)
append可以向字符串尾部追加值:
127.0.0.1:6379> get hello
"world"
127.0.0.1:6379> append hello redis
(integer) 10
127.0.0.1:6379> get hello
"worldredis"
字符串长度(strlen)
当前值为worldredis,所以返回值为10:
127.0.0.1:6379> get hello
"worldredis"
127.0.0.1:6379> strlen hello
(integer) 10
设置并返回原值(getset)
getset和set一样会设置值,但是不同的是,它同时会返回键原来的值:
127.0.0.1:6379> getset hello world
"worldredis"
127.0.0.1:6379> getset hello redis
"world"
127.0.0.1:6379> get hello
"redis"
设置指定位置的字符(setrange)
下面操作将值由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"
获取部分字符串(getrange)
getrange key start end
start和end分别是开始和结束的偏移量
127.0.0.1:6379> getrange redis 0 1
"be"
内部编码
字符串类型的内部编码有3种:
- int:8个字节的长整型。
- embstr:小于等于39个字节的字符串。
- raw:大于39个字节的字符串。
Redis会根据当前值的类型和长度决定使用哪种内部编码实现:
127.0.0.1:6379> set key 123
OK
127.0.0.1:6379> object encoding key
"int"
127.0.0.1:6379> set key "hello world"
OK
127.0.0.1:6379> object encoding key
"embstr"
典型使用场景
缓存功能
下图是比较典型的缓存使用场景,其中Redis作为缓存层,MySQL作为存储层,绝大部分请求的数据都是从Redis中获取。由于Redis具有支撑高并发的特性,所以缓存通常能起到加速读写和降低后端压力的作用。

计数
许多应用都会使用Redis作为计数的基础工具,它可以实现快速计数、查询缓存的功能,同时数据可以异步落地到其他数据源。如:视频的播放次数。
共享Session
可以使用Redis将用户的Session进行集中管理,在这种模式下只要保证Redis是高可用和扩展性的,每次用户更新或者查询登录信息都直接从Redis中集中获取。
限速
很多应用出于安全的考虑,会在每次进行登录时,让用户输入手机验证码,从而确定是否是用户本人。但是为了短信接口不被频繁访问,会限制用户每分钟获取验证码的频率,例如一分钟不能超过5次。
哈希
在Redis中,哈希类型是指键值本身又是一个键值对结构。
命令
设置值(hset)
设置成功会返回1,反之会返回0。此外Redis提供了hsetnx命令,它们的关系就像set和setnx命令一样,只不过作用域由键变为field。
127.0.0.1:6379> hset user:1 name tom
(integer) 1
获取值(hget)
如果键或field不存在,会返回nil:
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)
删除field(hdel)
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
计算field个数(hlen)
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 guangzhou
(integer) 1
127.0.0.1:6379> hlen user:1
(integer) 3
批量设置或获取field-value(hmget、hmset)
hmset和hmget分别是批量设置和获取field-value,hmset需要的参数是key和多对field-value,hmget需要的参数是key和多个field:
127.0.0.1:6379> hmset user:1 name mike age 12 city guangzhou
OK
127.0.0.1:6379> hmget user:1 name city
1) "mike"
2) "guangzhou"
判断field是否存在(hexists)
127.0.0.1:6379> hexists user:1 name
(integer) 1
127.0.0.1:6379> hexists user:1 no_exist_field
(integer) 0
获取所有field(hkeys)
hkeys返回指定哈希键所有的field:
127.0.0.1:6379> hkeys user:1
1) "name"
2) "age"
3) "city"
获取所有value(hvals)
127.0.0.1:6379> hvals user:1
1) "mike"
2) "12"
3) "guangzhou"
获取所有的field-value(hgetall)
127.0.0.1:6379> hgetall user:1
1) "name"
2) "mike"
3) "age"
4) "12"
5) "city"
6) "guangzhou"
在使用hgetall时,如果哈希元素个数比较多,会存在阻塞Redis的可能。如果开发人员只需要获取部分field,可以使用hmget,如果一定要获取全部field-value,可以使用hscan命令,该命令会渐进式遍历哈希类型。
hincrby hincrbyfloat
hincrby和hincrbyfloat,就像incrby和incrbyfloat命令一样,但是它们的作用域是filed。
内部编码
哈希类型的内部编码有两种:
ziplist(压缩列表):当哈希类型元素个数小于hash-max-ziplist-entries配置(默认512个)、同时所有值都小于hash-max-ziplist-value配置(默认64字节)时,Redis会使用ziplist作为哈希的内部实现,ziplist使用更加紧凑的结构实现多个元素的连续存储,所以在节省内存方面比hashtable更加优秀。hashtable(哈希表):当哈希类型无法满足ziplist的条件时,Redis会使用hashtable作为哈希的内部实现,因为此时ziplist的读写效率会下降,而hashtable的读写时间复杂度为O(1)。
列表
列表(list)类型是用来存储多个有序的字符串,列表中的每个字符串称为元素(element),一个列表最多可以存储2^32-1个元素。在Redis中,可以对列表两端插入(push)和弹出(pop),还可以获取指定范围的元素列表、获取指定索引下标的元素等。
列表类型有两个特点:
- 列表中的元素是有序的,这就意味着可以通过索引下标获取某个元素或者某个范围内的元素列表。
- 列表中的元素可以是重复的。
命令
添加操作
从右边插入元素(rpush)
127.0.0.1:6379> rpush listkey c b a
(integer) 3
从左边插入元素(lpush)
使用方法和rpush相同。
向某个元素前或者后插入元素(linsert)
linsert key before|after pivot value
linsert命令会从列表中找到等于pivot的元素,在其前(before)或者后(after)插入一个新的元素value,返回结果,代表当前的长度:
127.0.0.1:6379> linsert listkey before b java
(integer) 4
127.0.0.1:6379> lrange listkey 0 -1
1) "c"
2) "java"
3) "b"
4) "a"
查找
获取指定范围内的元素列表(lrange):
lrange key start end
lrange操作会获取列表指定索引范围所有的元素。索引下标有两个特点:
- 索引下标从左到右分别是0到N-1,但是从右到左分别是-1到-N
lrange中的end选项包含了自身
获取列表的第2到第4个元素:
127.0.0.1:6379> lrange listkey 1 3
1) "java"
2) "b"
3) "a"
获取列表指定索引下标的元素(lindex):
127.0.0.1:6379> lindex listkey -1
"a"
获取列表长度(llen):
127.0.0.1:6379> llen listkey
(integer) 4
删除
从列表左侧弹出元素(lpop)
如下操作将列表最左侧的元素c会被弹出:
127.0.0.1:6379> lpop listkey
"c"
127.0.0.1:6379> lrange listkey 0 -1
1) "java"
2) "b"
3) "a"
从列表右侧弹出(rpop)
使用方法和lpop一样。
删除指定元素(lrem)
lrem key count value
lrem命令会从列表中找到等于value的元素进行删除,根据count的不同分为三种情况:
- count>0,从左到右,删除最多count个元素。
- count<0,从右到左,删除最多count绝对值个元素。
- count=0,删除所有。
当前列表为“a a a a a java b a”,下面操作将从列表左边开始删除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"
按照索引范围修剪列表(ltrim)
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"
修改
修改指定索引下标的元素(lset):
lset key index newValue
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"
阻塞操作
blpop key [key …] timeout
brpop key [key …] timeout
blpop和brpop是lpop和rpop的阻塞版本:
- key[key…]:多个列表的键。
- timeout:阻塞时间(单位:秒)。timeout=0,那么客户端一直阻塞等下去。
内部编码
列表类型的内部编码有两种:
ziplist(压缩列表):当列表的元素个数小于list-max-ziplist-entries配置(默认512个),同时列表中每个元素的值都小于list-max-ziplist-value配置时(默认64字节),Redis会选用ziplist来作为列表的内部实现来减少内存的使用。linkedlist(链表):当列表类型无法满足ziplist的条件时,Redis会使用linkedlist作为列表的内部实现。
使用场景
消息队列
Redis的lpush+brpop命令组合即可实现阻塞队列,生产者客户端使用lrpush从列表左侧插入元素,多个消费者客户端使用brpop命令阻塞式的“抢”列表尾部的元素,多个客户端保证了消费的负载均衡和高可用性。
文章列表
每个用户有属于自己的文章列表,现需要分页展示文章列表。此时可以考虑使用列表,因为列表不但是有序的,同时支持按照索引范围获取元素。
集合
集合(set)类型也是用来保存多个的字符串元素,但和列表类型不一样的是,集合中不允许有重复元素,并且集合中的元素是无序的,不能通过索引下标获取元素。一个集合最多可以存储2^32-1个元素。Redis除了支持集合内的增删改查,同时还支持多个集合取交集、并集、差集。
命令
集合内操作
添加元素(sadd)
返回结果为添加成功的元素个数:
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
删除元素(srem)
返回结果为成功删除元素个数:
127.0.0.1:6379> srem myset a b
(integer) 2
127.0.0.1:6379> srem myset hello
(integer) 0
计算元素个数(scard)
scard的时间复杂度为O(1),它不会遍历集合所有元素,而是直接用Redis内部的变量:
127.0.0.1:6379> scard myset
(integer) 1
判断元素是否在集合中(sismember)
如果给定元素在集合内返回1,反之返回0:
127.0.0.1:6379> sismember myset c
(integer) 1
随机从集合返回指定个数元素(srandmember)
srandmember key [count]
[count]是可选参数,如果不写默认为1:
127.0.0.1:6379> sadd myset a b c d
(integer) 3
127.0.0.1:6379> srandmember myset 2
1) "a"
2) "b"
127.0.0.1:6379> srandmember myset
"c"
从集合随机弹出元素(spop)
spop操作可以从集合中随机弹出一个元素:
127.0.0.1:6379> spop myset
"a"
获取所有元素(smembers)
127.0.0.1:6379> smembers myset
1) "d"
2) "b"
3) "c"
集合间操作
求多个集合的交集(sinter)
127.0.0.1:6379> sadd myset1 a b c d
(integer) 4
127.0.0.1:6379> sadd myset2 c d e f
(integer) 4
127.0.0.1:6379> sinter myset1 myset2
1) "d"
2) "c"
求多个集合的并集(sunion)
127.0.0.1:6379> sunion myset1 myset2
1) "f"
2) "e"
3) "c"
4) "a"
5) "d"
6) "b"
求多个集合的差集(sdiff)
127.0.0.1:6379> sdiff myset1 myset2
1) "a"
2) "b"
将交集、并集、差集的结果保存(sinterstore、suionstore、sdiffstore)
sinterstore destination key [key …]
suionstore destination key [key …]
sdiffstore destination key [key …]
集合间的运算在元素较多的情况下会比较耗时,所以Redis提供了上面三个命令(原命令+store)将集合间交集、并集、差集的结果保存在destination key中。
127.0.0.1:6379> sinterstore myset myset1 myset2
(integer) 2
127.0.0.1:6379> smembers myset
1) "c"
2) "d"
内部编码
集合类型的内部编码有两种:
intset(整数集合):当集合中的元素都是整数且元素个数小于set-maxintset-entries配置(默认512个)时,Redis会选用intset来作为集合的内部实现,从而减少内存的使用。hashtable(哈希表):当集合类型无法满足intset的条件时,Redis会使用hashtable作为集合的内部实现。
使用场景
集合类型比较典型的使用场景是标签(tag)。例如一个用户可能对娱乐、体育比较感兴趣,另一个用户可能对历史、新闻比较感兴趣,这些兴趣点就是标签。有了这些数据就可以得到喜欢同一个标签的人,以及用户的共同喜好的标签,这些数据对于用户体验以及增强用户黏度比较重要。
有序集合
它保留了集合不能有重复成员的特性,但不同的是,有序集合中的元素可以排序。它给每个元素设置一个分数(score)作为排序的依据。有序集合中的元素不能重复,但是score可以重复。
命令
集合内
添加成员(zadd)
zadd key score member [score member …]
返回结果代表成功添加成员的个数:
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的时间复杂度为O(log(n)),sadd的时间复杂度为O(1)
计算成员个数(zcard)
和集合类型的scard命令一样,zcard的时间复杂度为O(1)。
127.0.0.1:6379> zcard user:ranking
(integer) 6
计算某个成员的分数(zscore)
如果成员不存在则返回nil:
127.0.0.1:6379> zscore user:ranking tom
"251"
127.0.0.1:6379> zscore user:ranking no_exist_key
(nil)
计算成员的排名(zrank、zrevrank)
zrank key member
zrevrank key member
zrank是从分数从低到高返回排名,zrevrank反之。(排名从0开始计算)
127.0.0.1:6379> zrank user:ranking tom
(integer) 5
127.0.0.1:6379> zrevrank user:ranking tom
(integer) 0
删除成员(zrem)
返回结果为成功删除的个数:
127.0.0.1:6379> zrem user:ranking mike
(integer) 1
增加成员的分数(zincrby)
下面操作给tom增加了9分,分数变为了260分:
127.0.0.1:6379> zincrby user:ranking 9 tom
"260"
返回指定排名范围的成员(zrange、zrevrange)
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"
返回指定分数范围的成员(zrangebyscore、zrevrangebyscore)
zrangebyscore key min max [withscores] [limit offset count]
zrevrangebyscore key max min [withscores] [limit offset count]
其中zrangebyscore按照分数从低到高返回,zrevrangebyscore反之。withscores选项会同时返回每个成员的分数。[limit offset count]选项可以限制输出的起始位置和个数:
127.0.0.1:6379> zrangebyscore user:ranking 200 221 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"
返回指定分数范围成员个数(zcount)
127.0.0.1:6379> zcount user:ranking 200 221
(integer) 2
删除指定排名内的升序元素(zremrangebyrank)
zremrangebyrank key start end
下面操作删除第start到第end名的成员:
127.0.0.1:6379> zremrangebyrank user:ranking 0 2
(integer) 3
删除指定分数范围的成员(zremrangebyscore)
127.0.0.1:6379> zremrangebyscore user:ranking (250 +inf
(integer) 1
内部编码
ziplist(压缩列表):当有序集合的元素个数小于zset-max-ziplistentries配置(默认128个),同时每个元素的值都小于zset-max-ziplist-value配置(默认64字节)时,Redis会用ziplist来作为有序集合的内部实现,ziplist可以有效减少内存的使用。skiplist(跳跃表):当ziplist条件不满足时,有序集合会使用skiplist作为内部实现,因为此时ziplist的读写效率会下降。
使用场景
有序集合比较典型的使用场景就是排行榜系统。例如视频网站需要对用户上传的视频做排行榜,榜单的维度可能是多个方面的:按照时间、按照播放数量、按照获得的赞数。
键管理
单个键管理
键重命名(rename)
127.0.0.1:6379> set python jedis
OK
127.0.0.1:6379> set python java
OK
127.0.0.1:6379> get python
"java"
127.0.0.1:6379> get java
(nil)
如果在rename之前,键已经存在,那么它的值也将被覆盖:
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命令,确保只有键不存在时候才被覆盖,返回结果是0代表没有完成重命名:
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的可能性。
随机返回一个键(randomkey)
randomkey命令会随机从数据库中挑选一个键:
127.0.0.1:6379> randomkey
"hello"
键过期
除了expire、ttl命令以外,Redis还提供了expireat、pexpire、pexpireat、pttl、persist等一系列命令:
- expire key seconds:键在seconds秒后过期。
- expireat key timestamp:键在秒级时间戳timestamp后过期。
ttl命令和pttl都可以查询键的剩余过期时间,但是pttl精度更高可以达到毫秒级别,有3种返回值:
- 大于等于0的整数:键剩余的过期时间(
ttl是秒,pttl是毫秒)。 - -1:键没有设置过期时间。
- -2:键不存在。
除此之外,Redis2.6版本后提供了毫秒级的过期方案:
- pexpire key milliseconds:键在milliseconds毫秒后过期。
- pexpireat key milliseconds-timestamp:键在毫秒级时间戳timestamp后过期。
但无论是使用过期时间还是时间戳,秒级还是毫秒级,在Redis内部最终使用的都是pexpireat。
在使用Redis相关过期命令时,需要注意以下几点:
如果expire key的键不存在,返回结果为0:
127.0.0.1:6379> expire not_exist_key 30 (integer) 0如果过期时间为负值,键会立即被删除,犹如使用
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)persist命令可以将键的过期时间清除:127.0.0.1:6379> set hello world OK 127.0.0.1:6379> expire hello 50 (integer) 1 127.0.0.1:6379> ttl hello (integer) 48 127.0.0.1:6379> persist hello (integer) 1 127.0.0.1:6379> ttl hello (integer) -1对于字符串类型键,执行
set命令会去掉过期时间:127.0.0.1:6379> ttl hello (integer) 47 127.0.0.1:6379> set hello world OK 127.0.0.1:6379> ttl hello (integer) -1Redis不支持二级数据结构(例如哈希、列表)内部元素的过期功能,例如不能对列表类型的一个元素做过期时间设置。setex命令作为set+expire的组合,不但是原子执行,同时减少了一次网络通讯的时间。
迁移键
Redis发展历程中提供了move、dump+restore、migrate三组迁移键的方法,它们的实现方式以及使用的场景不太相同。
| 命令 | 作用域 | 原子性 | 支持多个键 |
|---|---|---|---|
| move | Redis实例内部 | 是 | 否 |
| dump + restore | Redis实例之间 | 否 | 否 |
| migrate | Redis实例之间 | 是 | 是 |
遍历键
全量遍历键
keys pattern
*代表匹配任意字符。·代表匹配一个字符。[]代表匹配部分字符,例如[1,3]代表匹配1,3,[1-10]代表匹配1到10的任意数字。\x用来做转义,例如要匹配星号、问号需要进行转义。127.0.0.1:6379> mset hello world redis best jedis best hill high OK 127.0.0.1:6379> keys [j,r]edis 1) "jedis" 2) "redis" 127.0.0.1:6379> keys hell* 1) "hello"
keys是一个很有帮助的命令,例如想删除所有以video字符串开头的键,可以执行如下操作:
redis-cli keys video* | xargs redis-cli del
如果Redis包含了大量的键,执行keys命令很可能会造成Redis阻塞,所以一般建议不要在生产环境下使用keys命令。可以在以下三种情况使用:
- 在一个不对外提供服务的
Redis从节点上执行,这样不会阻塞到客户端的请求,但是会影响到主从复制。 - 如果确认键值总数确实比较少,可以执行该命令。
- 使用
scan命令渐进式的遍历所有键,可以有效防止阻塞。
渐进式遍历
和keys命令执行时会遍历所有键不同,scan采用渐进式遍历的方式来解决keys命令可能带来的阻塞问题,每次scan命令的时间复杂度是O(1),但是要真正实现keys的功能,需要执行多次scan。
scan cursor [match pattern] [count number]
cursor是必需参数,实际上cursor是一个游标,第一次遍历从0开始,每次scan遍历完都会返回当前游标的值,直到游标值为0,表示遍历结束。match pattern是可选参数,它的作用的是做模式的匹配,这点和keys的模式匹配很像。count number是可选参数,它的作用是表明每次要遍历的键个数,默认值是10,此参数可以适当增大。
现在要遍历所有的键,使用scan命令效果的操作如下。第一次执行scan 0,返回结果分为两个部分:第一个部分9就是下次scan需要的cursor,第二个部分是10个键:
127.0.0.1:6379> scan 0
1) "9"
2) 1) "hill"
2) "d"
3) "mylist"
4) "python"
5) "key"
6) "hello"
7) "redis"
8) "c"
9) "user:1"
10) "listkey"
继续执行scan 9得到结果cursor变为0,说明所有的键已经被遍历过了:
127.0.0.1:6379> scan 9
1) "0"
2) 1) "b"
2) "java"
3) "jedis"
除了scan以外,Redis提供了面向哈希类型、集合类型、有序集合的扫描遍历命令,解决诸如hgetall、smembers、zrange可能产生的阻塞问题,对应的命令分别是hscan、sscan、zscan,它们的用法和scan基本类似。
渐进式遍历可以有效的解决keys命令可能产生的阻塞问题,但是scan并非完美无瑕,如果在scan的过程中如果有键的变化(增加、删除、修改),那么遍历效果可能会碰到如下问题:新增的键可能没有遍历到,遍历出了重复的键等情况,也就是说scan并不能保证完整的遍历出来所有的键,这些是我们在开发时需要考虑的。
数据库管理
切换数据库
Redis用数字作为多个数据库的实现。Redis默认配置中是有16个数据库,但是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)
默认使用的就是0号数据库,当选择其他数据库时,会有[index]的前缀标识,其中index就是数据库的索引下标。
flushdb/flushall
flushdb/flushall命令用于清除数据库,两者的区别的是flushdb只清除当前数据库,flushall会清除所有数据库。
127.0.0.1:6379> dbsize
(integer) 13
127.0.0.1:6379> flushdb
OK
127.0.0.1:6379> dbsize
(integer) 0
flushdb/flushall命令可以非常方便的清理数据,但是也带来两个问题:
flushdb/flushall命令会将所有数据清除,一旦误操作后果不堪设想。- 如果当前数据库键值数量比较多,
flushdb/flushall存在阻塞Redis的可能性。