- 简介:基于内存的key-value数据库,使用单线程模型处理命令,当命令进入时,不是立即执行,而是进入队列,从而保证同一时间不会有两条命令执行。
限制并发的一种做法就是利用队列特性来保证优先级问题。 - 安装配置:https://github.com/judasn/Linux-Tutorial/blob/master/markdown-file/Redis-Install-And-Settings.md
- 单线程Reactor模型:因此需要额外注意调用命令的操作复杂度,避免阻塞Redis
构成Redis的底层数据结构
-
字符串:类似于Java中的StringBuilder,内部存放着一个char数组,当前使用量,因此插入前会先扩容处理。
int lenint freechar buf[] -
链表:类似于Java中的LinkedList,为双向链表
每一个节点如下listNode *prevlistNode *nextvoid *value -
字典:实际上是hash表的实现,类似于Java中的HashMap,hash冲突也是使用链地址法解决。
dictEntry **tablelong sizelong usedlong sizemask -
跳跃表:redis是单线程处理请求,因此并发性是不需要考虑的,跳跃表作为有序集合的实现是很容易实现范围查找的。
-
整数集合:适用于值都是整数,并且元素数量不多的情况下的集合实现方式
整数集合就是一个数组+长度int8_t contents[]uint32_t lengthuint32_t encoding -
压缩列表:一般作为列表与hash键的实现方式,节省内存使用。
-
对外对象:由基本数据结构构成对外使用的对象,使用redisObject进行包裹。
- 字符串对象(string):使用SDS实现
- 列表对象(list):使用链表或者压缩列表实现
- 哈希对象(hash):使用压缩列表或者字典实现
- 集合对象(set)
- 整数集合:底层是数组
- 普通集合:使用压缩列表或者字典实现
- 有序集合对象(zset):使用压缩列表或者跳跃表实现
- 位图(Bitmaps):本身是字符串,可以实现位操作,一般理解为一个只存储0和1的数组。
- HyperLogLog:底层也是字符串,其存在一定误差率,但是极大的节省了内存,如果只为计算独立总数,不需要获取单条数据则可以使用
- 地理信息定位(GEO):
-
-
Key迁移
- move:针对Redis内部数据迁移,从一个DB转移到另一个DB,不支持多个key
- dump+restore:先序列化成RDB,然后使用Restore进行RDB复原,整个过程分两个步骤,非原子性,不支持多个key
- migrate(推荐使用):类似于dump+restore,不过整个流程是原子性操作,支持多个key
-
Key遍历
- keys:全量遍历,该命令在键值对很多的情况下会造成Redis阻塞。
- scan:渐进式遍历,每次返回新游标方式遍历Redis,不会造成阻塞
- hscan:遍历hash
- sscan:遍历set
- zscan:遍历zset
-
扩容:redis本身是哈希表的实现,因此必然会出现扩容的情况
- rehash:新建一个哈希表h1,把旧的哈希表h0全部迁移到对应的h1当中,在使用h1替换h0
- 渐进式rehash:当key非常多的时候不能一次性迁移完,每次当操作key时会顺便完成迁移任务,解决了集中式rehash带来的庞大计算量。
渐进式情况下会出现两个哈希表,因此查需要查两次,h0中查不到则再去h1中查找。另外新添加的只会添加到h1中。 - 问题
- rehash会增大内存,可能触发满容淘汰机制,大量key被淘汰,然后主从同步,导致redis抖动。
- 美团解决方案:修改源代码,当剩余内存不足时,不触发rehash操作
- 美团解决方案:做好容量规划,提前分配足够内存。
- 集群状态下某一个分片可能因为key多,触发rehash,导致当前分片内存分配不均匀。
- rehash会增大内存,可能触发满容淘汰机制,大量key被淘汰,然后主从同步,导致redis抖动。
-
附加功能
- 订阅/发布
在RedisServer中存放的两个属性pubsub_channels(订阅字典)pubsub_patterns(模式匹配链表)当消息触发时先去 订阅字典中找到对应的订阅链,像Client逐一推送,然后再遍历模式匹配链表,像匹配的Client依次推送。 - 事务与Lua:原生事务不支持回滚操作,一般都使用lua脚本代替,lua脚本是原子性执行。
- eval:eval 脚本内容 key个数 key列表 参数列表
- evalsha:Redis先预加载lua脚本,然后只要使用对应的sha1签名定位即可。
- Pipeline:将命令缓存起来,然后一次性发送给RedisServer执行,减少网络耗时。Pipiline非原子性,需要客户端支持。
- 慢查询:Redis的慢查询记录只是命令的执行时间,不包括排队时间。
- slowlog-log-slower-than:预设阈值,搞OPS场景下建议1毫秒
- slowlog-max-len:记录日志最大长度,线上可设置1000以上
- key过期策略与内存回收
- 惰性删除:当调用读写命令时回去RedisDb实例中的expires中判断是否已过期,过期则清理key
- 定期删除:定时任务会全局的遍历RedisDb中的expires,从中随机选择key,判断是否需要清除。
每次检查会从RedisDb[0] -> RedisDb[16]依次轮询定期删除是使用了ServerCron这一周期性事件循环机制,默认100ms一次。 - 持久化策略的影响
- RDB
生成RDB文件时会检查对应的key,过期的key不会被持久化到RDB文件中。恢复模式下主节点再入RDB文件时也会主动剔除过期的key,从节点会全部载入。 - AOF
写入时清除过期key会向AOF文件中添加一条DEL指令。AOF文件重写时会主动过滤过期的key。 - 主从同步
复制模式下以主节点为主,只有主节点的操作才会触发删除,从节点的读操作不会删除对应的key过期数据。(3.2版本之后会删除该key),
- RDB
- 内存回收:key过期被删除并不代表内存被回收,内存只有在引用计数的值为0时才会被回收。
- volatile-lru:从已设置过期时间的数据集(server.db[i].expires)中挑选最近最少使用的数据淘汰
- volatile-ttl:从已设置过期时间的数据集(server.db[i].expires)中挑选将要过期的数据淘汰
- volatile-random:从已设置过期时间的数据集(server.db[i].expires)中任意选择数据淘汰
- allkeys-lru:从数据集(server.db[i].dict)中挑选最近最少使用的数据淘汰
- allkeys-random:从数据集(server.db[i].dict)中任意选择数据淘汰
- no-enviction(驱逐):禁止驱逐数据
- 订阅/发布
-
持久化
- RDB:RDB是每隔指定时间生成一次内存快照。
- 命令:SAVE(阻塞) BGSAVE(后台fork进程进行),当BGSAVE时会拒绝其他的BGSAVE指令。
- 快照:后台生成rdb之后再原子性的替换当前rdb文件。
- 载入:Redis启动时自动载入,如果服务器开启了AOF则优先载入AOF文件,因为一般情况下AOF比RDB更精确。
载入过程中服务器是一直处于阻塞状态 - 配置:
- save:save 900 1 在900ms中变化一次。
- 优点
- 最大化Redis的性能,备份时再子进程中进行,因此对处理请求的进程没有影响
- 全量备份,适合容灾,不像AOF记录每一条指令,因此恢复速度快
- 缺点
- 因为是按照时间线来保存数据的,因此数据丢失会损失该时间段之内的数据。
- 本身fork()在数据集比较大的情况下可能耗时一般几毫秒,如果CPU吃紧的话,那么可能长达1秒,该操作会使得当前处理请求的线程阻塞。
- AOF:AOF是每次针对写命令记录到缓冲区aof_buf,然后再同步到AOF文件中。
- 命令:
- 载入:Redis启动时读取AOF文件,执行一遍指令即可
- 重写:AOF文件随着日积月累必然很大,因此Redis提供重写功能。
类似RDB,但是把已有的内容以命令形式写入到新的AOF文件中,重写在后台进行,因此Redis还需要提供一个重写时接收的命令缓冲区,当新的AOF文件写入完毕后缓冲区的再次写入,最后用新的AOF文件原子性替换旧的AOF文件所使用的命令为:BGREWEITEAOF - 优点
- 每次记录写指令,在默认情况下最多丢失1秒数据
- 自动重写机制,在AOF过大时,Redis可以自动重写,再原子性的替换
- 缺点
- 体积大,恢复慢
- 性能略弱与RDB
- 配置
- appendfsync
同步操作是由于操作系统写入缓存的存在。- always:将aof_buf中所有的内容写入并同步到AOF文件中。
- everysec(默认):每一秒将aof_buf中所有的内容写入并同步到AOF文件中。同步操作由一个线程来执行。最多丢失2秒数据。
- no:将aof_buf的所有内容写入到AOF文件中,但是不同步,交由操作系统同步。
- redis-check-aof :当aof文件出错,使用该工具修复 redis-check-aof --fix
- appendfsync
- 持久化相关问题
- fork操作:fork操作对于操作系统来说是一个重量级操作,尤其是针对大内存Redis,高并发下fork操作可能会导致数万条Redis命令延迟。
- 耗时定位:info stats中latest_fork_usec获取最近一次fork耗时
- 改善操作
- 1. 优先使用物理机或者高效支持fork操作的虚拟化技术。
- 2. 控制Redis实例最大可用内存,fork耗时与内存量成正比。
- 3. 降低fork操作的频率,放款AOF自动触发时机。
- fork操作:fork操作对于操作系统来说是一个重量级操作,尤其是针对大内存Redis,高并发下fork操作可能会导致数万条Redis命令延迟。
- RDB:RDB是每隔指定时间生成一次内存快照。
-
主从
主从复制建议用单向链表结构更为稳定,即:Master <- Slave1 <- Slave2 <- Slave3...,当master挂了只要升级第一个Salve1为master即可,其他不要动- 复制:当从服务器启动时像主服务器发送sync信号,主服务器开始RDB后台备份,之后主服务器将rdb文件发送给从服务器。
- 从节点默认只读:对从节点的修改主节点无法感知,因此默认是只读模式。
- 全量复制:一般用于初次复制场景,会把主节点全部数据一次性发送给从节点。
全量复制有个过程是发送RDB文件,如果RDB过大导致网络IO被打满,并且Redis默认超时是60秒,就会导致复制失败 - 增量复制:由于网络抖动引起的主从断开等场景,主节点会补发丢失数据给从节点,使用复制游标控制。
- 复制计划缓冲区:初次复制时主节点会使用缓冲区缓存进来的写命令,等到复制完成之后再发送给从节点。
- 复制风暴问题:大量从节点短时间内对同一个主节点发起全量复制,主要是主节点重启时容易发生。
- 树状复制结构图解决
- 主节点不要分不到一台机器上
- 响应时间加长,避免带宽不足导致RDB发送超时。
- 复制:当从服务器启动时像主服务器发送sync信号,主服务器开始RDB后台备份,之后主服务器将rdb文件发送给从服务器。
-
哨兵 Sentinel
- 监控:Sentinel定期检测Redis数据节点以及其他Sentinel节点是否可达。
- 通知:Sentinel节点将故障转移结果通知到应用方
- 转移:Sentinel将从节点晋升为主节点并维护后续正确的主从关系。
-
集群:提供分布式数据库方案,提供数据共享,复制,故障转移等功能。
目前一台机器上通常跑多个实例,然后这些实例的管理就是集群所要做的。- Redis cluster:官方提供的集群方案
- 原理:一致性hash算法
对于cluster来说,其管理的redis集群分为16384个槽slot,其中每一个集群中的节点node可以选择负责一定范围内的槽。这是一种服务器Sharding技术,当一个key-value请求到集群后,会先计算出其落在哪个槽,然后去改槽中进行操作。集群中每一个node都是相互知道其他node的,因此客户端呢连接任意一个node都是可以达到访问整个集群的效果。因此客户端连接时往往指定全部节点的ip,然后客户端负责容错处理,对已经下线的节点进行标记,后续请求不请求该节点。 - 动态扩容
集群增加机器会使得slot重新分配,分配过程中涉及到之前键值对的拷贝。 - 故障处理
其推荐每一个node都配置为主从结构,当主master挂掉之后,对应的salve会自动提升为master,也就具备了故障转移功能。
- 原理:一致性hash算法
- Codis
-
原理
codis属于一个代理层,可以理解为其背后是一个容量无限制的redis的实例,客户端访问都是通过code_proxy访问,虽然redis没什么问题,但是该proxy本身要做到高可用 -
codis_proxy高可用
一般起多个,客户端可以配置多个proxy地址,做到高可用。或者中间增加一层ha
-
- Redis cluster:官方提供的集群方案
- Redis为什么那么快?
两个原因:1.纯内存操作,每一个命令执行都很快 2.使用Reactor模型,异步非阻塞IO处理,可以使用相当少的资源管理大量请求排队。Redis的因为是直接对内存操作,因此每一个命令都是非常快的,并且Redis的单进程实例,其只会用一个线程去处理客户端的请求,那么怎么应对客户端的并发请求呢?答案是串行线程封闭模式,该模式下客户端的并发请求会进去Redis的请求队列中,Redis从队列中依次取出每一个请求,执行完毕后返回客户端。 - flush all之后的处理
不小心执行了清空数据命令,应该立即执行 shutdown nosave,避免AOF文件重写。2. 修改AOF文件,删掉flushall指令,重启redis,让redis自行恢复。
- 分布式锁
新版的set指令支持原子性的设置key,以及超时时间。 - MySQL 里有 2000w 数据,redis 中只存 20w 的数据,如何保证 redis 中都是热点数据?
估算20w数据量所占用的内存,然后设置Redis的内存过期淘汰策略为LRU,当Redis内存满了后会自动淘汰对应的数据。 - Redis内存突然增长的原因?
https://mp.weixin.qq.com/s/eXKkfhdG8VyS9OmKZOkeEw其中的一种情况是Redis在rehash,rehash过程中Redis会使用数组中的第二个槽,然后rehash结束后替换,因此在这个过程中会新建很多指针引用,这些会占用大量内存,一个6000w规模的key-value可能会多产生2G内存。 - 大量key同时过期为什么可能造成cpu的卡顿?
定期删除策略是每一轮循环扫描出过期的key大于25%,则再次继续下一轮扫描,那么大量过期key存在的话,该任务会不停的执行,因此就可能造成cpu短期内被占用,也就是卡顿现象。解决方案是使得key的过期时间分布均匀,可以加上随机时间戳。本质原因Redis是单进程单线程结构,使用事件机制做的多任务处理。 - 缓存穿透解决方案:缓存穿透意思是缓存没命中,同时数据库也没命中,因此每次都需要去DB层查询。
- 缓存空对象:数据库查询不到则生成个空对象放入缓存中,下次查询则走缓存,启到保护DB的作用。
- 布隆过滤器拦截:其解决的问题是如何判断一个key是否在大量数据的池子中,可以使用redis bitmap实现
- 缓存雪崩解决方案:指缓存层挂掉,然后请求全部打到了DB层,导致数据库挂掉。
- 缓存服务做高可用
- 重要组件限流与隔离:缓慢访问远远好于服务挂掉。
- 热点缓存重建解决方案:缓存失效后需要重建,但可能由于重建很复杂,导致大量请求打到数据库层。
热点缓存比如排行榜,推荐榜等数据,并发访问很高,当缓存失效的一瞬间,很可能多个线程同时重建缓存,导致后端服务压力剧增。- 互斥锁:缓存重建时加锁,保证只有一个线程在重建缓存。
- 永不过期:所谓永不过期是缓存层面上不设置过期时间,应用层面上设置过期时间,当线程发现缓存过期之后,启动异步线程去更新数据,然后该线程先返回老数据。
该方案会导致数据短时间内的不一致性,取决于业务所能容忍的上限。该方案的优势就是能杜绝热点key所带来的问题。
- Redis CPU饱和怎么处理?
Redis是单线程架构,因此CPU饱和导致客户端的操作大量超时,Redis几乎不可用状态。此时使用Redis-cli --stat查看该Redis状态,定位到具体原因,如果是qps太大则考虑集群水平分担压力,如果是复制导致则考虑更改一些配置优化。使用 info commandstats可以查看命令执行详细,考虑是不是命令复杂度太高导致。 - 处理BigKey?
bigKey可能造成Redis的阻塞,因为单条命令耗时过长,并且在传输过程中可能造成网络的拥堵。使用redis-cli --bigkeys可以统计bigkey的分布。使用scan扫描监控 - 优秀文章与书籍
- Redis的设计与实现
- Redis开发与运维
- 美团针对Redis Rehash机制的探索和实践
https://mp.weixin.qq.com/s/ufoLJiXE0wU4Bc7ZbE9cDQ