Redis的理解和使用
准备
全局命令
Redis有5种数据结构,它们是键值对中的值,对于键来说有一些通用的命令。
#插入键值对
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) "python"
2) "java"
3) "hello"
#键总数
127.0.0.1:6379> dbsize
(integer) 10
#127.0.0.1:6379> rpush mylist a b c d e f g
(integer) 7
#检查键是否存在
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> del mylist
(integer) 1
127.0.0.1:6379> exists mylist
(integer) 0
#返回结果为成功删除键的个数,假设删除一个不存在的键,就会返回0
127.0.0.1:6379> del not_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
#键过期
127.0.0.1:6379> set hello world
OK
127.0.0.1:6379> expire hello 10 #设置了10秒过期时间
(integer) 1
#ttl命令会返回键的剩余过期时间,它有3种返回值:
#大于等于0的整数:键剩余的过期时间
#-1:键没设置过期时间。
#-2:键不存在
# 还剩 7 秒
127.0.0.1:6379> ttl hello
(integer) 7
...
# 还剩 1 秒
127.0.0.1:6379> ttl hello
(integer) 1
# 返回结果为 -2 ,说明键 hello 已经被删除
127.0.0.1:6379> ttl hello
(integer) -2
127.0.0.1:6379> get hello
(nil)
#键的数据结构类型
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
#如果键不存在,则返回none:
127.0.0.1:6379> type not_exsit_key
none
dbsize命令在计算键总数时不会遍历所有键,而是直接获取Redis内置的键总数变量,所以dbsize命令的时间复杂度是O(1)。而keys命令会遍历所有键,所以它的时间复杂度是O(n),当Redis保存了大量键时,线上环境禁止使用
数据结构和内部编码
单线程架构
引出单线程模型
现在开启了三个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是单线程来处理命令的,所以一条命令从客户端达到服务端不会立刻被执行,所有命令都会进入一个队列中,然后逐个被执行。
执行的顺序是不确定的,但是 结果是不会变得,不会产生并发问题
单线程还能这么快
- 纯内存访问
- 非阻塞I/O,Redis使用epoll作为I/O多路复用技术的实现,再加上Redis自身的事件处理模型将epoll中的连接、读写、关闭都转换为事件,不在网络I/O上浪费过多的时间
- 单线程避免了线程切换和竞态产生的消耗
字符串
字符串类型是Redis最基础的数据结构,键都是字符串类型,字符串类型的值实际可以是字符串(简单的字符串、复杂的字符串(例如JSON、XML))、数字(整数、浮点数),甚至是二进制(图片、音频、视频),但是值最大不能超过512MB
命令
-
常用命令
设置值
set key value [ex seconds] [px milliseconds] [nx|xx]
下面操作设置键为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> setxx hello jedis OK
setnx和setxx在实际使用中有什么应用场景吗?以setnx命令为例子,由于Redis的单线程命令处理机制,如果有多个客户端同时执行setnx key value,根据setnx的特性只有一个客户端能设置成功,setnx可以作为分布式锁的一种实现方案,Redis官方给出了使用setnx实现分布式锁的方。
#获取值 get key #下面操作获取键hello的值: 127.0.0.1:6379> get hello "world" #如果要获取的键不存在,则返回nil(空) 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 #批量获取值 mget key [key ...] #下面操作批量获取了键a、b、c、d的值: 127.0.0.1:6379> mget a b c d 1) "1" 2) "2" 3) "3" 4) "4" #如果有些键不存在,那么它的值为nil(空),结果是按照传入键的顺 #redis可以支撑10w/s的读写,由于需要多次请求redis数据,对于客户端来说网络的IO可能会成为性能的瓶颈。 127.0.0.1:6379> mget a b c f 1) "1" 2) "2" 3) "3" 4) (nil) #计数 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命令用于对值做自增操作,返回结果分为三种情况:
- 值不是整数,返回错误。
- 值是整数,返回自增后的结果。
- 键不存在,按照值为0自增,返回结果为1。
除了incr命令,Redis提供了decr(自减)、incrby(自增指定数字)、decrby(自减指定数字)、incrbyfloat(自增浮点数):
- decr key
- incrby key increment
- decrby key decrement
- incrbyfloat key increment
很多存储系统和编程语言内部使用CAS机制实现计数功能,会有一定的CPU开销,但在Redis中完全不存在这个问题,因为Redis是单线程架构,任何命令到了Redis服务端都要按顺序执行。
- 不常用命令
#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" #字符串长度 127.0.0.1:6379> get key "redisworld" 127.0.0.1:6379> strlen key (integer) 10 #每个中文基本上占用2个字节 127.0.0.1:6379> set hello "世界" OK 127.0.0.1:6379> strlen hello (integer) 4 #设置并返回原值 127.0.0.1:6379> getset hello world (nil) 127.0.0.1:6379> getset hello redis "world" #设置指定位置的字符 setrange key offeset 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:偏移量从0开始计算 127.0.0.1:6379> getrange redis 0 1 "be"
内部编码
字符串类型的内部编码有3种:
- int:8个字节的长整型。
- embstr:小于等于39个字节的字符串。
- raw:大于39个字节的字符串。
#整数类型
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
"raw"
127.0.0.1:6379> strlen key
(integer) 40
使用场景
- 缓存功能
由于Redis具有支撑高并发的特性,所以缓存通常能起到加速读写和降低后端压力的作用
// 定义键 userRedisKey = "user:info:" + id; // 从 Redis 获取值 value = redis.get(userRedisKey); if (value != null) { // 将值进行反序列化为 UserInfo 并返回结果 userInfo = deserialize(value); return userInfo; }
- 计数
实现快速计数、查询缓存的功能,同时数据可以异步落地到其他数据源
long incrVideoCounter(long id) { key = "video:playCount:" + id; return redis.incr(key); }
- 共享Session
一个分布式Web服务将用户的Session信息(例如用户登
录信息)保存在各自服务器中,这样会造成一个问题,出于负载均衡的考虑,分布式服务会将用户的访问均衡到不同服务器上,用户刷新一次访问可能会发现需要重新登录
使用redis管理session,利用expire模拟session过期,可以放在网管check用户合法性。 - 限速
应用出于安全的考虑,会在每次进行登录时,让用户输入手机验证码,从而确定是否是用户本人。但是为了短信接口不被频繁访问,会限制用户每分钟获取验证码的频率,例如一分钟不能超过5次。
访问速度限制,对每个用户的请求通一接口速率控制。
命名规则
设计合理的键名,有利于防止键冲突和项目的可维护性,比较推荐的方式是使用“业名:对象名:id:[属性]”作为键名
注意redis配置
redis默认的key值序列化方式是JDK,但是JDK值不够直观,所以在数据量不是特别的情况下,修改redis的序列化的方式。
哈希
几乎所有的编程语言都提供了哈希(hash)类型,它们的叫法可能是哈希、字典、关联数组
命令
#设置值
#hset key field value
127.0.0.1:6379> hset user:1 name tom
(integer) 1
#获取值
#hget key field
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)
#删除field
#hdel key 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 key
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
#批量设置或获取field-value
#hmget key field [field ...]
#hmset key field value [field value ...]
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"
#判断field是否存在,包含返回结果为1,不包含时返回0
#hexists key field
127.0.0.1:6379> hexists user:1 name
(integer) 1
#获取所有field
#hkeys key
127.0.0.1:6379> hkeys user:1
1) "name"
2) "age"
3) "city"
#获取所有value
#hvals key
127.0.0.1:6379> hvals user:1
1) "mike"
2) "12"
3) "tianjin"
#获取所有的field-value
#hgetall key
127.0.0.1:6379> hgetall user:1
1) "name"
2) "mike"
3) "age"
4) "12"
5) "city"
6) "tianjin"
hgetall时,如果哈希元素个数比较多,会存在阻塞Redis的可能,只需要获取部分field,可以使用hmget,一定要获取全部field-value,可以使用hscan命令
#自增数据:就像incrby和incrbyfloat命令一样,但是它们的作用域是filed。
#hincrby key field
#hincrbyfloat key field
127.0.0.1:6379> hincrby user:2 sex 3
(integer) 4
#计算value的字符串长度(需要Redis3.2以上)
#hstrlen key field
127.0.0.1:6379> hstrlen user:1 name
(integer) 3
内部编码
- ziplist(压缩列表)
当哈希类型元素个数小于hash-max-ziplist-entries配置(默认512个)、同时所有值都小于hash-max-ziplist-value配置(默认64字节)时,Redis会使用ziplist作为哈希的内部实现,ziplist使用更加紧凑的结构实现多个元素的连续存储,所以在节省内存方面比hashtable更加优秀。
- hashtable(哈希表)
当哈希类型无法满足ziplist的条件时,Redis会使用hashtable作为哈希的内部实现,因为此时ziplist的读写效率会下降,而hashtable的读写时间复杂度为O(1)
使用场景
- 缓存用户信息
使用字符串序列化缓存用户信息,哈希类型变得更加直观,并且在更新操作上会更加便捷。
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; }
- 三种方法缓存用户信息
- 原生字符串类型:每个属性一个键。
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)类型是用来存储多个有序的字符串
- 列表中的元素是有序的
- 列表中的元素可以是重复的
命令
- 添加和弹出操作
#从右边插入元素
#rpush key value [value ...]
127.0.0. 1:6379> rpush listkey c b a
(integer) 3
#从左边插入元素
#lpush key value [value ...]
127.0.0. 1:6379> lpush listkey1 c b a
(integer) 3
#向某个元素前或者后插入元素
#linsert key before|after pivot 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 key start end
- 索引下标从左到右分别是0到N-1,但是从右到左分别是-1到-N。
- lrange中的end选项包含了自身
127.0.0.1:6379> lrange listkey 1 3
1) "java"
2) "b"
3) "a"
- 获取列表指定索引下标的元素
lindex key index
127.0.0.1:6379> lindex listkey -1
"a"
- 获取列表长度
llen key
127.0.0.1:6379> llen listkey
(integer) 4
- 删除
- lpop key 从列表左侧弹出元素
127.0.0.1:6379>t lpop listkey "c" 127.0.0.1:6379> lrange listkey 0 -1 1) "java" 2) "b" 3) "a"
- rpop key 从列表右侧弹出
- lrem key count value 删除指定元素
1 count>0,从左到右,删除最多count个元素。
2 count<0,从右到左,删除最多count绝对值个元素。
3 count=0,删除所有。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 key start end127.0.0.1:6379> ltrim listkey 1 3 OK 127.0.0.1:6379> lrange listkey 0 -1 1) "java" 2) "b" 3) "a"
- lpop key 从列表左侧弹出元素
- 修改
- 修改指定索引下标的元素
lset key index newValue127.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 …] timeoutkey[key…]:多个列表的键
timeout:阻塞时间(单位:秒)
- 列表为空:如果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 ... 阻塞 ...
- 如果此期间添加了数据element1,客户端立即返回:
第一点,如果是多个键,那么brpop会从左至右遍历键,一旦有个键113能弹出元素,客户端立即返回。127.0.0.1:6379> brpop list:test 3 1) "list:test" 2) "element1" (2.06s)
第二点,如果多个客户端对同一个键执行brpop,那么最先执行brpop命令的客户端可以获取到弹出的值,其余客户端继续阻塞。
内部编码
- quicklist
在版本3.2之后,重新引入了一个 quicklist 的数据结构,列表的底层都由quicklist实现。
类似1.8之后的hashmap,对查询和存储做了一个平衡。
- ziplist
当列表的元素个数小于list-max-ziplist-entries配置(默认512个),同时列表中每个元素的值都小于list-max-ziplist-value配置时(默认64字节),Redis会选用ziplist来作为列表的内部实现来减少内存的使用。
- linkedlist
当列表类型无法满足ziplist的条件时,Redis会使用linkedlist作为列表的内部实现。
当元素个数超过512个,内部编码变为linkedlist:127.0.0.1:6379> rpush listkey e1 e2 e3 (integer) 3 127.0.0.1:6379> object encoding listkey "ziplist"
或者当某个元素超过64字节,内部编码也会变为linkedlist:127.0.0.1:6379> rpush listkey e4 e5 ... 忽略 ... e512 e513 (integer) 513 127.0.0.1:6379> object encoding listkey "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
使用场景
- 消息队列
- 文章列表
热点数据缓存,可以进行也处理,使用分布式锁处理好redis和数据库的数据同步关系。
集合
集合(set)类型也是用来保存多个的字符串元素,但和列表类型不一样的是,集合中不允许有重复元素,并且集合中的元素是无序的,不能通过索引下标获取元素。一个集合最多可以存储2 32 -1个元素。Redis除了支持集合内的增删改查,同时还支持多个集合取交集、并集、差集,合理地使用好集合类型
命令
- 集合内操作
- 添加元素
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
- 删除元素
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
- 计算元素个数
scard key ,scard的时间复杂度为O(1),它不会遍历集合所有元素,而是直接用
Redis内部的变量127.0.0.1:6379> scard myset (integer) 1
- 判断元素是否在集合中
sismember key element:在集合内返回1,反之返回0127.0.0.1:6379> sismember myset c (integer) 1
- 随机从集合返回指定个数元素
srandmember key [count]:[count]是可选参数,如果不写默认为1127.0.0.1:6379> srandmember myset 2 1) "a" 2) "c" 127.0.0.1:6379> srandmember myset "d"
- 从集合随机弹出元素
spop key: Redis从3.2版本开始,spop也支持[count]参数127.0.0.1:6379> spop myset "c" 127.0.0.1:6379> smembers myset 1) "d" 2) "b" 3) "a"
- 获取所有元素
smembers key 所有元素,并且返回结果是无序的,元素过多存在阻塞Redis的可能性,这时候可以使用sscan来完成。127.0.0.1:6379> smembers myset 1) "d" 2) "b" 3) "a"
- 集合间操作
127.0.0.1:6379> sadd user:1:follow it music his sports (integer) 4 127.0.0.1:6379> sadd user:2:fol low it news ent sports (integer) 4
- 求多个集合的交集
sinter key [key …]127.0.0.1:6379> sinter user:1:follow user:2:follow 1) "sports" 2) "it"
- 求多个集合的并集
suinon key [key …]127.0.0.1:6379> sunion user:1:follow user:2:follow 1) "sports" 2) "it" 3) "his" 4) "news" 5) "music" 6) "ent"
- 求多个集合的差集
suinon key [key …]127.0.0.1:6379> sdiff user:1:follow user:2:follow 1) "music" 2) "his"
- 将交集、并集、差集的结果保存
sinterstore destination key [key …]
suionstore destination key [key …]
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"
内部编码
- intset(整数集合):当集合中的元素都是整数且个数小于set-max-intset-entries配置(默认512个)时,redis会选用intset来作为集合的内部实现,从而减少内存的使用。
- hashtable(哈希表):当几个类型无法满足intset的条件时,Redis会使用hashtable作为集合的内部实现。
使用场景
存放一系列数据结构单一的数据。
有序集合
可以排序的集合。
命令
- 集合内
- 添加成员
zadd key score member 【score member…】
Redis3.2为zadd命令添加了nx、xx、ch、incr四个选项:zadd user:ranking 251 tom (integer) 1 zadd user:ranking 1 kris 91 mike 200 frank 220 tim 250 martin (integer) 5
- nx:member必须不存在,才可以设置成功,用于添加。
- xx:member必须存在,才可以设置成功,用于更新。
- ch:返回此次操作后,有序集合元素和分数发生变化的个数
- incr:对score做增加,相当于后面介绍的zincrby
有序集合相比集合提供了排序字段,但是也产生了代价,zadd的时间
复杂度为O(log(n)),sadd的时间复杂度为O(1)。
-
计算成员个数
zcard key127.0.0.1:6379> zcard user:ranking (integer) 5
-
计算某个成员的分数
zscore key member127.0.0.1:6379> zscore user:ranking tom "251" 127.0.0.1:6379> zscore user:ranking test (nil)
-
计算成员的排名
zrank key member #从分数从低到高返回排名
zrevrank key membe #zrevrank反之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 member127.0.0.1:6379> zincrby user:ranking 9 tom "260"
-
返回指定排名范围的成员
zrange key start end [withscores]
zrevrange key start end [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 key min max [withscores] [limit offset count]
zrevrangebyscore key max min [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"
-
返回指定分数范围成员个数
zcount key min max127.0.0.1:6379> zcount user:ranking 200 221 (integer) 2
-
删除指定排名内的升序元素
zremrangebyrank key start end127.0.0.1:6379> zremrangebyrank user:ranking 0 2 (integer) 3
-
删除指定分数范围的成员
zremrangebyscore key min max127.0.0.1:6379> zremrangebyscore user:ranking (250 +inf (integer) 2
- 集合间的操作
127.0.0.1:6379> zadd user:ranking:1 1 kris 91 mike 200 frank 220 tim 250 martin251 tom (integer) 6 127.0.0.1:6379> zadd user:ranking:2 8 james 77 mike 625 martin 888 tom (integer) 4
- 交集
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"
- 并集
zunionstore destination numkeys key [key …] [weights weight [weight …]][aggregate sum|min|max127.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"
内部编码
- ziplist(压缩列表):当有序集合的元素个数小于zset-max-ziplist-entries配置(默认128个),同时每个元素的值都小于zset-max-ziplist-value配置(默认64字节)时,Redis会用ziplist来作为有序集合的内部实现,ziplist可以有效减少内存的使用。
- skiplist(跳跃表):当ziplist条件不满足时,有序集合会使用skiplist作为内部实现,因为此时ziplist的读写效率会下降。
使用场景
排行榜系统、热搜榜、点赞榜等等
例如:mike上传一个视频,并获得了3个赞,
(1)可以使用有序集合zadd和zincrby:
zadd user:ranking:2016_03_15 3 mike
获取赞后:使用zincrby
zincrby user:ranking:2016_03_15 1mike
(2)取消用户赞数,使用zrem
zrem user:ranking:2016_03_15 mike
(3)获取赞数最多的是个用户
zrevrangbyrank user:ranking:2016_03_15 0 9
(4)展示用户信息以及用户分数
hgetall user:info:tom
zsocre user:ranking:2016_03_15 mike
zrank user:ranking:2016_03_15 mike
键管理
单个键管理
-
键重命名
rename key newkey #强制重新命名127.0.0.1:6379> set python jedis OK 127.0.0.1:6379> get python "jedis" 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 (nil) 127.0.0.1:6379> get java "jedis"
renamenx #只有newKey不存在时候才被覆盖
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如果相同,在redis3.2会返回OK,3.2之前的版本返回 “ERR source and destination objects are the same"
-
随机返回一个键
randomkey127.0.0.1:6379> dbsize 1000 127.0.0.1:6379> randomkey "hello" 127.0.0.1:6379> randomkey "jedis"
-
键过期
- expire key seconds:键在seconds秒后过期。
- expireat key timestamp:键在秒级时间戳timestamp后过期
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命令可以设置键的秒级过期时间戳
127.0.0.1:6379> expireat hello 1469980800 (integer) 1
Redis2.6版本后提供了毫秒级的过期方案
-
pexpire key milliseconds:键在milliseconds毫秒后过期
-
pexpireat key milliseconds-timestamp键在毫秒级时间戳timestamp后过期
-
如果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> 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
- 对于字符串类型键,执行set命令会去掉过期时间
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); }
- Redis不支持二级数据结构(例如哈希、列表)内部元素的过期功能
- setex命令作为set+expire的组合,不但是原子执行,同时减少了一次网络通讯的时间
-
迁移键
迁移键功能非常重要,因为有时候我们只想把部分数据由一个Redis迁移到另一个Redis。- move key db
move命令用于在Redis内部进行数据迁移 - dump+restore
dump key
restore key ttl value
1)在源Redis上,dump命令会将键值序列化,格式采用的是RDB格式
2)在目标Redis上,restore命令将上面序列化的值进行复原,其中ttl参数代表过期时间,如果ttl=0代表没有过期时间。
- 在源Redis上执行dump
redis-source> set hello world OK redis-source> dump hello "\x00\x05world\x06\x00\x8f<T\x04%\xfcNQ"
- 在目标Redis上执行restore:
redis-target> get hello (nil) redis-target> restore hello 0 "\x00\x05world\x06\x00\x8f<T\x04%\xfcNQ" OK redis-target> get hello "world"
伪代码:
Redis sourceRedis = new Redis("sourceMachine", 6379); Redis targetRedis = new Redis("targetMachine", 6379); targetRedis.restore("hello", 0, sourceRedis.dump(key));
- migrate
migrate host port key|“” destination-db timeout [copy] [replace] [keys key [key …]]
migrate命令也是用于在Redis实例间进行数据迁移的,实际上migrate命令就是将dump、restore、del三个命令进行组合,从而简化了操作流程。migrate命令具有原子性,而且从Redis3.0.6版本以后已经支持迁移多个键的功能,有效地提高了迁移效率。
migrate的参数进行逐个说明:
- host:目标Redis的IP地址。
- port:目标Redis的端口。
- key|“”:在Redis3.0.6版本之前,migrate只支持迁移一个键,所以此处是要迁移的键,但Redis3.0.6版本之后支持迁移多个键,如果当前需要迁移多个键,此处为空字符串""。
- destination-db:目标Redis的数据库索引,例如要迁移到0号数据库,这里就写0。
- timeout:迁移的超时时间(单位为毫秒)。
- [copy]:如果添加此选项,迁移后并不删除源键。
- [replace]:如果添加此选项,migrate不管目标Redis是否存在该键都会正常迁移进行数据覆盖。
- [keys key[key…]]:迁移多个键,例如要迁移key1、key2、key3,此处填写“keys key1 key2 key3”。
- move key db
Redis使用6379端口,目标Redis使用6380端口
情况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"
OK
如果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,目标Redis没有
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
遍历键
- 全量遍历键
keys pattern
上面为了遍历所有的键,pattern直接使用星号,这是因为pattern使用的是glob风格的通配符:127.0.0.1:6379> dbsize (integer) 0 127.0.0.1:6379> mset hello world redis best jedis best hill high OK 127.0.0.1:6379> keys * 1) "hill" 2) "jedis" 3) "redis" 4) "hello"
- *代表匹配任意字符。
- ?代表匹配一个字符。
- []代表匹配部分字符,例如[1,3]代表匹配1,3,[1-10]代表匹配1到10的任意数字。
- \x用来做转义,例如要匹配星号、问号需要进行转义
127.0.0.1:6379> keys [j,r]edis 1) "jedis" 2) "redis" 127.0.0.1:6379> keys hll* 1) "hill" 2) "hello"
想删除所有以video字符串开头的键
redis-cli keys video* | xargs redis-cli del
- 渐进式遍历
Redis是单线程架构,执行keys命令很可能会造成Redis阻塞,从2.8版本后,提供了一个新的命令scan,它能有效的解决keys命令存在的问题。和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,此参数可以适当增大。
127.0.0.1:6379> scan 0 MATCH user* 1) "5" 2) 1) "user:ranking" 2) "user:2" 127.0.0.1:6379> scan 5 MATCH user* 1) "0" 2) 1) "user:1"
Redis提供了面向哈希类型、集合类型、有序集合的扫描遍历命令,解决诸如hgetall、smembers、zrange可能产生的阻塞问题,对应的命令分别是hscan、sscan、zscan。
// 分别以old:user和new:user开头,先需要将old:user开头的元素全部删除
String key = "myset";
// 定义 pattern
String pattern = "old:user*";
// 游标每次从 0 开始
String cursor = "0";
while (true) {
// 获取扫描结果
ScanResult scanResult = redis.sscan(key, cursor, pattern);
List elements = scanResult.getResult();
if (elements != null && elements.size() > 0) {
// 批量删除
redis.srem(key, elements);
}
// 获取新的游标
cursor = scanResult.getStringCursor();
// 如果游标为 0 表示遍历结束
if ("0".equals(cursor)) {
break;
}
}
渐进式遍历可以有效的解决keys命令可能产生的阻塞问题,但是scan并非完美无瑕,如果在scan的过程中如果有键的变化(增加、删除、修改),那么遍历效果可能会碰到如下问题:新增的键可能没有遍历到,遍历出了重复的键等情况,也就是说scan并不能保证完整的遍历出来所有的键,这些是我们在开发时需要考虑的
数据库管理
- 切换数据库
select dbIndex
Redis只是用数
字作为多个数据库的实现。Redis默认配置中是有16个数据库,select0操作将切换到第一个数据库,但是0号数据库和15号数据库之间的数据没有任何关联,甚至可以存在相同的键。127.0.0.1:6379> set hello world # 默认进到 0 号数据库 OK 127.0.0.1:6379> get hello "world" 127.0.0.1:6379> select 15 # 切换到 15 号数据库 OK 127.0.0.1:6379[15]> get hello # 因为 15 号数据库和 0 号数据库是隔离的,所以 get hello 为空 (nil)
- 清除数据库
flushdb:清除当前数据库
flushall:清除该redis所有数据