Redis开发与运维笔记_API的理解和使用

Redis提供了5种数据结构,理解每种数据结构的特点对于Redis开发运维非常重要,同时掌握Redis的单线程命令处理机制,会使数据结构和命令的选择事半功倍

参考书籍:Redis开发与运维


API的理解和使用

预备

全局命令

  1. 查看所有键
1
keys *
  1. 键总数
1
dbsize

dbsize命令直接获取Redis内置的键总数,复杂度是O(1)keys会获取所有键,复杂度是O(n),当Redis中保存大量键时,线上环境应禁止使用。

  1. 检查键是否存在
1
exists key
  1. 删除键
1
del key [key ...]
  1. 键过期
1
expire key seconds

ttl命令会返回键的过期时间。返回结果有以下几种:

  • 大于等于0的整数:键剩余的过期时间
  • -1:键没有设置过期时间
  • -2:键不存在
  1. 键的数据结构类型
1
type key

数据结构和内部编码

type命令返回的是当前键的数据结构,它们分别是:string、hash、list、set、zset
实际上,每种数据结构都有自己底层的内部编码实现。
可以使用object encoding key命令获取当前键使用的内部编码。

Redis这样设计有两点好处:1)改进内部编码时,对外的数据结构和命令没有影响。2)不同的内部编码具有不同的优势。如:ziplist比较节省内存。

单线程架构

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

单线程模型

Redis使用单线程处理命令,所以不会存在两条命令同时执行的情况,避免了并发问题。

为什么单线程这么快

通常来讲,单线程处理能力要比多线程差,为什么Redis这么快?

  1. 纯内存访问
  2. 非阻塞I/O,Redis使用epoll[2]作为I/O多路复用技术的实现
  3. 单线程避免了线程切换和竞争产生的消耗

单线程简化了数据结构和算法的实现,避免了线程切换和线程竞争的消耗。但是存在如下问题:单线程模型对每条命令的执行时间是有要求的。如果某个命令执行时间过长,会造成其他命令的阻塞。

字符串

字符串是Redis最基本的数据结构,字符串类型的值实际可以是字符串(简单的字符串、复杂的字符串[如Json、xml等]、数字[整数、浮点数]、甚至是二进制[图片、音频、视频]),但是值最大不能超过512M

命令

常用命令

  1. 设置值
1
SET key value [expiration EX seconds|PX milliseconds] [NX|XX]
  • EX seconds:秒级过期时间,功能等同SETEX key seconds value命令
  • PX milliseconds:毫秒级过期时间
  • NX:键不存在则设置成功,功能等同SETNX key value命令
  • XX:键存在则设置成功

setnx在多个客户端执行时,只有一个客户端才能成功,可以作为分布式锁的一种实现方式。Redis官方方案

  1. 获取值
1
GET key

如果键不存在,返回nil

  1. 批量设置值
1
MSET key value [key value ...]
  1. 批量获取值
1
MGET key [key ...]

批量操作可以节省网络开销时间。

  1. 计数
1
2
3
4
5
INCR key
INCRBY key increment
DECR key
DECRBY key decrement
INCRBYFLOAT key increment

编程语言中常用CAS机制实现计数功能,存在一定CPU开销。Redis使用单线程机制则不存在这个问题

不常用命令

  1. 追加值 APPEND key value
  2. 字符串长度 STRLEN key
  3. 设置并返回值 GETSET key value
  4. 设置指定位置的字符 SETRANGE key offset value
  5. 获取部分字符串 GETRANGE key start end

内部编码

字符串的内部编码有三种:

  1. int:8个字节的长整型
  2. embstr:小于等于39个字节的字符串
  3. raw:大于39个字节的字符串

典型使用场景

  1. 缓存功能
1
2
3
4
5
6
7
8
9
10
11
12
13
14
UserInfo getUserInfo(long id){
String userInfoKey = "user:info" + id;
String value = redis.get(userInfoKey);
UserInfo userInfo;
if(value != null){
userInfo = deserialize(value);
}else{
userInfo = mysql.get(id);
if(userInfo != null){
redis.setex(userInfoKey, 3600, serialize(userInfo));
}
}
return userInfo;
}
  1. 计数
1
2
3
4
long incrVideoCount(long id){
key = "video:playCount:" + id;
return redis.incr(key);
}
  1. 共享Session

将Session保存在Redis实现多端共享。

  1. 限速
