Redis--学习笔记分享

Redis

简介:基于内存的key-value数据库,使用单线程模型处理命令,当命令进入时,不是立即执行,而是进入队列,从而保证同一时间不会有两条命令执行。

限制并发的一种做法就是利用队列特性来保证优先级问题。

安装配置:https://github.com/judasn/Linux-Tutorial/blob/master/markdown-file/Redis-Install-And-Settings.md

结构图

单线程Reactor模型:因此需要额外注意调用命令的操作复杂度,避免阻塞Redis。

基本数据结构:构成Redis的底层数据结构

字符串:类似于Java中的StringBuilder,内部存放着一个char数组,当前使用量,因此插入前会先扩容处理。

int len int free char buf[]​​

链表:类似于Java中的LinkedList,为双向链表

每一个节点如下 ​​listNode *prev listNode *next void *value​​

字典:实际上是hash表的实现,类似于Java中的HashMap,hash冲突也是使用链地址法解决。

dictEntry **table long size long used long sizemask​​​

跳跃表:redis是单线程处理请求,因此并发性是不需要考虑的,跳跃表作为有序集合的实现是很容易实现范围查找的。

整数集合:适用于值都是整数,并且元素数量不多的情况下的集合实现方式

整数集合就是一个数组+长度 ​int8_t ​contents[] uint32_t length uint32_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,导致当前分片内存分配不均匀。

附加功能

订阅/发布

在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),

内存回收: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

持久化相关问题

fork操作:fork操作对于操作系统来说是一个重量级操作,尤其是针对大内存Redis,高并发下fork操作可能会导致数万条Redis命令延迟。

耗时定位:info stats中latest_fork_usec获取最近一次fork耗时

改善操作

1. 优先使用物理机或者高效支持fork操作的虚拟化技术。

2. 控制Redis实例最大可用内存,fork耗时与内存量成正比。

3. 降低fork操作的频率,放款AOF自动触发时机。

主从

主从复制建议用单向链表结构更为稳定,即:Master <- Slave1 <- Slave2 <- Slave3...,当master挂了只要升级第一个Salve1为master即可,其他不要动

复制:当从服务器启动时像主服务器发送sync信号,主服务器开始RDB后台备份,之后主服务器将rdb文件发送给从服务器。

从节点默认只读:对从节点的修改主节点无法感知,因此默认是只读模式。

全量复制:一般用于初次复制场景,会把主节点全部数据一次性发送给从节点。

全量复制有个过程是发送RDB文件,如果RDB过大导致网络IO被打满,并且Redis默认超时是60秒,就会导致复制失败

增量复制:由于网络抖动引起的主从断开等场景,主节点会补发丢失数据给从节点,使用复制游标控制。

复制计划缓冲区:初次复制时主节点会使用缓冲区缓存进来的写命令,等到复制完成之后再发送给从节点。

复制风暴问题:大量从节点短时间内对同一个主节点发起全量复制,主要是主节点重启时容易发生。

树状复制结构图解决

主节点不要分不到一台机器上

响应时间加长,避免带宽不足导致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,也就具备了故障转移功能。

Codis

原理

codis属于一个代理层,可以理解为其背后是一个容量无限制的redis的实例,客户端访问都是通过code_proxy访问,虽然redis没什么问题,但是该proxy本身要做到高可用

codis_proxy高可用

一般起多个,客户端可以配置多个proxy地址,做到高可用。 ​或者中间增加一层ha​

常见问题

Redis为什么那么快?

两个原因:1.纯内存操作,每一个命令执行都很快 2.使用Reactor模型,异步非阻塞IO处理,可以使用相当少的资源管理大量请求排队。 ​ ​Redis的因为是直接对内存操作,因此每一个命令都是非常快的,并且Redis的单进程实例,其只会用一个线程去处理客户端的请求,那么怎么应对客户端的并发请求呢? 答案是串行线程封闭模式,该模式下客户端的并发请求会进去Redis的请求队列中,Redis从队列中依次取出每一个请求,执行完毕后返回客户端。​

flush all之后的处理

1. 不小心执行了清空数据命令,应该立即执行 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扫描监控​

欢迎工作一到五年的Java工程师朋友们加入Java架构开发:779792048

本群提供免费的学习指导 架构资料 以及免费的解答

不懂得问题都可以在本群提出来 之后还会有职业生涯规划以及面试指导

猜你喜欢

转载自blog.csdn.net/yupi1057/article/details/82427363