Redis 是一个使用 C 语言开发的开源的高性能的 Key-Value 数据库。常用于数据缓存和高访问负载,Redis 通过提供多种 Value 数据类型来适应不同场景下的存储需求,目前 Redis 支持的 Value 数据类型有 string、hash、list、set、zset 类型。据 Redis 官方测试,有 50 个并发程序来执行 10 万次请求,Redis 读的速度达到了 11 万次/秒,写的速度达到了 8.1 万次/秒。
Redis 的八大特性:
- 速度快(官方给的数字是单机 10W OPS,速度快的原因:数据存在内存中;Redis 是 C 语言实现的);
- 支持持久化(断电不丢数据);
- 多种数据结构(Strings/Blobs/Bitmaps、Hash Tables(objects!)、Linked Lists、Sets、Sorted Sets,除了这五种,在 Redis 迭代中,还提供了一些其他的数据结构,例如 HyperLogLog(超小内存唯一值计数,12k,缺点是计数不太准确)、GEO(地理信息定位);
- 支持多种编程语言(Java、php、Python、Ruby、Lua、Node.js);
- 功能丰富(Redis 除了提供五种数据结构以外,还提供了发布订阅、Lua 脚本、事务、pipeline);
- 简单(核心代码只有2W多行,不依赖外部库,单线程模型);
- 主从复制;
- 高可用分布式支持(V2.8 版本开始提供了 Redis Sentinel 支持高可用,V3.0 版本开始提供了 Redis Cluster 支持分布式)。
计算机的存储介质从快到慢(从小到大,从昂贵到廉价)依次包括:
Redis 是一款基于内存的数据库,那么对于内存的使用是至关重要的,我们在使用一种数据结构的时候,以空间换取时间的话,可以使用一些压缩的结构,比如 hash 里面会有 ziplist。
实际上对于 Redis 源码内部有一个 redisObject 这样的对象,它会有很多属性,其中比较重要的属性有两个,第一个就是数据类型(type),第二个就是编码方式(encoding),除了这两种还有数据指针(ptr)、虚拟内存(vm)和其他信息。
1.Redis API
Redis 的操作都是原子性的。
Redis 基本类型包括 string(字符串)、hash(哈希)、list(列表)、set(集合)、zset(有序集合)五种,除了这五种,还有一些更高级的 Redis 类型,比如用来计数的 HyperLogLog,用于支持存储地理位置信息的 Geo(地理信息定位)。
1.string
字符串键值结构:key-value。本质上 value 都是二进制的,字符串类型的 value 有一个限制是不能大于 512 MB。
API:
API | 说明 | 时间复杂度 |
---|---|---|
get key | 获取 key 对应的 value | O(1) |
set key value | 不管 key 是否存在,都设置 key-value | O(1) |
setnx key value | 必须 key 不存在,才设置 key-value | O(1) |
setxx key value | 必须 key 存在,才设置 key-value | O(1) |
del key | 删除 key-value | O(1) |
mget key1 key2 key3 | 批量获取 key,原子操作,1 次 mget 时间 = 1 次网络时间 + n 次命令时间 | O(n) |
mset key1 value1 key2 value2 key3 value3 | 批量设置 key-value,原子操作 | O(n) |
getset key newvalue | set key newvalue 并返回旧的 value | O(1) |
append key value | 将 value 追加到旧的 value | O(1) |
strlen key | 返回字符串的长度(UTF-8 中每个英文占 1 个字节,每个中文占 2 个字节) | O(1) |
getrange key start end | 获取字符串指定下标所有的值 | O(1) |
setrange key index value | 设置指定下标所有对应的值 | O(1) |
整型操作增减的 API:
API | 说明 | 时间复杂度 |
---|---|---|
incr key | key 自增 1,如果 key 不存在,自增后 get key = 1 | O(1) |
decr key | key 自减 1,如果 key 不存在,自减后 get key = -1 | O(1) |
incrby key k | key 自增 k,如果 key 不存在,自增后 get key = k | O(1) |
decrby key k | key 自减 k,如果 key 不存在,自减后 get key = -k | O(1) |
浮点型操作的 API:
API | 说明 | 时间复杂度 |
---|---|---|
incrbyfloat key value | 增加 key 对应的值 value | O(1) |
使用场景:缓存、计数器、分布式锁、分布式 id 生成器等。
2.hash
哈希键值结构:key-field-value,值是分为两部分的,属性项 field 和属性值 value,同一个 key 中,field 不能相同,value 可以相同。对比字符串类型,哈希类型的优点是更为直观、节省空间、可以部分更新,缺点是编程稍微复杂、TTL 不好控制(只能对 key 设置过期时间,无法对 field 设置过期时间)。
API(哈希类型的 API 都是以 h 开头的):
API | 说明 | 时间复杂度 |
---|---|---|
hget key field | 获取 hash key 对应的 field 的 value | O(1) |
hset key field value | 设置 hash key 对应的 field 的 value | O(1) |
hsetnx key field value | 设置 hash key 对应的 field 的 value,如果 field 已经存在,则失败 | O(1) |
hdel key field | 删除 hash key 对应的 field 的 value | O(1) |
hexists key field | 判断 hash key 是否有 field | O(1) |
hlen key | 获取 hash key field 的数量 | O(1) |
hmget key field1 field2 … fieldN | 批量获取 hash key 的一批 field 对应的值 | O(n) |
hmset key field1 value1 field2 value2 … fieldN valueN | 批量设置 hash key 的一批 field value | O(n) |
hgetall key | 返回 hash key 对应所有的 field 和 value,小心使用 | O(n) |
hvals key | 返回 hash key 对应所有 field 的 value | O(n) |
hkeys key | 返回 hash key 对应所有的 field | O(n) |
hincrby key field intCounter | hash key 对应的 field 的 value 自增 intCounter | O(1) |
hincrbyfloat key field intCounter | hincrby 浮点数版本 | O(1) |
使用场景:记录网站每个用户个人主页的访问量。
4.list
列表键值结构:key-elements(elements 由 element 组成,例如 a - b - c - d - e - f,可以看出 llen = 6),列表值是有序的且可以重复的,从左右两边插入、弹出。
API(列表类型的 API 都是以 l 开头的):
API | 说明 | 时间复杂度 |
---|---|---|
rpush key value1 value2 …valueN | 从 key 对应的列表右端插入值(1~N个) | O(1~N) |
lpush key value1 value2 …valueN | 从 key 对应的列表左端插入值(1~N个) | O(1~N) |
linsert key before/after value newValue | 在 list 指定的值前/后插入 newValue | O(1) |
rpop key | 删除操作,从 key 对应的列表右侧弹出一个 item | O(1) |
brpop key | rpop 阻塞版本,timeout 是阻塞超时时间,timeout=0 为永远不阻塞 | O(1) |
lpop key | 删除操作,从 key 对应的列表左侧弹出一个 item | O(1) |
blpop key | lpop 阻塞版本,timeout 是阻塞超时时间,timeout=0 为永远不阻塞 | O(1) |
lrem key count value | 根据 count 值,从列表中删除所有 value 相等的项; (1)count >0,从左到右,删除最多 count 个 value 相等的项; (2)count<0,从右到左,删除最多 Math.abs(count) 个 value 相等的项; (3)count=0,删除所有value相等的项 | O(n) |
ltrim key start end | 按照索引范围修剪列表 | O(n) |
lrange key start end | 获取列表指定索引范围所有 item,包含 start 和 end | O(n) |
lindex key index | 获取列表指定索引的item | O(n) |
llen key | 获取列表长度 | O(1) |
lset key index newValue | 设置列表指定索引值为 newValue | O(n) |
使用场景:比如微博,将关注的用户的最新微博按照从新到旧的顺序排列,还有分页(lrange 命令)等。如果想要实现一个栈的功能,可以使用 lpush + lpop 命令;如果想要实现一个队列,可以使用 lpush + rpop 命令;如果要想控制一个有固定数量的列表,可以使用 lpush + ltrim;如果想要实现一个消息队列,可以使用 lpush + brpop 命令。
5.set
集合键值结构:key-values(values 由 element 组成,例如 b、a、c),集合中的元素是无序的且不可重复的,同时集合支持集合间操作。
集合内 API:
API | 说明 | 时间复杂度 |
---|---|---|
sadd key element | 向集合 key 添加 element(如果 element 已经存在,则添加失败) | O(1) |
srem key element | 将集合 key 中的 element 移除掉 | O(1) |
scard key | 获取集合 key 元素个数 | O(1) |
sismember key element | 判断 element 是否在集合 key 中 | |
srandmember key count | 从集合中随机取出 count 个元素 | |
spop key | 从集合中随机弹出(删除)一个元素 | |
smembers key | 获取集合中的所有元素,返回结果是无序的,小心使用 | O(n) |
使用场景:抽奖系统、标签(sadd 命令)、随机数场景(spop/srandmember)。
集合间 API:
API | 说明 | 时间复杂度 |
---|---|---|
sdiff key1 key2 | 差集 | |
sinter key1 key2 | 交集 | |
sunion key1 key2 | 并集 | |
sdiff/sinter/sunion + store destkey … | 将差集/交集/并集结果保存在 destkey 中 |
使用场景:例如微博中共同关注的好友。
6.zset
通过分数来为集合中的成员进行从小到大的排序,实际开发中用的比较少。数据结构:???
集合键值结构:key-values(values 是由 score 和 element 组成,score 是可以重复的,element 不可重复,例如 (1, a) - (2, b) - (3, c)),集合中的元素是有序的且不可重复的,同时集合支持集合间操作。
有序集合类型相对于集合类型,普遍 API 时间复杂度要更高。
重要 API:
API | 说明 | 时间复杂度 |
---|---|---|
zadd key score element(可以是多对) | 添加 score 和 element | O(logN) |
zrem key element(可以是多个) | 删除元素 | O(1) |
zscore key element | 获取元素的分数 | O(1) |
zincrby key increScore element | 增加或减少元素的分数 | O(1) |
zcard key | 返回元素的总个数 | O(1) |
zrange key start end [WITHSCORES] | 返回指定索引范围内的升序元素[分值] | O(log(n)+m),n 指有序集合中元素的个数,m指要获取指定索引范围内元素的个数 |
zrangebyscore key minScore maxScore [WITHSCORES] | 返回指定分数范围内的升序元素[分值] | O(log(n)+m) |
zcount key minScore maxScore | 返回有序集合内在指定分数范围内的个数 | O(log(n)+m) |
zremrangebyrank key start end | 删除指定排名内的升序元素 | O(log(n)+m) |
zremrangebyscore key start end | 删除指定分数内的升序元素 | O(log(n)+m) |
zrevrank | ||
zrevrange | ||
zrevrangebyscore | ||
zinterstore | ||
zunionstore |
使用场景:排行榜。
7.HyperLogLog
更加极端的一种节省内存的数据结构。
1、新的数据结构?
基于 HyperLogLog 算法,使用极小空间完成独立数量统计。不是新的数据结构,本质上还是字符串。
2、三个命令
API | 说明 | 时间复杂度 |
---|---|---|
pfadd key element [element…] | 向 HyperLogLog 添加元素 | |
pfcount key [key…] | 计算 HyperLogLog 的独立总数 | |
pfmerge destkey sourcekey [sourcekey…] | 合并多个 HyperLogLog,并存储到到 destkey |
3、内存消耗
百万独立用户:1 天 15KB,1 个月 450 KB。相对于 set 和 Bitmap,消耗内存更小。
4、使用建议
是否能容忍错误?HyperLogLog 是存在错误率的,官方给出 HyperLogLog 错误率在 0.81%;
是否需要单条数据?HyperLogLog 无法取出单条数据。
8.GEO
Redis 3.2 添加的一个特性,用于存储经纬度,计算两地距离,范围计算等。应用场景:周围的酒店等功能。GEO 是使用 zset 实现的(type geoKey = zset)。
1、2 个城市经纬度
城市 | 经度 | 纬度 | 简称 |
---|---|---|---|
北京 | 116.28 | 39.55 | beijing |
天津 | 117.12 | 39.08 | tianjin |
2、相关命令
API | 说明 | 时间复杂度 |
---|---|---|
geo key longitude latitude member [longitude latitude member…] | 增加地理位置信息,member 指标识 | |
zrem key member | 删除 member 位置信息 | |
geopos key member [member…] | 获取地理位置信息 | |
geodist key member1 member2 [unit] | 获取两个地理位置的距离,unit:m(米)、km(千米)、mi(英里)、ft(尺) | |
georadius key longitude latitude radiusm/km/ft/mi [withcoord] [withdist] [withhash] [COUNT count] [asc/desc] [store key] [storedist key] | 获取指定经纬度范围内的地理位置信息集合 | |
georadiusbymember key member radiusm/km/ft/mi [withcoord] [withdist] [withhash] [COUNT count] [asc/desc] [store key] [storedist key] | 获取指定 member 位置范围内的地理位置信息集合 |
withcoord:返回结果中包含经纬度。
withdist:返回结果中包含距离中心节点位置。
withhash:返回结果中包含 geohash。
COUNT count:指定返回结果的数量。
asc/desc:返回结果按照距离中心节点的距离做升序或者降序。
store key:将返回结果的地理位置信息保存到指定键。
storedist key:将返回结果距离中心节点的距离保存到指定键。
2.Redis 其他功能
Redis 除了提供五种数据结构之外,还提供了很多其他的功能,像慢查询、pipeline(流水线)、发布订阅、Bitmap、HyperLogLog、GEO 的功能。
1.慢查询
帮助找到系统中瓶颈的命令。
1、客户端请求的生命周期:
- 客户端发送命令到 Redis;
- Redis 中命令排队;
- Redis 执行命令;
- Redis 返回结果到客户端。
慢查询发生在第 3 阶段,客户端超时不一定慢查询,但慢查询是客户端超时的一个可能因素。
2、两个配置:
slowlog-max-len,默认值 128
首先慢查询是一个先进先出的固定长度的保存在内存中的队列(list 实现的),不会自己持久化。
slowlog-log-slower-than,默认值 10000
慢查询阈值(单位:微秒),slowlog-log-slower-than=0 表示记录所有命令。
动态配置(设置是 config set,获取是 config get):
config set slowlog-max-len 1000
config set slowlog-log-slower-than 1000
3、三个命令:
API | 说明 | 时间复杂度 |
---|---|---|
slowlog get [n] | 获取慢查询队列 | |
slowlog len | 获取慢查询队列长度 | |
slowlog reset | 清空慢查询队列 |
4、运维经验:
slowlog-max-len 不要设置过大,默认 10 ms,通常设置 1 ms;slowlog-log-slower-than 不要设置过小,通常设置 1000 左右;定期持久化慢查询。
2.pipeline(流水线)
1 次时间 = 1 次网络时间 + 1 次命令时间,n 次时间 = n 次网络时间 + n 次命令时间。pipeline 将一批命令进行打包,然后在服务端进行批量计算,按顺序将结果返回给我们,所以 1 次 pipeline(n 条命令)时间 = 1 次网络时间 + n 次命令时间,可以大大减少网络时间的开销,提高客户端的效率。这就是 pipeline。
两点需要注意下:Redis 的命令时间是微秒级别的;pipeline 每次条数要控制(网络成为了瓶颈,光速=30000公里/秒,光纤传输速度约等于光速的2/3)。
1、客户端实现:
pipeline-jedis 实现:
Jedis jedis = new Jedis("127.0.0.1", 6379);
Pipeline pipeline = jedis.pipelined();
for (int i = 0; i < 10000; i++) {
pipeline.hset("hashkey:" + i, "field" + i, "value" + i);
}
pipeline.syncAndReturnAll();
2、与原生操作对比
原生操作是一个原子操作,即在 Redis 服务端是有这样原生的命令的,pipeline 到 Redis 会进行命令拆分,不是一个原子命令,但是返回结果是顺序的。
3、使用建议
注意每次 pipeline 携带数据量;
pipeline 每次只能作用在一个 Redis 节点上,pipeline 是不允许使用在多个 Redis 节点上的;
3.发布订阅
实现发布订阅的功能。
1、角色
分为发布者(publisher)、订阅者(subscriber)、频道(channel)。
2、发布订阅通信模型
需要注意的是,如果发布者已经发布一条消息到一个频道了,新的订阅者订阅频道是收不到之前的消息的,无法做消息的堆积,因为 Redis 不是做一个真正的消息队列或者发布订阅专用的工具,它只是提供了这样一个功能。
3、API
API | 说明 | 时间复杂度 |
---|---|---|
publish channel message | 发布者发布 message 到 channel,会返回订阅者个数 | |
subscribe [channel…] | 订阅者订阅一个或多个频道 | |
unsubscribe | 订阅者取消订阅一个或多个频道 | |
pubsub channels | 列出至少有一个订阅者的频道 | |
pubsub numsub [channel…] | 列出给定频道的订阅者数量 |
4、发布订阅与消息队列对比
消息队列通信模型
对比发布订阅,消息队列是一个抢的功能,只有一个消息订阅者可以收到消息。需要知道的是,Redis 并没有原生提供消息队列的功能,需要自己使用 list 类型的 lpush + rpop 命令实现。
pub/sub的缺点: 消息的发布是无状态的,无法保证可达。
4.Bitmap(位图)
提供了一种节省内存的方案。
1、API
API | 说明 | 时间复杂度 |
---|---|---|
setbit key offset value | 给位图指定索引设置值 | |
getbit key offset | 获取位图指定索引的值 | |
bitcount key [start end] | 获取位图指定范围(start 到 end,单位为字节,如果不指定就是获取全部)位值为 1 的个数 | |
bitop op destkey key [key…] | 做多个 Bitmap 的 and(交集)、or(并集)、not(非)、xor(异或)操作并将结果保存在 destkey 中 | |
bitpos key targetBit [start] [end] | 计算位图指定范围(start 到 end,单位为字节,如果不指定就是获取全部)第一个偏移量对应的值等于 targetBit 的位置 |
2、独立用户统计
以独立用户统计这个功能为例,使用 set 和 Bitmap,1 亿用户,5 千万独立用户,进行对比:
数据类型 | 每个userid占用空间 | 需要存储的用户量 | 全部内存量 |
---|---|---|---|
set | 32位(假设userid用的是整型,实际很多网站用的是长整型) | 50,000,000 | 32位 * 50,000,000 = 200MB |
Bitmap | 1位 | 100,000,000 | 1位 * 100,000,000 = 12.5MB |
如果只有 10 万独立用户呢?
数据类型 | 每个userid占用空间 | 需要存储的用户量 | 全部内存量 |
---|---|---|---|
set | 32位(假设userid用的是整型,实际很多网站用的是长整型) | 1,000,000 | 32位 * 1,000,000 = 4MB |
Bitmap | 1位 | 100,000,000 | 1位 * 100,000,000 = 12.5MB |
3、使用建议
type = string,最大 512 MB;
注意 setbit 时的偏移量,可能有较大耗时;
位图不是绝对好。
3.常见问题
1、Redis 为什么速度能这么快?
- 完全基于内存,执行效率高;
- 采用了单线程架构,单线程避免了多线程环境下的锁开销;
- 使用了多路 IO 复用模型,非阻塞 IO。
Redis 多路 IO 复用模型:
- Redis 采用的多路 IO 复用函数:epoll/ kqueue/ evport/ select
- 优先选择时间复杂度为 O(1) 的多路 IO 复用函数作为底层实现,以时间复杂度为 O(n) 的 select 作为保底;
- 基于 react 设计模式监听 I/O 事件 。
2、大量的 key 同时过期,清除大量的 key 很耗时,会出现短暂的卡顿现象,怎么解决?
在设置 key 的过期时间的时候,给每个 key 加上随机数。
3、如何从海量数据里查询某一固定前缀的 key?
4、如何使用 Redis 做异步队列?
使用 List 作为队列,rpush 生成消息,lpop 消费消息。缺点是 lpop 不会去等待队列里有值才去消费的,此时可以通过在应用层引入 sleep 机制去调用 lpop 重试。
如果不通过 sleep 机制怎么等待?
还可以通过 blpop key [key …] timeout 命令阻塞直到队列有消息或超时。blpop 的缺点就是只提供一个消费者消费。
如果是一对多,可以看上面说的 发布订阅。
5、Redis 如何做持久化?
Redis 做持久化一般有三种方式:
- RDB 持久化,即快照方式,保存某个时间点的全量数据快照,默认开启;
- AOF 持久化,保存写状态,即记录下除了查询以外的所有变更数据库状态的指令,以 append 的形式追加保存到 AOF 文件中,默认关闭;
- 混合模式,RDB 做全量备份,AOF 做增量备份。
手动触发 RDB 持久化:
API | 说明 | 时间复杂度 |
---|---|---|
save | 阻塞 Redis 的服务器进程,知道 RDB 文件被创建完毕,不推荐使用。 | |
bgsave | fork 出一个子进程来创建 RDB 文件,不阻塞服务器进程。 |
自动化触发 RDB 持久化:
- 根据 redis.conf 配置里的 save m n 定时触发(用的是 bgsave);
- 主从复制时,主节点自动触发;
- 执行 debug reload 的时候触发;
- 执行 shutdown 且没有开启 AOF 持久化的时候触发。
RDB 持久化的缺点:
- 内存数据的全量同步,数据量大会由于 I/O 而严重影响性能;
- 可能会因为 Redis 挂掉而丢失从当前至最近一次快照期间的数据。
AOF 持久化:
随着写操作的不断增加,AOF 文件越来越大,日志重写解决了 AOF 文件大小不断增大的问题,原理:
- 调用系统 fork,创建一个子进程;
- 子进程把新的 AOF 写到一个临时文件里,不依赖原来的 AOF 文件;
- 主进程持续将新的变动同时写到内存和原来的 AOF 里;
- 主进程获取子进程重写 AOF 的完成信号,往新 AOF 同步增量变动;
- 使用新的 AOF 文件替换掉旧的 AOF 文件。
RDB 和 AOF 的优缺点:
RDB 优点:全量数据快照,文件小,恢复快;
RDB 缺点:无法保存最近一次快照之后的数据;
AOF 优点:可读性高,适合保存增量数据,数据不易丢失;
AOF 缺点:文件体积大,恢复时间长;
RDB-AOF 混合持久化方式,默认的持久化方式。
- bgsave 做镜像全量持久化,AOF 做增量持久化。