1
2
3
4
5
6
7
8
9
10
// 60s内短信发送不得超过5次
boolean phoneVerify(String phoneNumber){
key = "shortMsg:limit:" + phoneNumber;
isExists = redis.set(key,1,"EX 60","NX");
if(isExists != null || redis.incr(key) <= 5){
// 通过
}else{
// 限速
}
}

哈希

命令

  1. 设置值
  2. 获取值 HGET key field
  3. 删除field HDEL key field [field ...]
  4. 计算field个数 HLEN key
  5. 批量设置或获取field-value
1
2
HMGET key field [field ...]
HMSET key field value [field value ...]
  1. 判断field是否存在 HEXISTS key field
  2. 获取所有的field HKEYS key
  3. 获取所有的value HVALS key
  4. 获取所有的field-value HGETALL key
  5. hincrby hincrbyfloat
1
2
HINCRBY key field increment
HINCRBYFLOAT key field increment
  1. 计算value的字符串长度 HSTRLEN key field

列表

命令

操作类型 操作
添加 rpushrpushxlpushlpushxlinsert
lrangelindexllen
删除 lpoprpoplremltrim
修改 lset
阻塞操作 blpopbrpop

添加

  • 从列表右侧添加: RPUSH key value [value ...]
  • 当列表存在时,从右侧添加:RPUSHX key value
  • 在首个指定元素pivot(前/后)添加元素:LINSERT key BEFORE|AFTER pivot value

  • 获取指定范围内的列表元素:LRANGE key start stop
  • 根据索引获取元素:LINDEX key index
  • 获取列表长度:LLEN key

删除

  • 从列表左侧弹出元素:LPOP key
  • 删除指定元素:LREM key count value
    • count = 0:删除所有等于指定值的元素
    • count < 0:从右到左删除count个元素
    • count > 0:从左到右删除count个元素
  • 删除指定范围之外的元素:LTRIM key start stop

修改

  • 修改指定索引的元素:LSET key index value

阻塞操作

  • BLPOP key [key ...] timeout
  • BRPOP key [key ...] timeout
    • lpop/rpop的阻塞版本,当列表内无元素时,客户端会阻塞等待,直到有元素可以弹出
  • 如果是多个键,brpop会从左到右遍历键,一旦有一个键可以弹出元素,客户端立即返回。
  • 多个客户端阻塞获取同一个key,最先执行获取的最先获取到

内部编码

列表类型的内部编码有两种:

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

Redis3.2版本提供了quicklist内部编码,简单地说它是以一个ziplist为节点的linkedlist,它结合了ziplist和linkedlist两者的优势,为列表类型提供了一 种更为优秀的内部编码实现,它的设计原理可以参考Redis的另一个作者 Matt Stancliff的博客[3]

使用场景

  1. 消息队列

  2. 文章列表

    1. 文章有三个属性:title、timestamp、content
    1
    2
    3
    4
    hmset acticle:1 title xx timestamp 1476536196 content xxxx
    ...
    hmset acticle:k title yy timestamp 1476512536 content yyyy
    ...
    1. 添加文章,key是user:{id}:articles
    1
    2
    3
    4
    lpush user:1:acticles article:1 article:3
    ...
    lpush user:k:acticles article:5
    ...
    1. 分页获取文章,获取id为1的前10篇文章
    1
    2
    3
    articles = lrange user:1:articles 0 9
    for article in {articles}
    hgetall {article}
  • 分页获取文章个数过多时,多次hgetall可能存在性能问题
  • lrange 在列表两端时性能较好,列表数据过大时,可以考虑做二次拆分减小列表长度

列表使用场景可参考以下口诀:

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

集合

命令

集合内操作

  1. 添加元素 SADD key member [member ...]
  2. 删除元素 SREM key member [member ...]
  3. 计算元素个数 SCARD key
  4. 判断元素是否在集合中 SISMEMBER key member
  5. 随机从集合中返回指定个数元素 SRANDMEMBER key [count]
  6. 从集合随机弹出元素 SPOP key [count]
  7. 获取所有元素 SMEMBERS key
    • 元素过多存在阻塞可能,可以使用SSCAN key cursor [MATCH pattern] [COUNT count]来完成

集合间操作

  1. 求多个集合的交集: SINTER key [key ...]
  2. 求多个集合的并集: SUINON key [key ...]
  3. 求多个集合的差集: SDIFF key [key ...]
  4. 将交集、并集、差集的结果保存: SINTERSTORE/SUINONSTORE/SDIFFSTORE destination key [key ...]

