基本使用
数据类型
类型 |
名称 |
应用 |
string |
字符串 |
分布式锁,设置 key-value-ttl,考虑超时和可重入性 |
list |
列表 |
异步消息队;利用 lrange 命令,做基于 Redis 的分页功能列 |
set |
集合 |
利用 Set 的交集、并集、差集等操作 |
zset |
有序集合 |
延迟队列,score 为延迟时间;限流器,score 为加入时间,移除加入时间超过与之的成员,zset 大小超过限流时拒绝请求 |
hash |
字典 |
|
setbit |
位图 |
位图不是特殊的数据类型,而是将字符串看出 byte 数组后按位操作。用于记录 1/0 数据节约使用空间 |
hyperloglog |
|
不精确的去重计数方案,标准误差 0.81% |
bloom filter |
布隆过滤器 |
检查一个值是否在集合里,有一定误差。当判断一个值存在时,可能不存在,但判断一个值不存在,就一定不存在 |
redis cell |
限流器 |
使用漏斗算法 |
geo hash |
地理模块 |
原理:将二维地理映射到一维 zset 中进行排序 |
使用 MULTI
EXEC
DISCARD
WATCH
Stream
Redis 5.0 后加入的支持多播的可持久化消息队列。它的实现参考了 Kafka 的设计,有消费组(customer group)和消费者(customer)概念,消费组通过维护游标指向已消费到的地方,消费组内的消费者是竞争关系,只会有一个消费者接到消息,消费组间相互独立。消费组会维护pending_ids
记录没有被消费(没有收到 ack)的消息,保证至少一次的消费。stream 没有分区的概念,可以由客户端手动创建多个 stream 并设置路由规则实现分区。
xadd # 追加消息
xdel # 删除消息
xrange # 获取stream中的消息列表
xlen # 消息长度
del # 删除stream
xread # 独立消费,可以忽略消费者组概念,将stream当成普通list
xgroup create # 创建消费者组
xreadgroup # 消费组内消费
安全保护
- 危险指令用
rename-command
设置别名,只有通过这个别名才能请求
- 增加密码限制
- lua 脚本安全,禁止客户输入生产 lua 脚本
- SSL 代理,Redis 本身不支持 SSL 安全链接,需要通过代理使数据加密。官方推荐 spiped 工具
原理
I/O 模型
单线程,多路复用
Redis 为什么这么快
- C 语言效率更高
- 内存数据库,避免 I/O
- 单线程避免了上下文切换资源竞争
- I/O 多路复用技术处理网络 socket 连接
- 对象压缩,如果对象小,一维结构内存小于二维结构。ziplist 紧凑型字节数组,inset 紧凑型整数数组
持久化
- AOF 增量日志:只记录对内存进行修改的指令,redis 先进行操作,再记录日志,日志先缓存在内存中,可通过 fsync 强刷
- rdb 快照:fork 一个子进程完成快照写入文件,用 COW(copy on write)机制保障子进程中的数据不受之后修改的影响。通常在从节点生成快照。
通常在生产环境使用混合持久话方式,因为只用 rdb 可能会丢数据,只用 AOF 效率慢。在 Redis 重启时先加载 rdb 的内容,然后再增量重放 AOF 日志。
过期策略
- 主节点:
- 维护一个设置了过期时间的 key 的字典
- 定期删除每秒执行一定次数(10)次的过期扫描。过期扫描并不会扫描所有的 key,会随机取 20key,将过期的删除;若过期 key 超过 1/4 就重复扫描;整个过程设置了时间上限(默认 25ms)。所以当大量
key
同时过期时可能导致节点循环删除过期key
,以至于 25ms 不可使用,这种情况可以使用设置随机的过期时间方式避免
- 惰性检查获取 key 的时候,如果此时 key 已经过期,就删除,不会返回任何东西
- 从节点:从节点并不会执行过期扫描,会同步主节点删除
key
时写入AOF
的del
命令
LRU
Redis 内存超过内存使用限制时,需要执行淘汰策略,提供以下选择:
- noeviction默认策略。不会继续执行写请求,读、删请求可继续执行,这样保证数据不丢失,但可能造成线上业务不能持续进行
- volatile-lru淘汰设置了过期时间的 key 中最少使用的
- volatile-ttl淘汰设置了过期时间的 key 中剩余寿命(ttl)值最小的
- volatile-random随机淘汰设置了过期时间的 key
- allkey-lru淘汰所有 key 中最少使用的
- allkey-random随机淘汰所有 key
集群
主从
Redis 支持一主一从,一主多从的模式,从节点可以从主节点同步,也可以从其他从节点同步。Redis 可以通过wait
指令指定命令的同步由异步变为同步,wait
指令接收两个参数,节点数量N
,等待时间t
,表示指令需要等待N
个节点同步完成,最长等待时间是t
。
主从同步有两种方式:
- 增量同步:主节点将执行的命令存在 buffer 中,buffer 是循环记录的,然后异步的将指令同步到从节点,从节点执行命令并更新自己同步的偏移量
- 快照同步:主节点生成 rdb 快照,从节点同步 rdb 快照,完成后再同步复制 buffer。这个操作也可以不生成 rdb,主节点直接将内存中的数据通过网络传输给从节点
Redis Sentinel
Redis Sentinel 是 Redis 集群管理的工具,类似 zookeeper 集群,客户端通过 Sentinel 链接 Redis 可以实现无感切换主节点,用以保证一定的可用性。Sentinel 切换主节点时,可能产生数据丢失,因为有些主节点执行了的命令没来的及同步到从节点。
Codis
Codis 是 Redis 的集群方案之一。Redis 需要集群的原因:
- 单节点内存过大会导致 rdb 变大,进一步导致主从节点全量同步时间加长,恢复时间变长
- Redis 是单线程工作的,只能利用单 CPU
Codis 原理:
- 无状态的代理层,不改变 Redis 协议,将命令转发给接入的 Redis
- 利用分片原理管理 Redis,使用 key 的 hash 后的值取存储的位置
- 槽位和 Redis 关系通过 zookeeper 同步
- Codis 可以实现动态扩容,扩容时将原来的 key 迁移到新的槽位中,在过程中发现正在迁移的 key 有修改时,会先执行迁移,然后在新的 Redis 节点中执行命令
缺点:
- 增加网络开销
- 增加同步机制 zookeeper 的维护
- 同步带来的性能消耗,以及单个集合类型的数据的大小限制
- 非官方项目新功能同步慢
Redis Cluster
Redis 官方集群支持,去中心化的集群,每个节点管理一部分 key,共同组成一个对等集群。
原理:
- 将所有数据划分为 16384 槽位,每个节点负责一部分槽位
- 客户端也会存一份槽位映射信息,可以计算槽位应在哪个节点,直接调用
- 若客户端槽位信息和真实槽位不一致,Redis 会返回 MOVED 指令和错误信息,客户端需要重试并纠正自己的槽位映射
- 动态扩容将槽位发生变化的 key 迁移至新的槽位;当客户端请求迁移中的 key 时,Redis 会返回 ASKING 指令和错误信息,客户端需要重试
- Cluster 中的节点支持主从,若主节点故障,会将从节点提升为主节点
- 节点下线使用 Gossip 协议,一个节点发现连接不到某个节点,广播这个信息,集群中的大部分节点承认后,再广播下线信息使所有节点承认下线
源码
字符串
Redis 中的字符串使用SDS(Simple Dynamic String)
结构。
struct SDS<T>{
T capacity; // 数组容量
T len; // 当前长度
byte flags;
byte[] content; // 内容
}
这里用 T 的泛型,是因为当字符串较短时可以使用长度更短的byte
或short
减少内存的占用。Redis 规定字符串长度不能超过 512M。创建字符串时len
和capacity
指定值是相同的,因为一般不会append
修改字符串。
append
append
方法会在现有字符串后面追加新的内容,追加时会计算当前容量是否够用,不够用会扩容,并复制当前值到新的对象中。扩容策略在字符串小于 1M 时使用加倍策略,之后只会多追加 1M。
embstr/raw
字符串有两种存储方式embstr
和raw
,当长度超过 44 时,使用raw
。embstr
将SDS
对象头结构和数据存储在一起,因为一次申请最大是 64 字节的空间,其中对象头占用 19 字节,1 字节站位符null
,剩余就是 44 字节。当大于 44 字节采用raw
结构,对象头和数据会分开存储。
字典
字典在 Redis 应用很广泛,不仅是hash
结构的数据会用到,所有的 key/value 组成全局字典,带有过期时间的 key/ttl 组成过期时间字典,zset
集合中的 value/score 也是字典。字典的数据结构类似hashmap
。
refresh
随着操作的不断执行,哈希表保存的键值对会逐渐地增多或者减少,为了让哈希表的负载因子(load factor)维持在一个合理的范围之内,当哈希表保存的键值对数量太多或者太少时,程序需要对哈希表的大小进行相应的扩展或者收缩。
字典在refresh
时需要至少O(n)
的时间复杂度,Redis 采用渐进式迁移方法避免耗时阻塞程序。每次操作(增删查改)时首先会判断当前字典是否有正在执行的渐进式refresh
,如果有就帮助执行一次。Redis 也会设置定时辅助进行渐进式refresh
。
列表
压缩列表
快速列表
跳跃列表
紧凑列表
缓存
缓存读写策略
- Cache Aside(旁路缓存)策略:在更新数据时不更新缓存,而是删除缓存中的数据,在读取数据时,发现缓存中没了数据之后,再从数据库中读取数据,更新到缓存中。
- 问题:
- 很小几率的缓存不一致
- 数据库主从同步延迟,造成新写入的数据没有写入从库,查询不到,写不进缓存
- 解决:
- 写数据库同时,加锁写入缓存
- 写数据库同时,写缓存,设置较小的缓存过期时间
- Read/Write Through(读穿 / 写穿)策略:这个策略的核心原则是用户只与缓存打交道,由缓存和数据库通信,写入或者读取数据。
- Write Through 的策略:先查询要写入的数据在缓存中是否已经存在,如果已经存在,则更新缓存中的数据,并且由缓存组件同步更新到数据库中,如果缓存中数据不存在,我们把这种情况叫做“Write Miss(写失效)”。可以选择两种“Write Miss”方式:一个是“Write Allocate(按写分配)”,做法是写入缓存相应位置,再由缓存组件同步更新到数据库中;另一个是“No-write allocate(不按写分配)”,做法是不写入缓存中,而是直接更新到数据库中。
- Read Through 策略:先查询缓存中数据是否存在,如果存在则直接返回,如果不存在,则由缓存组件负责从数据库中同步加载数据。
- 问题:Write Through 策略中写数据库是同步的,这对于性能来说会有比较大的影响,因为相比于写缓存,同步写数据库的延迟就要高很多了。
- Write Back(写回)策略:这个策略的核心思想是在写入数据时只写入缓存,并且把缓存块儿标记为“脏”的。而脏块儿只有被再次使用时才会将其中的数据写入到后端存储中。
缓存穿透
请求穿透缓存,直接访问数据库。两种解决方案:
- 回种空值:当我们从数据库中查询到空值或者发生异常时,我们可以向缓存中回种一个空值。但是因为空值并不是准确的业务数据,并且会占用缓存的空间,所以我们会给这个空值加一个比较短的过期时间,让空值在短时间之内能够快速过期淘汰。建议在使用的时候应该评估一下缓存容量是否能够支撑。如果需要大量的缓存节点来支持,那么就无法通过通过回种空值的方式来解决,这时可以考虑使用布隆过滤器。
- 布隆过滤器:把集合中的每一个值按照提供的 Hash 算法算出对应的 Hash 值,然后将 Hash 值对数组长度取模后得到需要计入数组的索引值,并且将数组这个位置的值从 0 改成 1。在判断一个元素是否存在于这个集合中时,你只需要将这个元素按照相同的算法计算出索引值,如果这个位置的值为 1 就认为这个元素在集合中,否则则认为不在集合中。两个由于 hash 碰撞有关的缺陷:
- 它在判断元素是否在集合中时是有一定错误几率的,比如它会把不是集合中的元素判断为处在集合中
- 不支持删除元素
- 极热点缓存穿透可以通过后台加载、设置分布式锁控制穿透的数量