概述
redis是基于内存的数据库,对数据的读写操作都是在内存中完成。因此读写速度非常快。常用于缓存、消息队列、分布式锁等场景。他提供多种数据类型支持不同场景。如字符串、散列、列表、集合、有序集合(Zset)、基数统计、地理信息、流、位图
除此之外redis还支持事务、持久化、lua脚本,多种集群方案(主从复制、哨兵、cluster)提供高可用性、发布/订阅模式,内存淘汰机制、过期删除机制。
redis和memcached区别
- redis支持更丰富的数据类型。memcache只支持最简单的key-value数据类型
- redis支持数据持久化
- redis支持集群
- redis支持发布、订阅模式、lua脚本、事务
为什么选择redis作为mysql的缓存
- redis具备【高性能】、【高可用】的特性
- 单台redis的QPS(每秒处理完请求的次数)是MySQL的10倍。redis单机qps能破10W。
- 数据结构
- string
- 缓存对象
- 限流
- 技术
- 分布式锁
- 共享session
- List
- 简单的消息队列
- 生产者需要自行实现全局唯一id
- 不能以消费组形式进行消费
- 消息会丢失
- hash
- set
- Zset
- BitMap
- hyperLogLog
数据结构内部实现
- String
- SDS,简单动态字符串
- 不仅保存文本数据,还保存二进制数据
- 可以获取字符串长度,sds记录了字符串长度
- api安全,不会造成缓冲区溢出
- List
- 双向链表和压缩列表
列表元素个数小于512且每个元素的值小于64字节,redis使用压缩列表
否则使用双向链表
- 但是redis3.2版本后,list数据类型底层数据结构只由quickList(快速列表)实现
- hash类型内部实现
- 哈希类型元素 个数小于512且每个元素小于64字节,redis会使用压缩列表
- 否则使用哈希表
- 7.0版本 使用listpack (紧凑列表)
- set类型
- 哈希表和整数集合实现
- 集合中的元素都是整数且元素个数小于512,使用整数集合
- 否则使用hash
- zset
- 有序集合元素个数小于128个,且每个元素的值小于64字节,使用压缩列表
- 否则使用跳表
- 7.0版本使用listpack数据结构
> 原理
>- 内部使用HashMap和跳跃表来保证数据的存储和有序。
>- hashMap里放的是成员到score的映射
- 跳跃表存的则是所有成员
- 排序依据是hashMap里存的score
- 使用跳跃表可以获得比较高的查找效率,且实现比较简单
redis单线程模型
指的是[接收客户端请求->解析请求->进行数据读写等操作->发送数据给客户端] 这个过程是由一个线程完成的。redis6.0引入多线程
多线程处理
- 处理关闭文件线程
- AOF刷盘
- 异步释放redis内存,unlink key等删除命令交给后台线程执行。
- 好处:不会导致redis主线程卡顿,使用unlink异步删除大key
关闭文件、AOF刷盘、释放内存这三个任务都有各自的任务队列
文件事件处理器
redis内部是由文件事件处理器实现的
IO对路复用程序监听套接字,放到队列,事件分派器从队列中一个一个取出套接字,交给不同处理器进行处理
为什么单线程那么快
- redis的大部分操作都在内存中完成,并采用高效数据结构。因此redis瓶颈可能是机器的内存或者网络带宽,并非CPU。
- redis采用单线程,避免上下文切换,锁竞争
- redis采用IO多路复用机制处理大量用户端的socket。IO多路复用指的是一个线程处理多个IO流
redis6.0引入多线程处理网络请求,默认情况下IO多线程只针对发送响应数据
持久化
redis共有3种数据持久化
- aof
- redis执行完写操作,把命令追加到aof缓冲区;
- 通过write()系统调用,将缓冲区数据写入到aof文件。这个时候还没写到磁盘,而是拷贝到内核缓冲区
- 回盘策略(内核缓冲区数据什么时候写入磁盘)
- always:每次写操作命令执行就写回硬盘
- every sec:每秒,每次写命令执行完,先将命令写入到AOF文件的内核缓冲区,每隔1秒将缓冲区里的内容写回磁盘
- no:由操作系统决定何时缓冲区内容写回硬盘
- AOF日志过大,触发重写机制
- rdb
- 快照,记录某一个瞬间的内存数据
- redis提供了两个命令生成RDB文件
- 每隔一段时间自动执行一次bgsave :配置文件配置
- 900秒内,对数据库进行至少1次修改
- 300秒内,对数据库进行至少10次修改
- 60秒内,对数据库进行至少1W次修改
- fork子进程生成RDB
- 子进程和父进程共享同一片内存数据
- 因为创建子进程时,会复制父进程的页表,但页表指向的物理内存还是一个
- 如果主线程执行写操作,被修改的数据会复制一份副本,bgsave将该副本数据写入rdb文件
- 混合持久化
aof重写过程
-
主进程创建重写AOF的子进程,此时父子进程共享物理内存,重写子进程只会对这个内存进行只读。
-
重写工作:子进程读取当前数据库的所有键值对,将每个键值对用一条命令记录到『新的aof文件』
-
重写期间,写命令会记录到重写缓冲区、AOF缓存区
-
子进程完成AOF重写工作,向主进程发送一条信号,主进程接收到该信号,调用信号处理函数
- 将AOF重写缓冲区中所有内容追加到新的AOF文件,使得新旧两个AOF所保存的数据库状态一致
- 新AOF文件进行改名,覆盖现有AOF文件
重写过程是由后台子进程bgrewriteaof来完成的
- 好处
- 子进程进行AOF重写期间,主进程可继续处理命令请求,避免阻塞
- 子进程带有主进程数据副本。如果是线程,会共享内存。使用子进程,父子进程共享内存数据,但只能以只读方式,父子进程任一方修改共享内存,会发生写时复制,父子进程就有独立的数据副本,不用加锁保证数据安全
redis集群
想要设计一个高可用的redis服务,需要从redis的多服务节点来考虑。如主从复制、哨兵模式、切片集群
主从复制
是redis高可用服务的最基础保证。主从复制实现读写分离。
主从服务器之间的命令复制是异步的。
哨兵
主从复制宕机后,无法主动转移故障。哨兵模式提供了主从节点故障转移的功能。
多个哨兵实例定时ping主从节点,指定的哨兵实例数认为主节点挂了,就会进行故障转移
- 选举新的主节点
- 其他从节点从新的主节点同步数据
- 通知客户端切换主节点
- 将旧的主节点作为新主节点的子节点
cluster
redis缓存数据量大到一台的服务器无法缓存时,需要使用redis切片集群。将数据分布在不同服务器上。
redis cluster方案采用哈希槽,来处理数据和节点之间的关系。redis cluster中,一个切片集群共有16384个槽。
redis 的key根据计算映射到一个hash槽。哈希槽可平均、手动分配到节点上。
节点通信协议
gossip
脑裂产生的数据丢失
产生原因
由于网络问题,集群节点之间失去联系,主从数据不同步。重新平衡选举,产生两个主服务。网络恢复,旧主节点恢复,降级为新主节点的从节点,旧主节点与新主节点进行同步复制,旧主节点清空自己的数据,就会导致之前客户端写入的数据丢失
解决
主节点发现从节点下线或通信超时的总数量小于阈值,就拒绝客户端的写入。
至少有N个从库,在和主库进行数据复制时ACK延迟不超过T秒。新主库上线,就只有新主库能接收和处理客户端请求。
过期删除策略
- 定期删除:隔一段时间随机从数据库取出一定量的key进行检查,删除其中过期的key
- 惰性删除:访问到的key判断是否过期,过期就删除
redis持久化时,过期键如何处理
RDB
- 文件生成阶段
- 生成rdb的时候,会对key进行过期检查,过期的键不会被保存到新的rdb文件中
- 文件加载阶段
- 主服务器,载入rdb文件时,程序会对文件中保存的键进行检查,过期键不会载入到数据库
- 从服务器,不论是否过期都会被载入到数据库,由于主从服务器进行同步,从服务的数据会被清空,一般来说,过期键载入RDB文件的从服务器也不会造成影响
AOF
- 文件写入阶段
- 持久化时,数据库某个键未过期,AOF保留此过期键
- 当此过期键被删除,redis会向AOF文件追加一条del命令显示删除该键值
- 文件重写阶段
- 执行AOF重写,会对Redis键进行检查,已过期的键不会被保存到重写后的文件中
主从模式,如何处理过期键
从库对过期key处理是被动的,即使从库的key过期。
从库的过期键处理依靠主服务器控制,主库在key到期时,会在AOF文件增加一条del指令,同步到从库,从库通过执行del指令删除过期key。
内存淘汰策略
redis内存满了就会触发内存淘汰机制。
不进行数据淘汰
运行内存超过最大设置内存,不淘汰任何内存,不再提供服务,直接返回错误
进行数据淘汰
- 设置了过期的数据中淘汰
- volatile-random :随机淘汰设置了过期时间的任意键值
- volatile-ttl:优先淘汰更早过期的键值
- volatile-lru:淘汰设置过期时间的键值中,最久未使用的键值
- volatile-lfu:淘汰设置过期时间的键值中,最少使用的键值
- 在所有数据范围内淘汰
- allkeys-random:随机淘汰任意键值;
- allkeys-lru:淘汰整个键值中最久未使用的键值;
- allkeys-lfu:淘汰整个键值中最少使用的键值
如何设计一个缓存策略,可以动态缓存热点数据?
内存有限,只能将其中一部分热点数据缓存起来,所以我们要设计一个热点数据动态缓存的策略
思路
通过数据最新访问时间做排名,并过滤掉不常访问的数据,只留下经常访问的数据。
如电商平台,只缓存用户经常访问的TOP 1000的商品
- 通过缓存系统做一个排序队列,比如存放1000个商品。系统给根据商品访问时间,更新队列信息,越是最近访问的商品排名越靠前
- 同时系统定期过滤队列中排名最后的200个商品,然后再从数据库随机读取出200个商品加入
- 这样每次请求到达时,会先从队列获取商品id,如果命中,根据id再从另一个缓存数据结构中读取实际的商品信息,并返回
- 在redis中可以用zadd方法和zrange方法完成排序队列和获取200个商品的操作
redis缓存策略
- Cache Aside (旁路缓存) — 常用
- Read/Write Through(读穿/写穿)策略
- Write back(写回策略)
Cache Aside
应用程序直接与【数据库、缓存】交互,并负责对缓存的维护
cache Aside适合读多写少的场景,不适合写多场景
如果业务对缓存命中率有严格要求,可以考虑:
1、更新数据时,更新缓存。更新缓存前加分布式锁,这样在同一时间只允许1个线程更新缓存。
2、更新数据时,更新缓存。缓存加较短的过期时间。即使数据不一致,但缓存数据也很快过期
redis实现延迟队列
使用有序集合(ZSet)。ZSet有一个score属性用于存储延迟执行的时间。
使用zadd score1 value1 命令往内存中生产消息,再利用zrangebyscore查询符合条件的所有待处理的任务,然后循环执行队列任务即可。
redis大key
什么是大key
1.String 类型的值大于10KB;
2.集合类型的元素个数超过5000个,或者hash所有元素的总大小大于100M
大key的影响
- 客户端超时阻塞
- 主要是持久化,大key会使得fork子进程造成阻塞
- 网络阻塞
- 阻塞工作线程
- 删除大key会阻塞工作线程。建议使用unlink命令,异步删除
- 内存分布不均
- 集群模式在slot分布均匀情况下,会出现数据和查询倾斜,部分有大key的节点占用内存多,qps会比较大
如何查找大key
1.使用redis-cli --bigkeys查找
- 注意事项
- 最好在从节点执行,因为该命令会阻塞
- 如果没有从节点,选择在业务低峰期执行
- 不足
- 只能返回每种类型中最大的big key。无法得到大小排在前N位的big key
- 对于集合类型,这个方法只统计集合元素个数的多少。而非实际占用内存量。但个数多,不一样占用内存就多
2.使用rdbTools工具查找
解析rdb文件,找到大key
redis实现分布式锁
redis的set命令有个NX参数实现分布式锁。它表示key不存在才插入。
- 如果key不存在,显示插入成功,用于表示加锁成功
- 如果key存在,显示插入失败,用于表示加锁失败
基于redis节点实现分布式锁时,对于加锁操作,需要满足3个条件
1.set命令带上nx选项。加锁包括读取锁变量,检查锁变量值和设置锁变量3个操作,需要以原子操作的方式完成,所以使用set命令带上nx选项实现加锁
2.set命令带上ex选项。锁变量需要设置过期时间,以免客户端拿到锁发生异常,导致锁无法释放。
3.使用set命令设置锁变量值时, 每个客户端设置的值是唯一值。锁变量的值需要能区分来自不同客户端的加锁操作。以免在锁释放时,出现误释放操作。
SET lock_key unique_value NX PX 10000
- lock_key是key键
- unique_value是客户端生成的唯一标识,用于区分不同客户
- NX表示只在lock_key不存在时,才对lock_key进行设置操作
redis分布式锁优缺点
优点
缺点
- 超时时间不好设置
- 如何合理设置超时时间
- 基于续约的方式设置超时时间。先给锁设置一个超时时间,启动守护线程,让守护线程在一段时间后,重新设置这个锁的超时时间
- 实现方式:写一个守护线程,判断锁的情况。锁快失效时,进行续约加锁。主线程执行完成后,销毁续约锁即可(实现复杂)
- reids主从复制模式的数据是异步复制的,导致分布式锁不可靠
- 客户端在redis主节点获取到锁后,在没有同步到其他节点前,宕机了。客户端能够在新主节点获取到锁
redis如何解决集群情况下分布式锁的可靠性
红锁