全局命令
Redis有五种数据结构,它们是键值对中的值,对于键来说有一些通用的命令。
- 查看所有的键(
keys *
)keys *
会遍历所有的键,生产环境禁止使用。
172.17.236.250:6379> set hello world
OK
172.17.236.250:6379> set java jedis
OK
172.17.236.250:6379> set python redis-py
OK
172.17.236.250:6379> keys *
1) "java"
2) "python"
3) "hello"
172.17.236.250:6379>
复制代码
- 键总数(
dbsize
)dbsize
命令计算键总数时直接获取Redis内置的键总数变量,不会去遍历所有的键。
172.17.236.250:6379> dbsize
(integer) 3
复制代码
- 检查键是否存在(
exists key
) 存在返回1,不存在返回0。
172.17.236.250:6379> exists java
(integer) 1
172.17.236.250:6379> exists pp
(integer) 0
复制代码
- 删除键(
del key \[key ...\]
) del无论值是什么类型,del命令都可以将其删除。也可以支持删除多个键。返回结果为删除成功的键数,如果删除一个不存在的键则返回0。
172.17.236.250:6379> del java
(integer) 1
172.17.236.250:6379> del python hello
(integer) 2
172.17.236.250:6379> exists java
(integer) 0
复制代码
- 键过期(
expire key seconds
) Redis支持对键添加过期时间,当超过过期时间后,会自动删除键,例如为键hello设置10秒过期时间。
172.17.236.250:6379> set hello world
OK
172.17.236.250:6379> expire hello 10
(integer) 1
复制代码
- 返回键过期的剩余时间(
ttl key
)ttl key
有三种返回值: 大于等于0的整数:键剩余的过期时间; -1:键没有设置过期时间; -2:键不存在。
172.17.236.250:6379> set hello world
OK
172.17.236.250:6379> ttl helo
(integer) -2
172.17.236.250:6379> ttl hello
(integer) -1
172.17.236.250:6379> expire hello 10
(integer) 1
172.17.236.250:6379> ttl hello
(integer) 5
复制代码
- 键的数据结构类型(
type key
)type key
会显示键的数据结构类型,如果键不存在则返回结果为none
。
数据结构和内部编码
type key
命令实际返回的就是键当前的数据结构类型,分别是string(字符串)、hash(哈希)、list(列表)、set(集合)、zset(有序集合)。但是这些都是Redis对外的数据结构。如下图所示:
- 查看内部编码(
object encoding key
),例如查看键hello
对应值的内部编码
172.17.236.250:6379> set hello world
OK
172.17.236.250:6379> object encoding hello
"embstr"
复制代码
- 设计优点
- 可以改进内部编码,但是对外部的数据结构无影响;
- 多种内部编码实现可以在不同场景下发挥各自的优势;
单线程架构
- 开启两个客户端同时执行命令:
127.0.0.1:6379> incr counter
127.0.0.1:6379> incr counter
复制代码
看到如下Redis客户端与服务端的简化模型图:
因为Redis是单线程处理命令,所以当一条命令到达服务端是不会被立即执行,所有的命令都会进入到一个队列中,然后再逐步执行,所以上面两个命令的执行顺序是不确定的。如下图所示: 但是可以确定的是不会有两条命令同时执行,所以两条incr
命令不管是怎么执行都是2,不会产生并发问题。
为什么Redis单线程还能那么快?
- Redis将所有的数据存在内存中,是纯内存操作;
- 非阻塞I/O,Redis使用epoll作为I/O多路复用技术实现,再加上Redis自身的事件处理模型将epoll中的连接、读写、关闭都转行为事件,不会在网络I/O上浪费时间。如下图:
- 单线程避免了线程切换和竞态产生消耗;
单线程的好处:
- 简化数据结构和算法实现;
- 避免了线程切换和竞态的消耗;
单线程的问题:
- 如果某个命令执行过长,会造成其它命令阻塞,这对Redis是致命的。
字符串
字符串是Redis最基础的数据结构。首先键都是字符串类型,并且其他数据结构都是在字符串类型的基础上进行构建的。 字符串类型的值可以为:
- 字符串(简单的字符串和复杂的字符串(例如:JSON、XML))
- 数字(整数、浮点数)
- 二进制(音视频或图片(不能超过512MB))
字符串常用操作命令
设置值
- 设置成功则返回
OK
set key value \[ex seconds\] \[px milliseconds] \[nx|xx\]
复制代码
set
命令有以下几个选项:
ex seconds
:为键设置秒级过期时间;px milliseconds
:为键设置毫秒级过期时间;nx
:键必须不存在才可以设置成功;xx
:与nx相反,键必须存在,才可以设置成功; 除了set
之外,Redis还提供了setex
和setnx
两个命令,作用和set
命令的ex
和nx
选项一样。如下:
setex key seconds value
setnx key value
复制代码
用例子说明set、setnx、set xx的区别:
##键hello不存在
172.17.236.250:6379> exists hello
(integer) 0
##set xx更新键hello的值返回nil
172.17.236.250:6379> set hello redis xx
(nil)
##为键hello设置值
172.17.236.250:6379> set hello world
OK
##setnx为键hello设置值,因为键hello已存在所以设置失败返回0
172.17.236.250:6379> setnx hello redis
(integer) 0
##set xx为键hello更新值成功
172.17.236.250:6379> set hello jedis xx
OK
172.17.236.250:6379> get hello
"jedis"
复制代码
- 批量设置值
###mset key value \[key value ...\]
172.17.236.250:6379> mset a 1 b 2 c 3 d 4
OK
复制代码
在实际的应用场景中,由于Redis是单线程的,如果有多个客户端同时执行setnx key value
,根据setnx
的特性只有一个客户端能设置成功,setnx
可以作为分布式锁的一个实现方案(Redis官方使用 setnx 实现分布式锁方案传送门)
获取值
- 获取单个值,如果要获取的键不存在则返回
nil
get key
复制代码
- 批量获取值(如果获取的键不存在则返回
nil
)
mget key \[key ... \]
172.17.236.250:6379> mget a b c d
1) "1"
2) "2"
3) "3"
4) "4"
复制代码
批量操作命令可以提高开发效率,如果没有mget
这样的命令,要执行n次get
命令会按照下图方式执行,具体耗时如下:
n次get时间 = n次网络时间 + n次命令执行时间
复制代码
使用
mget
命令会按照下图方式执行,具体耗时如下:
n次get时间 = 1次网络时间 + n次命令时间
复制代码
Redis一次命令的执行时机包括网络时间和命令执行时间,Redis服务端的执行速度已经够快。
网络可能会成为性能瓶颈。在实际开发场景中,每次批量操作发送的命令数不是无节制的,数量过多可能会造成Redis阻塞或网络堵塞。
计数
incr key
复制代码
incr命令用于对值进行自增操作,返回情况有三种:
- 值不是整数,返回错误;
- 值是整数,返回自增后的结果;
- 键不存在,按照值为0自增,返回结果为1。 示例如下:
### 键 a 不存在
172.17.236.250:6379> exists a
(integer) 0
### 对不存在的键 a 进行自增,返回结果为 1
172.17.236.250:6379> incr a
(integer) 1
### 再次对 a 进行自增操作,返回结果为 2
172.17.236.250:6379> incr a
(integer) 2
### 值不是整数,报错
172.17.236.250:6379> set hello world
OK
172.17.236.250: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 increment
incrbyfloat key increment
172.17.236.250:6379> decr a
(integer) 1
172.17.236.250:6379> incrby a 5
(integer) 6
172.17.236.250:6379> decrby a 5
(integer) 1
172.17.236.250:6379> incrbyfloat a 4.9
"5.9"
复制代码
字符串不常用操作命令
追加值
append key value
复制代码
append
可以向字符串尾部追加值,如下所示:
172.17.236.250:6379> set hello world
OK
172.17.236.250:6379> get hello
"world"
172.17.236.250:6379> append hello redis
(integer) 10
172.17.236.250:6379> get hello
"worldredis"
复制代码
字符串长度
strlen key
复制代码
当前值为worldredis,所以返回长度为10(中文占用3个字节),如下:
172.17.236.250:6379> strlen hello
(integer) 10
复制代码
设置并返回原值
getset key value
复制代码
getset
和set
会设置值,不同的是,它会返回这个键原来的值,如下:
172.17.236.250:6379> getset hello world
(nil)
172.17.236.250:6379> getset hello redis
"world"
复制代码
设置指定位置的字符串
setrange key offeset value
复制代码
将"adcd"变成"ddcd":
172.17.236.250:6379> set redis adcd
OK
172.17.236.250:6379> setrange redis 0 d
(integer) 4
172.17.236.250:6379> get redis
"ddcd"
复制代码
获取部分字符串
getrange key start end
复制代码
start和end分别是开始和结束的偏移量,偏移量从0开始计算,如下:
172.17.236.250:6379> getrange redis 0 1
"dd"
复制代码
字符串类型命令时间复杂度表
内部编码
字符串的内部编码有3种:
- int:8个字节的长整型;
- embstr:小于等于39个字节的字符串;
- raw:大于39个字节的字符串。 Redis会根据当前值的类型和长度决定使用哪种内部编码实现。如下所示:
#### int 类型
172.17.236.250:6379> set key 81
OK
172.17.236.250:6379> object encoding key
"int"
#### embstr 类型
172.17.236.250:6379> set key hello,world
OK
172.17.236.250:6379> object encoding key
"embstr"
#### raw 类型
172.17.236.250:6379> set key qwertyuiopasdfghjklzxcvbnmqwertyuioplkjhgfdsaczvxbnm
OK
172.17.236.250:6379> object encoding key
"raw"
复制代码
典型使用场景
缓存功能
Redis作为缓存层,MySQL作为存储层,绝大部分请求的数据都是从Redis中获取。因为Redis有支撑高并发的特性,所以缓存能起到加速读写和降低后端压力的作用。如下图:
思路如下:- 定义获取用户基础信息函数
- 首先从Redis获取用户信息
- 若没有从Redis中获取到用户信息,需要从MySQL中进行获取,并且将结果写入Redis中,添加过期时间
- 伪代码如下:
UserInfo getUserInfo(long id) {
//定义键
userRedisKey = "user:info:"+id;
//从Redis获取值
value = redis.get(userRedisKey);
if(value!=null){
// 将值进行反序列化为UserInfo并返回结果
userInfo = deserialize(value);
return userInfo;
} else {
// 从MySQL获取用户信息
userInfo = mysql.get(id);
// 将userInfo序列化,设置3600秒过期时间,存入Redis
if (userInfo != null) {
redis.setex(userRediskey,3600,serialize(userInfo));
}
}
}
复制代码
注意:Redis没有命令空间,也没有对键名有强制要求(除了不能使用一些特殊字符)。键名要设计合理,有利于防止键冲突项目的可维护性。推荐使用"业务名: 对象名:id:[属性]"作为键名。可以在能描述键的含义下适当减少键的长度,从而减少键过长而造成的内存浪费。
计数
使用Redis作为计数的基础工具,他可以实现快速计数、查询缓存功能,同时数据可以异步落地到其他数据源。 伪代码如下:
long incrVideoCounter(long id) {
key = "video:playCount:"+id;
return redis.incr(key);
}
复制代码
在实际开发中,计数系统要考虑很多东西:防作弊、按照不同的维度计数,数据持久化到底层数据源等等。
共享session
一个分布式Web服务将用户的session信息保存在各自的服务器上,但是出于负载均衡的考虑,分布式服务会将用户的访问均衡到不同的服务器上,这样就导致用户刷新一次访问可能就会发现需要重新登录。如下图:
为了解决这个问题,开发人员可以使用Redis将用户的session进行集中的管理。在这种模式下只需要保证Redis是高可用和扩展性的,每次用户更新或者查询登录信息都可以直接从Redis种获取。如下图:限速
很多应用在每次进行登录时,会让用户输入手机验证码来确定是否是用户本人。为了让短信接口不被频繁访问,会限制用户在每分钟的获取验证码的频率,例如一分钟不能超过5次。如下图:
伪代码如下:phoneNum=""135xxxxxxxx;
key = "shortMsg:limit:"+phoneNum;
isExists = redis.set(key,1,"EX 60","NX");
if(isExists != null || redis.incr(key) <= 5) {
//通过
} else {
//限速
}
复制代码
例如某些网站不能在1秒之内访问超过n次也可以采用类似的思路。 除了以上几种应用场景,字符串还有很多的应用场景,这需要我们结合字符串提供的相应命令去决定怎样使用。