-
概述
-
Redis是由C编写的key-val型内存数据库, 可以被当做数据结构服务器
-
value提供了如下数据结构: string(字符串), list(链表), set(集合), zset(有序集合), hash(字典)
-
与memcache比较
-
都是key-value型数据库, 都存储在内存中
-
-
-
memcache的value仅支持string
-
redis支持数据持久化, 重启时可重新加载, memcache不支持持久化
-
-
redis速度远快于memcache
-
memcache是多线程, redis是单线程, IO复用
-
-
redis支持事务
-
使用redis的string做的事, 都可以换为memcache, 以此换取更好性能
-
-
具体结构
-
基础结构
-
string (字符串)
-
常用方法: set, get, incr, decr
-
应用: 计数, 分布式锁
-
底层结构
-
动态字符串, 字符串小于1M时, 每次扩容空间翻倍. 超过1M时, 每次扩容都增加1M, 最大为512M
-
struct {
int capacity; 容量
int len; 实际长度
byte[] content; 内容
}
-
-
-
list (列表)
-
常用方法: lpush, lpop, rpush, rpop
-
应用: 异步消息队列
-
双向链表, 插入/删除时效为O(1), 查找时效为O(n)
-
底层结构
-
当元素较少, 即元素个数少于512时, 使用压缩列表. 当元素较多时, 使用快速链表.
-
压缩列表 (ziplist)
-
压缩列表占用一块连续的内存, 构成没有前后指针的普通数组. 增加元素时, 可能在原地址上向外扩展, 也可能需要分配新的内存空间, 将内容从原地址拷贝至新地址. 故压缩列表不能存储过多元素
-
struct entry {
int prelen; 前一个元素的字节长度
int encoding;
byte[] content; 该元素内容
}
struct ziplist {
int bytes; 整个压缩列表占用的字节数
int tail_offset; 最后一个元素距离压缩列表起始位置的字节数, 便于反向遍历
Int length; 元素总数
T[] entires; 元素列表
int end; 压缩列表结束标志
}
-
-
快速链表 (quicklist)
-
将多个"压缩列表"通过双向指针相连, 称为"快速链表". 其避免内存碎片过多, 又兼顾效率
-
struct quickListNode {
quickListNode* prev; 指向前一个压缩列表
quickListNode* next; 指向后一个压缩列表
ziplist* p; 指向当前压缩列表头
int count; 当前压缩列表中元素总数
int size; 当前压缩列表占用的字节总数
}
struct quickList {
quickListNode* head; 快速链表头
quickListNode* tail; 快速链表尾
int count; 元素总数
int nodes; 压缩列表个数
}
-
-
-
-
hash (字典)
-
常用方法: hget, hset
-
应用: 适合存储对象, 避免全部序列化, 可以仅部分获取
-
底层结构
-
当元素较少, 即键值对数量少于512时, 使用压缩列表. 元素较多时, 使用字典
-
压缩列表 (ziplist)
-
key先作为一个元素存入压缩列表末尾, val作为另一个元素存入压缩列表末尾, 同个键值对总是紧挨着
-
-
字典 (dict)
-
字典内部包含两个哈希表, 一般只有一个有值, 当字典需要扩容/缩容时, 就需要用到另一个哈希表, 并进行渐进式搬迁, 搬迁完后删除旧的
-
哈希表由第一维数组+第二维链表组成, 第一维数组存储第二维链表的首元素地址
-
渐进式rehash
-
因Redis是单线程的, 扩容/缩容时不会一次性rehash, 而是使用渐进式rehash
-
开始rehash时, rehashindex置为0, 在任何对字典的操作后, 将把旧哈希表中rehashindex对应的key=>val搬迁到新哈希表中, 并且rehashindex+1
-
-
struct dict {
hash hash[2]; 两个哈希表
int rehashindex = -1
}
-
字典在Redis里用的很多, Redis本身也是一个字典结构
struct Redis {
dict dict; Redis中所有的"key=>val"对
dict expires; 设置过过期时间的"key=>过期时间"对
}
-
-
-
-
set (集合)
-
常用方法: sadd, spop, smembers, sismember, scard
-
应用: 异步消息队列, 中奖名单
-
底层结构
-
当元素较少时, 使用intset. 元素较多时, 使用字典 (val为None)
-
-
-
zset (有序集合)
-
是一个set, 可以给每个value赋予一个score, 代表该value的排序权重
-
常用方法: zadd,zrem, zcard, zremrangebyscore
-
应用: 限流
-
底层结构
-
当元素较少时, 使用压缩列表. 当元素较多时, 使用字典+跳跃列表
-
字典+跳跃列表
-
字典存储value和score, 跳跃列表会按score排序. 两结构结合, 使得查找时效为O(1), 排序/范围获取时效为O(logN)
-
struct zset {
dict A; 所有val=>score对
skiplist A; 跳跃列表
}
-
-
跳跃列表 (skiplist)
-
struct skiplistnode {
string value;
double score;
skiplistnode*[] forwards; 多层指针
}
struct skiplist {
skiplistnode* header; 首元素指针
int maxLevel; 当前最高层数
}
-
跳跃列表是在有序链表上附加指针数组组成的. 因查找/增加/删除元素时可快速跳过部分元素而得名.
-
有序链表有头结点, 头结点val为None, score为最小值
-
每个更高层都充当下层的快速通道, 层i中的元素按概率出现在层i+1中
-
查找/增加/删除元素的时间复杂度都是O(logN)
-
跳表使用概率性均衡, 而不是平衡二叉树的强制性均衡, 在插入/删除元素时更简洁高效. 跳表效率和平衡二叉树差不多, 但在并行计算下, 更新数据时, 需要锁的东西比较少, 相比平衡树不需要全局的重新平衡
-
查找X
-
一共有64层, 一般通过maxLevel确定目前顶层的位置, 节省从最顶层开始查找的时间
-
先通过每个节点最上层的指针查找, 当找到比X大的节点时, 表明X可能再前一个节点至该节点间, 进入下一层. 如此反复, 找到对应节点
-
-
插入X
-
查找X的位置
-
申请新节点
-
调整底层指针, 按概率调整上层指针. 拿一个数组保存每一层的前继指针
-
-
删除X
-
查找X的位置
-
调整指针
-
删除节点
-
-
更新X为Y
-
删除X
-
增加Y
-
-
-
-
-
-
高级结构
-
HyperLogLog
-
类似于set, 用于不精确统计
-
常用方法: pfadd, pfcount, pfmerge(合并多个页面的uv)
-
应用: 统计页面uv
-
-
Bloomfilter (布隆过滤器)
-
类似于set, 用于不精确判断. 已存入的一定能判断为"已存入", 未存入的也有一定几率能判断为"已存入"
-
应用: 用户推荐时去除已浏览, 爬虫去重, 垃圾邮件过滤, 预先排除查询HBase时不存在的row
-
原理:
-
初始化一个数组. 当一个val存入时, 会被多个无偏hash函数分别hash算得一个整数索引值并求余数组的长度从而得到一个位置, 再将初始数组中对应的位置都标记为1. 判断val是否存在时, 逻辑一致. 可见初始数组越大, 精度越高.
-
自行实现
-
hash算法使用MurmurHash3, 非加密哈希算法, 计算速度快, 计算均匀
-
上述hash算法, 可使用3个随机种子, 变成3个hash函数
-
使用管道, 合并多个val的存入和读取, 减少连接耗时
-
-
-
-
redis-cell (分布式令牌桶限流)
-
GeoHash (地理位置, 附近的人)
-
-
-
应用
-
分布式锁 (string)
-
set 键 值 ex 5 nx
-
该操作是原子性的, 不会被进程调度算法中断
-
必须有nx, 即不存在再set
-
必须有超时时间, 超时时间需大于加锁与释放锁间的逻辑执行时间
-
缺点是当对主节点加锁后, 主节点挂了, 但从节点还没同步就变为主节点, 使主节点可再次落锁. 解决是使用RedLock算法, 多个主节点组成集群, 对过半节点发送setnx命令, 收到过半节点响应就认为加锁成功, 释放锁时, 向所有节点发送del命令, 缺点是性能低
-
-
位图 (string)
-
应用: 用户签到 (1个用户1年1个对应的string, 365位, 即46字节), 布隆过滤器
-
位图不是redis专门的数据结构, 而是针对string结构, 提供了位操作setbit/getbit而已
-
-
异步消息队列 (list)
-
相较于消息队列, 优点是更简单, 缺点是没有ACK
-
消费者用lpop, rpop不停读取, 可用sleep降低qps, 但会有延时. 可用blpop/brpop阻塞读取解决延时, 即当队列为空时会自动休眠, 有数据到来, 会立即醒来. 休眠过久会超时, 服务器会断开连接, 注意捕获异常并不断尝试
-
-
限流
-
普通限流: 限定某一行为在某一段时间内的次数. 用zset制作滑动窗口, value和score都是timestamp
-
漏斗限流: 无法应对短时间的突发流量
-
令牌桶限流: 允许短时间的突发流量
-
-
-
Redis原理
-
redis是个单线程服务器, 其是高性能服务器的典范. 读10万/秒, 写8万/秒. 小心O(n)时效的命令, 会造成卡顿
-
高性能原因
-
数据在内存
-
IO多路复用
-
单线程, 避免了上下文切换, 不存在加锁/释放锁
-
-
每个进程开启一个socket连接, 都会占用一个文件描述符 redis会为每个客户端套接字关联一个指令队列, 客户端指令先放到队列中, redis服务器按顺序先到先服务 redis会为每个客户端套接字关联一个响应队列, redis服务器通过响应队列将指令处理结果返回给客户端
-
-
Redis持久化
-
3种方式
-
RDB (快照)
-
全量数据的二进制写入磁盘
-
原理: 父进程fork子进程, 共享内存. 子进程不断读取内存进行存盘, 此时父进程继续处理客户端请求, 当需要修改数据时, 会将数据复制到新的内存进行写入, 保证原有内存上数据不变
-
缺点: 重启时会丢失数据
-
优点: 重启时加载数据速度快
-
通过bgsave命令可触发全量备份. 另外还有save命令, 该命令不fork子进程, 而是父进程来进行全量备份, 会暂停服务
-
-
AOF日志
-
对数据修改命令进行增量备份
-
原理
-
先在内存中执行命令, 再存盘日志, 不同于MySQL的WAL技术
-
AOF日志在长期运行中会越来越大, 需要定期重写AOF, 缩减大小. 重写时, 父进程fork子进程遍历内存数据, 将数据转换成一系列Redis命令, 并顺序写入新AOF日志, 完毕后再将操作期间的增量AOF日志追加到新AOF日志中, 追加完毕后用新AOF替代旧AOF.
-
IO同步函数不用write, 而用fsync. 相较于write先写入缓冲再随机写入磁盘, fsync会将内容强制写入磁盘. Redis一般1秒调用1次fsync
-
-
缺点: 重启时需要加载AOF日志进行重放, 很耗时
-
优点: 重启时基本不会丢失数据
-
-
混合方式
-
AOF不再存储全量日志, 而是上一次持久化开始后的增量日志
-
重启时, 先通过快照加载数据, 然后重放增量AOF日志
-
-
-
持久化一般在从节点上进行. 若数据很重要又必须放在Redis中, 建议从节点开启AOF持久化做备份, 策略调整为一次命令调用一次fsync
-
-
Redis过期策略
-
惰性过期
-
客户端访问key时, 如果该key已过期, 就立即删除
-
优点: 对CPU友好
-
缺点: 对内存不友好
-
-
定时扫描
-
redis会将设置过期时间的key放入独立字典中, 每秒进行十次扫描,每次扫描随机从独立字典中取20个key, 删除这些key中过期的, 每次定时扫描处理不超过25ms, 以避免读写请求卡顿
-
优点: 对内存友好
-
缺点: 要避免同一时间大量key过期
-
-
主库同时用以上2种策略, 主库删除后通过在AOF中追加del命令, 从库通过同步与主库保持数据一致
-
-
Redis淘汰策略
-
当Redis内存超出物理内存限制时, 内存中的数据会频繁和磁盘产生交换. 生成环境要杜绝交换, 当Redis使用内存超出限制时, 选用策略淘汰数据
-
具体策略
-
noeviction: 不继续服务写请求, 只继续服务读请求, 默认策略
-
volatile-xxx: 对带过期时间的key淘汰. xxx为ttl, 剩余寿命越小先淘汰; xxx为random, 随机淘汰; xxx为lru, 最少使用先淘汰
-
allkeys-xxx: 对所有key淘汰. xxx可为lru, random
-
-
redis用于缓存时, 用allkey-xxx系列策略. redis用于持久化存储时, 用volatile-xxx策略
-
-
管道 (pipeline)
Redis底层是TCP连接. 管道并不是服务器特性, 而是客户端通过改变命令顺序, 将几次命令一并向服务器发送, 节省原本需要单独请求的时间, 从而大幅提升性能.
-
事务
-
Redis事务保证事务中的多个命令能被连续执行, 中间不会插入其他命令
-
事务的四大特性, Redis事务仅支持隔离性. 因无法回滚而没有原子性. 当持久化为AOF模式, 且一个命令一个fsync时, Redis事务也具有持久性
-
multi: 开启事务
exec: 提交并执行命令
discard: 回滚, 该回滚只是丢弃事务队列中的命令. 比如事务中对字符串自增, 虽然该操作失败, 但后续操作仍可生效
watch: 用来监控某个key, 写在multi开启事务前. exec时会检查key从watch后是否发生变化, 有变化则报错, 否则执行成功. watch是乐观锁, redis分布式锁是悲观锁
-
没exec前, 命令被按序存到服务器的事务队列中, exec后开始执行整个队列, 执行完后一次性返回结果
-
-
Redis主从
-
CAP原理
-
C: 一致性, 所有节点都有相同的数据
-
A: 可用性, 服务能对外正常提供服务, 不保证数据是最新的
-
P: 分区容忍性
-
三项中只能同时满足两项. 如坚持分区容忍性, 当网络分区时, 即节点间无法同步数据, 一致性和可用性两难全. 要么关闭服务牺牲可用性, 要么放弃节点间数据同步牺牲一致性.
-
-
主从复制
-
Redis同步支持主从同步, 从从同步(避免主压力太大), 过程是异步的, 不能保持一致性, 仅能保持最终一致性
-
当从节点首次连接主节点, 其会给主节点发送PSYNC命令, 会先做快照同步, 接着再做增量同步
-
同步方式
-
增量同步: 主节点会将修改数据的命令存储在内存的循环队列上, 并异步将命令同步到从节点上
-
快照同步: 从节点长时间无法同步后, 主节点会产生全量数据快照, 从节点根据快照重放, 重放后进行增量同步
-
-
主从架构下当主节点宕机后, 不要立即拉起, 而要先确认主节点是否有持久化, 如果没有则需要从节点的快照文件, 以避免主节点重启后, 将空数据同步给从节点
-
-
Sentinel集群 (哨兵集群)
-
类似于ZooKeeper集群, 由3至5节点构成, 是整个Redis集群的心脏
-
哨兵集群的出现解决了故障转移, 但本质上还是一台机器完成写操作
-
客户端连接集群时, 先连接哨兵集群, 通过哨兵集群获取Redis主节点地址, 再连接主节点. 当主节点异常时, 哨兵集群会进行主从切换, 客户端会通过哨兵集群获取新的主节点地址. 同时哨兵集群将监控异常节点
-
客户端通过哨兵集群获取新的主节点地址的情况: 1) 主节点不可用, 连接异常 2) 主节点可用, 但被哨兵集群降为从节点, 修改命令会报ReadOnlyError
-
-
客户端分片 (Redis Sharding)
-
客户端使用普通哈希算法(murmur hash)或一致性哈希算法将key进行散列到对应的Redis节点. 是一种客户端数据分区技术
-
优点是配置简单, 服务器各Redis节点间无关联. 缺点是客户端无法动态增删Redis节点, 可维护性差.
-
一致性hash
-
多个服务器节点用主机名或IP做hash, 求余后放置在圆环上
-
当要确定某个key需要存取到哪个节点时, 对key做hash并求余, 确定环上的位置. 按顺时针方向在环上行走, 遇到的第一个节点就是服务器节点
-
当部分服务器崩溃时, 少量key会漂移到下一个节点, 会对下一个节点造成压力, 故引入虚拟节点. 对服务器节点用主机名或IP增加数字后缀后hash, 求余后放置在圆环上, 即使一个服务器崩溃, 也会映射到不同的位置.
-
-
-
代理服务器分片
-
客户端将请求发送到代理服务器, 代理服务器将请求转发到某Redis节点, 并将结果返回给客户端
-
优点是便于增删节点, 缺点是耗时
-
业界方案: Codis, Twemproxy
-
-
查询路由 (Redis Cluster)
-
官方分区方案. Cluster是去中心化的, 客户端可以通过一个节点来发现所有节点地址, 用key请求任一节点, 该节点会告知目标节点地址
-
将数据划分为16384个槽, 每个节点负责一部分槽, 需要对key做crc16后对16384取模, 得到对应的槽位. 槽位信息存储在各节点中, 当客户端连接时, 它也会得到一份槽位信息.
-
客户端之后查找key时, 可以直接向槽位对应的目标节点请求. 当客户端请求了错误的节点后, 节点会返回moved指令, 并将key所对应槽位的服务器地址返回给客户端. 客户端纠正本地槽位信息, 并重发请求
-
迁移
-
Redis迁移的单位是槽, 当一个槽正在迁移时, 该槽在原节点与目标节点都会被设为迁移状态. Redis会获取该槽下的所有key, 再对key进行迁移, 迁移key时原节点是阻塞的, 所有对原节点的访问都会被阻塞, 故要尽量避免大key产生.
-
客户端访问在迁移中的槽, 会首先访问原节点. 如果key还在原节点中, 正常处理. 否则原节点向客户端返回"ask 目标节点"指令. 客户端先向目标节点发出"asking"指令, 告知目标下个指令不要向客户端返回moved指令, 以避免重定向循环. 然后客户端再向目标节点发送原先命令
-
-
一个主节点可有多个从节点, 数据并不保持一致性, 详见主从复制. 自动管理主从切换. 节点间通过gossip协议进行通信
-
-
-
缓存异常
-
缓存雪崩
-
定义: 缓存在同一时间大面积过期. 请求将直接落在数据库上, 造成数据库崩溃
-
解决: 过期时间随机设置
-
-
缓存穿透
-
定义: 缓存和数据库中都没有的数据. 请求直接落在数据库上, 造成数据库崩溃
-
解决
-
对数据库也不存在的数据, 设为key-null, 过期时间短点
-
入口检验
-
-
-
缓存击穿
-
定义: 热点数据过期, 对这个热点数据的请求将落在数据库上, 造成数据库崩溃
-
解决
-
热点数据永不过期
-
使用互斥锁 (写锁)
当缓存中没有数据需要访问数据库时, 先对该数据加分布式互斥锁, 加锁成功后再去从数据库查询数据到缓存中. 加锁不成功, 则重新读缓存. 这样保证对数据库中的同个key同时只有一个进程在访问.
-
-
-
缓存预热
-
定义: 服务即将上线前, 将数据先加载到缓存中, 避免请求落在数据库上
-
-
缓存降级
-
系统异常时, 缓存返回默认值
-
-
-
扩展
-
Redis默认有16个数据库, 编号从db0至db15, 默认为db0库, 可通过select命令切换, 库之间相互隔离, 不建议修改库.
-
数据库缓存双写时, 如何保证数据一致性
先写数据库, 写完后删除缓存. 下次读取时, 缓存中不存在数据, 则读取数据库, 之后更新到缓存
-
找出特定前缀的key
-
keys 简单正则
找到所有与正则符合的键值对, 时效为O(n), 不能用于生产环境, 会造成卡顿
-
scan 游标值 正则模式 正则式 count 数量限制
-
redis中存储结构是一个一维数组和二维链表. scan返回的游标是一维数组的位置索引
-
scan可通过类型和大小定位大key
-
-
-
考虑Redis扩容时, 应使用的哈希策略
-
Redis用于缓存: 一致性哈希
-
Redis用于数据存储, 使用固定的key2node映射
-
-
「Redis」概述
最新推荐文章于 2022-11-02 17:59:45 发布