内部编码

  • intset(整数集合): 集合中元素为整数且个数小于set-max-intset-entries(512)
  • hashtable(哈希表)

使用场景

集合类型比较经典的使用场景是标签(tag)。

  1. 给用户添加标签
1
2
3
4
5
sadd user:1:tags tag1 tag2 tag5
sadd user:2:tags tag2 tag3 tag5
...
sadd user:k:tags tag1 tag2 tag4
...
  1. 给标签添加用户
1
2
3
4
5
sadd tag1:users user:1 user:3
sadd tag2:users user:1 user:2 user:3
...
sadd tagk:users user:1 user:2
...

用户和标签的关系应该在一个事务内执行。

  1. 删除用户下的标签
1
2
srem user:1:tags tag1 tag5
...
  1. 删除标签下的用户
1
2
3
srem tag1:users user:1
srem tag5:users user:1
...
  1. 计算用户共同感兴趣的标签
1
sinter user:1:tags user:2:tags

集合使用场景可以按照如下方法判断:

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

有序集合

和集合不同是,Sorted Set为每个元素设置一个score作为排序的依据。

列表、集合和有序集合的区别:

数据结构 是否允许重复 是否有序 有序实现方式 应用场景
列表 索引下标 时间轴、消息队列等
集合 标签、社交等
有序集合 分值 排行榜系统、社交等

命令

集合内

  1. 添加成员 zadd key score member [score member ...]
  2. 计算成员个数 zcard key
  3. 计算某元素分数 zscore key member
  4. 计算成员的排名
1
2
zrank key member
zrevrank key member
  1. 删除成员 zrem key member [member ...]
  2. 增加成员的分数 zincrby key increment member
  3. 返回指定排名范围的成员
1
2
zrange key start end [withscores] # 从低到高
zrevrange key start end [withscores] # 从高到低
  1. 返回指定分数范围的成员
1
2
zrangebyscore key min max [withscores] [limit offset count] # 从低到高
zrevrangebyscore key max min [withscores] [limit offset count] # 从高到低
  1. 返回指定分数范围成员个数 zcount key min max
  2. 删除指定排名内的升序元素 zremrangebyrank key start end
  3. 删除指定分数范围的成员 zremrangebyscore key min max

集合间

  1. 交集 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
  2. 并集 zunionstore destination numkeys key [key ...] [weights weight [weight ...]] [aggregate sum|min|max]

  • 参数与交集参数一致。

内部编码

  • ziplist(压缩列表)
  • skiplist(跳跃表)

使用场景

排序集合典型的应用场景就是排行榜系统。例如视频网站需要对用户上传的视频做排行榜,榜单的维度可能是多个方面的:按照时间、按照播放数量、按照获得的赞数。

  1. 添加用户赞数
1
2
zadd user:ranking:2016_03_15 mike 3 # mike 上传视频 获得了3个赞
zincrby user:ranking:2016_03_15 mike 1 # mike的视频后续又获得了一个赞
  1. 取消用户赞数
1
zrem user:ranking:2016_03_15 mike # 用户mike视频注销了
  1. 展示获取赞数最多的十个用户
1
zrevrangebyrank user:ranking:2016_03_15 0 9
  1. 展示用户信息以及用户分数
1
2
3
hgetall user:info:tom
zscore user:ranking:2016_03_15 tom
zrank user:ranking:2016_03_15 tom

键管理

