#Redis
Redis 大纲
使用篇
- Redis 的使用场景
- 缓存
- 计数相关:计数器/排行榜/浏览量/播放量等
- 交并集操作:共同好友,朋友圈点赞
- 简单消息队列:发布订阅
- Session 服务器
- Redis 支持的数据结构
- String
- List
- Set
- Sorted Set
- Hash
- Bitmaps
- Hyperloglogs
- Geospatial
- Redis 实现消息队列
- Rpush 生产消息,lpop 消费消息,没有消息时 sleep 或者使用 blpop
- 消息支持多次消费,使用 pub / sub 模式可以达到 1: N 的消息队列
- Pub / sub 模式的缺点:消费者下线的情况下,生产的消息会丢失
- 延时队列的实现:基于 SortedSet,以时间戳为 score,消息内容作为 key 来生产消息。调用 zrangebyscore 来获取 N 秒前的数据。
- Redis 实现分布式锁
- 使用 set/del 添加和释放锁。可带生效时间设定。
- 删除锁时判断线程 ID 是否是自己,防止误删除。
- 重入性使用 state 值来判断。
- 判断锁释放可以通过轮询或者 Redis 的发布订阅机制实现。
- BigKey 有什么影响
- 网络阻塞,传输耗时长
- 超时阻塞,操作耗时长
- 内存占用高,空间分配不平衡
- 如何查询固定前缀大 key
- Keys 命令。会阻塞线程,查询事件复杂度是 o (n),且查询结果是全量,不支持分页。
- Scan 命令。不会阻塞线程,但有可能查出重复数据。不保证能得到查询期间被修改的元素。
- Redis 阻塞可能原因
- 服务器 CPU 负载过高
- 持久化过程占用资源过多
- API 使用不合理,如大 key 查询或 keys 扫描
- 缓存问题
- 缓存雪崩
- 原因:由于大批量的缓存突然失效导致请求都打到了数据库上
- 措施
- 缓存失效时间分开,设置随机失效时间
- 控制数据库写入操作,只允许一个线程写入
- 缓存穿透
- 原因:查询缓存中没有的数据,请求打到了数据库
- 措施
- 使用布隆过滤器拦截
- 缓存空的数据,并设置过期时间
- 缓存预热
- 原理:自动将热点数据加载到缓存中
- 缓存更新
- Cache-Aside 旁路缓存模式
- 做法:
- 读请求
- 先查询缓存
- 缓存中有直接返回
- 缓存中没有查询数据库更新缓存
- 写请求
- 先更新数据库
- 然后删除缓存
- 读请求
- 选择原因
- 为何不先删除缓存后更新数据库?
- 若先删除缓存,在缓存删除期间产生读请求,可能会将未更新的数据查询到缓存中导致缓存脏数据。
- 若选择先删除缓存后更新数据库,如何解决一致性问题?
- 采用延时双删,更新数据库后延时一段时间再次删除缓存,总共删除两次。这种做法不但需要两次删除而且有延迟,所以不推荐使用。
- 为何是删除缓存而不是更新缓存?
- 若产生两个并发的写请求,因为各种原因导致先来请求缓存更新操作晚于后来的请求,同样会导致缓存脏数据。
- Cache-Aside 存在数据不一致的可能吗?
- 存在,若缓存失效期间同时产生写请求与读请求,且读请求的缓存更新操作晚于写请求的缓存删除操作,这个时候也会出现数据不一致问题。这种情况要求缓存失效且读写同时触发,条件比较复杂。
- 另一种情况是读请求先读到了缓存,写请求更新了数据。这个时候缓存与数据库数据不一致,也是一种一致性问题。若是业务对此要求严格一致,可采取加锁方式解决。
- 为何不先删除缓存后更新数据库?
- Cache-Aside 的异常补偿机制
- 当删除缓存时存在缓存删除失败的问题,需要作出补偿策略。
- 删除重试机制,同步删除重试影响性能,因此可以使用异步重试删除,如使用 MQ。但这样引入了 MQ 中间件,以及删除失败后但逻辑需要业务代码但 trigger 触发。
- 监听 binlog 日志,解析 binlog 日志来处理缓存删除失败的问题。注意这里优先选择监听从数据库的 binlog 日志,防止主节点事务还未完成就过早的删除了缓存。
- 采用云服务商提供的 DTS (数据传输) 服务,DTS 服务适配了常见的数据源与数据操作场景,解决了如 binlog 日志回收,主备切换场景下的高可用问题。
- 适用点
- 缓存数据计算逻辑复杂
- 数据一致性要求高
- 不存在大key或热点数据
- 做法:
- Read-Through 读穿透模式
- 流程和 Cache-Aside 模式相似,不同点在于 Read-Through 多了访问控制层,读请求只和访问控制层交换,缓存能否命中与读请求无关。
- Write-Through 直写模式
- 同样提供了访问控制层来进行更高程度的封装。不同于 Cache-Aside 模式的是 Write-Through 不是删除缓存而是更新缓存。该模式适合写操作多且对一致性要求高对场景。
- Write-Behind 异步回写模式
- 写请求只更新缓存不更新数据库,数据库在合适的时机异步批量更新。
- 这种模式写延迟低,吞吐性好,但一致性弱,需要缓存做好高可用,适合于大量的写请求场景。如秒杀,MQ 的消息存储机制等。
- Write-Around
- 写请求不更新缓存,缓存设定失效时间,由失效时间自动更新。这种模式适用于一致性要求不高的业务场景。
- Cache-Aside 旁路缓存模式
- 缓存降级
- 原因:缓存失效或缓存服务器挂掉的情况下不去访问数据库直接返回内存数据或默认数据。以此减少降级对业务对影响操作。
- 缓存雪崩
原理篇
- Redis 效率高的原因
- C 语言实现,效率高
- 纯内存操作
- 基于非阻塞式的 IO 复用模型
- 单线程避免上下文切换,各种锁操作
- 丰富的数据结构,对数据存储做了优化,如亚索表,跳表。
- 文件事件模型
- 处理器结构
- 多个 Socket
- IO 多路复用程序
- 文件事件分派器
- 事件处理器
- 连接应答处理器
- 命令请求处理器
- 命令回复处理器
- 处理器流程
- 客户端 Socket 与 Redis 的 Server Socket 请求建立连接
- Server Socket 产生 AE_READABLE 事件,事件压入队列中
- 文件事件分派器从队列中获取事件,交给连接处理器
- 连接处理器建立通信,将 AE_READABLE 事件与命令处理器关联
- 客户端发起操作命令,产生 AE_READABLE 事件压入队列。事件分派器将事件交给命令处理器。
- 命令处理器处理事件,然后将 socket 的 AE_WRITABLE 事件与回复处理器关联。
- 客户端准备好接收结果时,产生 AE_WRITABLE 事件压入队列,命令回复器回复结果。
- 最后操作完成,解除命令回复器与 AE_WRITABLE 事件的关联。
- 处理器结构
- Redis 的主从同步
- 第一次同步时主节点使用 bgsave 命令,中途操作暂存到缓冲区。待从节点同步完原有的数据,将同步期间缓冲区产生的操作再次同步。
- Pipline
- 将多次 IO 压缩成一次,但是要求管道中的指令没有因果关系。
- 使用 pipline 实现请求/响应的功能。客户端未读取服务端响应时服务端可处理新的请求。客户端发送多个命令时只需等待服务端最终结果。
- Redis 的持久化方式
- RDB(Redis DataBase Back File)
- 原理:定期生成所有数据的快照,依赖快照恢复。采用写时复制技术实现,不会阻塞 Redis 读写操作。
- 优点
- 只有一个
dump.rdb文件,方便持久化 - 单个文件方便存储
- 性能最大化,fork 子线程备份,主线程不阻塞,IO 最大化。
- 数据集大时,比 AOF 启动效率高
- 只有一个
- 缺点:间隔时间备份数据,容易丢失数据
- AOF (Append-only-file)
- 原理:将所有命令以 Redis 命令请求协议存储,保存为 AOF 文件。命令保存时机有请求即保存、每秒一次、按操作系统机制刷盘三种方式。
- 优点
- 数据安全,一次操作就可以备份一次
- 通过 append 模式,即使中途宕机也可以回复数据
- 有 rewrite 模式,当 aof 文件过大时可以进行命令合并
- 缺点
- AOF 文件偏大,恢复慢
- 数据集大时,比 RDB 启动效率低
- 混合模式
- Redis4.0 引入了混合模式,支持 RDB 与 AOF 同时使用。新的 AOF 文件前半段是 RDB,后半段是增量的 AOF。
- RDB(Redis DataBase Back File)
- Redis 为什么是单线程的
- 原因
- Redis 的瓶颈往往在于内存和网络带宽,不在 CPU
- 单线程避免调度切换开销
- Redis中的多线程
- 在 Redis4.0 中引入了多线程处理异步任务
- 在 Redis6.0 中对网络模型实现了多线程 IO,用于处理网络数据对读写和协议解析。主线程将可读 Socket 分发给 IO 线程组进行并行请求解析,解析完毕的命令执行还是单线程的,执行结果交给 IO 线程组写回 Socket 是并行的。即将与 Socket 相关的读写操作变为并行执行的,以此减轻网络 IO 的负担。但是命令执行还是由主线程执行的,仍是单线程,且是线程安全的。
- 原因
- Redis 集群模式
- Sential
- Cluster
- 所有节点互相连接
- 集群消息通过集群总线通信
- 节点与节点通过二进制协议通信
- 客户端与集群节点正常文本协议通信
- 集群节点不代理查询
- 数据按 Slot 存储在多个 Redis 实例上。Redis 集群内置 16384 个哈希槽,将 key 按 CRC16 算法计算后对 16484 取余。
- 集群节点宕机时自动故障转移
- 可以平滑扩/缩容
- 集群优化策略
- Master 不做持久化工作
- 数据交给 Slave 开启持久化备份
- Master 和 Slave 保持在同一个局域网
- 避免在压力大大主库上增加从库
- 主从复制避免图结构,使用单项链表保持节点切换简单
- Redis 的内存淘汰策略
- Volatile-lru:从已设置过期时间的数据集中挑选最近最少使用的数据
- Volatile-ttl:从已设置过期时间的数据集中挑选即将过期的数据
- Volatile-random:从已设置过期时间的数据集中任意挑选数据
- Allkeys-lru:从所有键中挑选最近最少使用的数据
- Allkeys-random:从所有键中中任意挑选数据
- No-enviction:禁止淘汰数据。新写入操作会报错。
- 基于 volatile 策略时若没有键设置了超时时间,那么表现效果和 allkeys 效果一致。
- Redis 的过期策略
- 定时删除策略:设定定时器,时间到了就删。此种方式资源消耗大。
- 定期删除策略:每隔一定时间扫描删除。不是扫描所有 key,而是随机抽一部分。
- 惰性删除:操作 key 时如果发现过期了就删除。
- Redis 的事务
- Redis事务的特性
- 事务失败时不支持回滚
- 一个事务中出现运行错误,其余的命令会继续执行
- Redis 事务的使用
- MULTI 开启一个事务
- EXEC 执行一个事务
- DISCARD 取消一个事务
- WATCH 提供 Check And Set 的能力,可以监控一个或多个键。一旦某个键被修改/删除,之后的事务就不会执行。
- Redis事务的特性
- Redis 的 hash 冲突与 hash 扩容
- Hash 冲突:使用链表保存冲突的数据
- Hash 扩容:渐进式 hash,从第一个索引开始一点一点的转移数据
- Redis 分区
- 方案
- 代理分区:将请求发送给代理,让代理决定对哪个节点读写。如 Twemporxy
- 客户端分区:由客户端根据一定规则将数据分散到不同节点上
- 查询路由:客户端随机请求一个 Redis 实例,Redis 转发给正确的 Redis 节点。
- 缺点
- 涉及多 key 的操作不能很好的支持,如无法直接对分布到不同节点的 key 做交并集计算
- 同时操作多个 key,无法使用事务
- 分区的粒度是 key,无法使用一个非常长的排序 key 存储一个数据集
- 数据处理会变得复杂,如备份时需要收集多个节点的数据
- 需要处理动态缩扩容的问题
- 方案