Redis开发与运维笔记_API的理解和使用
Redis提供了5种数据结构,理解每种数据结构的特点对于Redis开发运维非常重要,同时掌握Redis的单线程命令处理机制,会使数据结构和命令的选择事半功倍
参考书籍:Redis开发与运维
API的理解和使用
预备
全局命令
- 查看所有键
| 1 | keys * | 
- 键总数
| 1 | dbsize | 
dbsize命令直接获取Redis内置的键总数,复杂度是O(1)。keys会获取所有键,复杂度是O(n),当Redis中保存大量键时,线上环境应禁止使用。
- 检查键是否存在
| 1 | exists key | 
- 删除键
| 1 | del key [key ...] | 
- 键过期
| 1 | expire key seconds | 
ttl命令会返回键的过期时间。返回结果有以下几种:
- 大于等于0的整数:键剩余的过期时间
- -1:键没有设置过期时间
- -2:键不存在
- 键的数据结构类型
| 1 | type key | 
数据结构和内部编码
type命令返回的是当前键的数据结构,它们分别是:
string、hash、list、set、zset
实际上,每种数据结构都有自己底层的内部编码实现。
可以使用object encoding key命令获取当前键使用的内部编码。
 
 
Redis这样设计有两点好处:1)改进内部编码时,对外的数据结构和命令没有影响。2)不同的内部编码具有不同的优势。如:ziplist比较节省内存。
单线程架构
Redis使用了单线程架构和I/O多路复用模型[1]来实现高性能的内存数据库服务
单线程模型
Redis使用单线程处理命令,所以不会存在两条命令同时执行的情况,避免了并发问题。
为什么单线程这么快
通常来讲,单线程处理能力要比多线程差,为什么Redis这么快?
- 纯内存访问
- 非阻塞I/O,Redis使用epoll[2]作为I/O多路复用技术的实现
- 单线程避免了线程切换和竞争产生的消耗
单线程简化了数据结构和算法的实现,避免了线程切换和线程竞争的消耗。但是存在如下问题:单线程模型对每条命令的执行时间是有要求的。如果某个命令执行时间过长,会造成其他命令的阻塞。
字符串
字符串是Redis最基本的数据结构,字符串类型的值实际可以是字符串(简单的字符串、复杂的字符串[如Json、xml等]、数字[整数、浮点数]、甚至是二进制[图片、音频、视频]),但是值最大不能超过512M
命令
常用命令
- 设置值
| 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 | GET key | 
如果键不存在,返回
nil
- 批量设置值
| 1 | MSET key value [key value ...] | 
- 批量获取值
| 1 | MGET key [key ...] | 
批量操作可以节省网络开销时间。
- 计数
| 1 | INCR key | 
编程语言中常用CAS机制实现计数功能,存在一定CPU开销。Redis使用单线程机制则不存在这个问题
不常用命令
- 追加值 APPEND key value
- 字符串长度 STRLEN key
- 设置并返回值 GETSET key value
- 设置指定位置的字符 SETRANGE key offset value
- 获取部分字符串 GETRANGE key start end
内部编码
字符串的内部编码有三种:
- int:8个字节的长整型
- embstr:小于等于39个字节的字符串
- raw:大于39个字节的字符串
典型使用场景
- 缓存功能
| 1 | UserInfo getUserInfo(long id){ | 
- 计数
| 1 | long incrVideoCount(long id){ | 
- 共享Session
将Session保存在Redis实现多端共享。
- 限速
| 1 | // 60s内短信发送不得超过5次 | 
哈希
命令
- 设置值
- 获取值 HGET key field
- 删除field  HDEL key field [field ...]
- 计算field个数 HLEN key
- 批量设置或获取field-value
| 1 | HMGET key field [field ...] | 
- 判断field是否存在 HEXISTS key field
- 获取所有的field HKEYS key
- 获取所有的value HVALS key
- 获取所有的field-value  HGETALL key
- hincrby hincrbyfloat
| 1 | HINCRBY key field increment | 
- 计算value的字符串长度 HSTRLEN key field
列表
命令
| 操作类型 | 操作 | 
|---|---|
| 添加 | rpush、rpushx、lpush、lpushx、linsert | 
| 查 | lrange、lindex、llen | 
| 删除 | lpop、rpop、lrem、ltrim | 
| 修改 | lset | 
| 阻塞操作 | blpop、brpop | 
添加
- 从列表右侧添加: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]
使用场景
- 
消息队列 
- 
文章列表 - 文章有三个属性:title、timestamp、content
 1 
 2
 3
 4hmset acticle:1 title xx timestamp 1476536196 content xxxx 
 ...
 hmset acticle:k title yy timestamp 1476512536 content yyyy
 ...- 添加文章,key是user:{id}:articles
 1 
 2
 3
 4lpush user:1:acticles article:1 article:3 
 ...
 lpush user:k:acticles article:5
 ...- 分页获取文章,获取id为1的前10篇文章
 1 
 2
 3articles = 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(消息队列)
集合
命令
集合内操作
- 添加元素 SADD key member [member ...]
- 删除元素 SREM key member [member ...]
- 计算元素个数 SCARD key
- 判断元素是否在集合中 SISMEMBER key member
- 随机从集合中返回指定个数元素 SRANDMEMBER key [count]
- 从集合随机弹出元素 SPOP key [count]
- 获取所有元素 SMEMBERS key- 元素过多存在阻塞可能,可以使用SSCAN key cursor [MATCH pattern] [COUNT count]来完成
 
- 元素过多存在阻塞可能,可以使用
集合间操作
- 求多个集合的交集: SINTER key [key ...]
- 求多个集合的并集: SUINON key [key ...]
- 求多个集合的差集: SDIFF key [key ...]
- 将交集、并集、差集的结果保存: SINTERSTORE/SUINONSTORE/SDIFFSTORE destination key [key ...]
内部编码
- intset(整数集合): 集合中元素为整数且个数小于- set-max-intset-entries(512)时
- hashtable(哈希表)
使用场景
集合类型比较经典的使用场景是标签(tag)。
- 给用户添加标签
| 1 | sadd user:1:tags tag1 tag2 tag5 | 
- 给标签添加用户
| 1 | sadd tag1:users user:1 user:3 | 
用户和标签的关系应该在一个事务内执行。
- 删除用户下的标签
| 1 | srem user:1:tags tag1 tag5 | 
- 删除标签下的用户
| 1 | srem tag1:users user:1 | 
- 计算用户共同感兴趣的标签
| 1 | sinter user:1:tags user:2:tags | 
集合使用场景可以按照如下方法判断:
- sadd=Tagging(标签)
- spop/srandmember=Random item(生成随机数,比如抽奖)
- sadd+sinter=Social Graph(社交需求)
有序集合
和集合不同是,Sorted Set为每个元素设置一个
score作为排序的依据。
列表、集合和有序集合的区别:
数据结构 是否允许重复 是否有序 有序实现方式 应用场景 列表 是 是 索引下标 时间轴、消息队列等 集合 否 否 无 标签、社交等 有序集合 否 是 分值 排行榜系统、社交等 
命令
集合内
- 添加成员 zadd key score member [score member ...]
- 计算成员个数 zcard key
- 计算某元素分数 zscore key member
- 计算成员的排名
| 1 | zrank key member | 
- 删除成员 zrem key member [member ...]
- 增加成员的分数 zincrby key increment member
- 返回指定排名范围的成员
| 1 | zrange key start end [withscores] # 从低到高 | 
- 返回指定分数范围的成员
| 1 | zrangebyscore key min max [withscores] [limit offset count] # 从低到高 | 
- 返回指定分数范围成员个数 zcount key min max
- 删除指定排名内的升序元素 zremrangebyrank key start end
- 删除指定分数范围的成员 zremrangebyscore key min max
集合间
- 
交集 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
 
- 
并集 zunionstore destination numkeys key [key ...] [weights weight [weight ...]] [aggregate sum|min|max]
- 参数与交集参数一致。
内部编码
- ziplist(压缩列表)
- skiplist(跳跃表)
使用场景
排序集合典型的应用场景就是排行榜系统。例如视频网站需要对用户上传的视频做排行榜,榜单的维度可能是多个方面的:按照时间、按照播放数量、按照获得的赞数。
- 添加用户赞数
| 1 | zadd user:ranking:2016_03_15 mike 3 # mike 上传视频 获得了3个赞 | 
- 取消用户赞数
| 1 | zrem user:ranking:2016_03_15 mike # 用户mike视频注销了 | 
- 展示获取赞数最多的十个用户
| 1 | zrevrangebyrank user:ranking:2016_03_15 0 9 | 
- 展示用户信息以及用户分数
| 1 | hgetall user:info:tom | 
键管理
单个键管理
- 键重命名 rename key newkey
- 键的新名称存在时,值会被覆盖,可以使用renamenx key newkey命令避免
- 重命名期间会执行del命令删除旧的键,当键对应的值比较大时存在阻塞风险
- 随机返回一个键 randomkey
- 键过期
 除了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的组合,不但是原子执行,同时减少了一次网络通讯的时间
 
- 迁移键
- move move key db
- dump+restore
- 在源Redis上,执行dump key将对应键值序列化,格式采用RDB格式
- 在目标Redis上,restore命令将上面序列化的值进行复原,其中ttl参数代表过期时间,如果ttl=0代表没有过期时间
- 整个过程非原子操作。且开启了两个客户端分别进行操作
 
- 在源Redis上,执行
- 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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 定义 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过程中,如果发生键值的修改、删除、增加等,可能会发生问题。
数据库管理
- 切换数据库 select dbIndex
- 清除数据库 flushdb/flushall
本节内容回顾
- Redis提供了5种数据结构,每种数据结构都有多种内部编码实现
- 纯内存存储、IO多路复用技术、单线程架构是造就Redis高性能的三个因素
- 由于Redis的单线程架构,所以需要每个命令能被快速执行完,否则会存在阻塞Redis的可能,理解Redis单线程命令处理机制是开发和运维Redis的核心之一
- 批量操作(例如mget、mset、hmset等)能够有效提高命令执行的效率,但要注意每次批量操作的个数和字节数
- 了解每个命令的时间复杂度在开发中至关重要,例如在使用keys、hgetall、smembers、zrange等时间复杂度较高的命令时,需要考虑数据规模对于Redis的影响
- persist命令可以删除任意类型键的过期时间,但是set命令也会删除字符串类型键的过期时间,这在开发时容易被忽视
- move、dump+restore、migrate是Redis发展过程中三种迁移键的方式,其中move命令基本废弃,migrate命令用原子性的方式实现了dump+restore,并且支持批量操作,是Redis Cluster实现水平扩容的重要工具。
- scan命令可以解决keys命令可能带来的阻塞问题,同时Redis还提供了hscan、sscan、zscan渐进式地遍历hash、set、zset。
- I/O多路复用模型详见 Redis 和 IO 多路复用 ↩︎