单个键管理

  1. 键重命名 rename key newkey
  • 键的新名称存在时,值会被覆盖,可以使用renamenx key newkey命令避免
  • 重命名期间会执行del命令删除旧的键,当键对应的值比较大时存在阻塞风险
  1. 随机返回一个键 randomkey
  2. 键过期
    除了expire、ttl命令以外,Redis还提供了 expireat、pexpire、pexpireat、pttl、persist等一系列命令
  • expire key seconds:键在seconds秒后过期
  • expireat key timestamp:键在秒级时间戳timestamp后过期
  • pttl: 查询键的剩余时间(毫秒级)
  • pexpire key milliseconds:键在milliseconds毫秒后过期
  • pexpireat key milliseconds-timestamp键在毫秒级时间戳timestamp后过期
  • 关于Redis相关过期命令,需要注意以下几点:
    • 如果expire key的键不存在,返回结果为0
    • 如果过期时间为负值,键会立即被删除,犹如使用del命令一样
    • persist命令可以将键的过期时间清除
    • 对于字符串类型键,执行set命令会去掉过期时间,这个问题很容易在开发中被忽视
    • Redis不支持二级数据结构(例如哈希、列表)内部元素的过期功能
    • setex命令作为set+expire的组合,不但是原子执行,同时减少了一次网络通讯的时间
  1. 迁移键
  • move move key db
  • dump+restore
    • 在源Redis上,执行dump key将对应键值序列化,格式采用RDB格式
    • 在目标Redis上,restore命令将上面序列化的值进行复原,其中ttl参数代表过期时间,如果ttl=0代表没有过期时间
    • 整个过程非原子操作。且开启了两个客户端分别进行操作
  • migrate migrate host port key|"" destination-db timeout [copy] [replace] [keys key [key ...]]
    • host:目标Redis的IP地址
    • port:目标Redis的端口
    • key|“”:Redis3.0.6后,支持迁移多个键,此处填""
    • destination-db:目标Redis的数据库索引
    • timeout:迁移的超时时间(单位为毫秒)
    • [copy]:如果添加此选项,迁移后并不删除源键
    • [replace]:如果添加此选项,migrate不管目标Redis是否存在该键都会 正常迁移进行数据覆盖
    • [keys key[key…]]:迁移多个键,例如要迁移key1、key2、key3,此处填 写keys key1 key2 key3

move、dump+restore、migrate三个命令比较

命令 作用域 原子性 支持多个键
move Redis实例内部
dump + restore Redis实例之间
migrate Redis实例之间

遍历键

全量遍历键

keys pattern
pattern支持glob风格的通配符

  • *:匹配任意字符
  • .:匹配一个字符
  • []:匹配部分字符,[1,3]代表1或3,[1-10]代表匹配1-10中任意一个数字
  • \x:用来做转义,例如要匹配*.

渐进式遍历

scan cursor [match pattern] [count number]

  • cursor:游标,从0开始,遍历至游标为0结束。每次遍历会返回当前游标的值
    match pattern:可选参数,它的作用的是做模式的匹配,这点和keys的 模式匹配很像
    count number:可选参数,它的作用是表明每次要遍历的键个数,默认值是10,此参数可以适当增大

除了scan外,Redis还提供了面向哈希类型、集合类型、有序集合的扫 描遍历命令,hscan、sscan、zscan

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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 && element.size() > 0 ){
// 批量删除
redis.srem(key,elements)
}
// 获取新的游标
cursor = scanResult.getStringCursor();
// 如果游标为0表示遍历结束
if("0".equals(cursor)){
break;
}
}
  • 当scan过程中,如果发生键值的修改、删除、增加等,可能会发生问题。

数据库管理

  1. 切换数据库 select dbIndex
  2. 清除数据库 flushdb/flushall

本节内容回顾

  1. Redis提供了5种数据结构,每种数据结构都有多种内部编码实现
  2. 纯内存存储、IO多路复用技术、单线程架构是造就Redis高性能的三个因素
  3. 由于Redis的单线程架构,所以需要每个命令能被快速执行完,否则会存在阻塞Redis的可能,理解Redis单线程命令处理机制是开发和运维Redis的核心之一
  4. 批量操作(例如mget、mset、hmset等)能够有效提高命令执行的效率,但要注意每次批量操作的个数和字节数
  5. 了解每个命令的时间复杂度在开发中至关重要,例如在使用keys、hgetall、smembers、zrange等时间复杂度较高的命令时,需要考虑数据规模对于Redis的影响
  6. persist命令可以删除任意类型键的过期时间,但是set命令也会删除字符串类型键的过期时间,这在开发时容易被忽视
  7. move、dump+restore、migrate是Redis发展过程中三种迁移键的方式,其中move命令基本废弃,migrate命令用原子性的方式实现了dump+restore,并且支持批量操作,是Redis Cluster实现水平扩容的重要工具。
  8. scan命令可以解决keys命令可能带来的阻塞问题,同时Redis还提供了hscan、sscan、zscan渐进式地遍历hash、set、zset。

  1. I/O多路复用模型详见 Redis 和 IO 多路复用 ↩︎

  2. epoll模型详见 epoll ↩︎

  3. https://matt.sh/redis-quicklist ↩︎