Redis由Salvatore Sanfilippo使用C语言编写的一种支持网络、可基于内存亦可持久化的日志型、Key-Value数据库。
2.1 redis的支持数据持久化
可以将内存中的数据保存在磁盘中,重启时通过再次加载,这样数据不会丢失;
2.2 Redis支持数据类型丰富
支持String,List,Set,Zset(sorted sets),Hash数据结构的存储;
2.3 Redis支持master-slave模式的数据备份;
2.4 Redis的操作时原子性的
Redis提供了简单的事务功能, 将一组需要一起执行的命令放到multi和exec两个命令之间(其中 multi命令代表事务开始, exec命令代表事务结束),它们之间的命令是原子顺序执行的。
2.5 性能极高
读写数据每秒可达10万次左右。
Redis提供了RDB(Redis DataBase)和AOF(Append Only File)两种持久化方式。
3.1 RDB是指在一定的时间间隔将数据快照存储到磁盘。
当到达数据备份时间时,Redis会启动一个线程,该线程将数据保存到一个临时文件,以替换上个时间周期备份的临时文件。优点:数据恢复快,缺点:最后一次持久化后的数据有可能会丢失。
3.2 AOF是以日志的形式记录每次对Redis的写操作
以redis协议追加保存每次写的操作到文件末尾,当Redis重启的时,重新执行这些命令来恢复原始的数据。优点:数据实时同步,缺点:AOF文件持续增长而过大(Redis通过设置文件大小的阀值来触发AOF文件的压缩)。
注:当同时开启RDB和AOF时,系统优先使用AOF。
4.1 主从复制的延迟
主从复制可以通过slaveof命令配置实现,默认情况下Redis都是主节点,每个从节点只能有一个主节点,而主节点可以有多个从节点,故复制的数据流是单向的,即只能从主节点复制到从节点,因此主节点无法感知从节点的修改,所以一般从节点提供只读模式。
由于主从部署在不同的机器上,Redis提供了repl-disable-tcp-nodelay参数用于控制是否关闭
TCP_NODELAY, 默认关闭。
a.当关闭时, 主节点产生的命令数据大小都会及时地发送给从节点, 优点:延迟会变小, 缺点:网络带宽的消耗增加。 适用于同机架或同机房部署等网络环境好的场景。
b.当开启时, 主节点会合并较小的数据包发送到从节点,一般时间间隔设置为30-50毫秒。优点:节省带宽,缺点:增大主从之间的延迟。 适用于主从网络环境较差的环境, 如跨机房部署等。
注意:从节点更换主节点后从节点会清空之前所有的数据, 所以执行该操作时一定要在正确的主节点和从节点上。
4.2 Redis拓扑结构
4.2.1 一主一从
4.2.2 一主多从
4.2.3 树状主从
4.3 主从复制过程
1.从节点保存主节点信息
即执行slaveof后,从节点保存主节点的地址信息。
2.主从建立Socket连接
从节点通过执行定时任务(即每秒执行一次)发现slaveof新的主节点后,会尝试建立Socket网络连接。如果连接失败,从节点会通过定时任务来无限重试。
3.发送ping命令
Socket连接成功后,从节点发送ping请求进行首次通信,主要目的是:检测主从之间网络套接字是否可用,并检测主节点当前是否可接受处理命令。
4.权限验证
如果主节点设置了requirepass参数, 则需要密码验证,从节点必须配置masterauth参数保证与主节点相同的密码才能通过验证。
5.同步数据集
主从复制连接正常后, 对于首次建立复制的场景, 主节点会把持有的数据全部发送给从节点,
6.命令持续复制
主节点会持续地把写命令发送给从节点, 从而保证主从数据一致。
4.4 数据延迟
Redis主从数据的延迟由于异步复制特性导致的,是无法避免的, 延迟受到网络带宽和命令阻塞情况的影响, 如果业务不允许短时间内的数据延迟, 可以编码实现监控程序,监听主从节点的复制偏移量, 当延迟较大时触发报警或通知客户端避免读取延迟过大的从节点,从而避免读不到数据的情况发生。
4.5 过期数据的处理
4.5.1 惰性删除
主节点每次处理读取命令时,除了读取key-value表,还会读取Redis超时时间表,以检查key是否超时, 如果超时则删除key对象, 之后del命令也会异步发送给从节点,从节点再执行del。
4.5.2 定时删除
Redis主节点在内部定时任务会循环读取一定数量的Key, 当发现读取的Key过期时执行del命令,之后del命令也会异步发送给从节点,从节点再执行del。
注意:为了保证复制数据的一致性, 从节点自身不会主动删除超时数据。
4.6 Redis读写分离的考量
考虑到数据延迟,过期数据等,以及Redis的高性能,因此在考虑读写分离之前,一定要在主节点上做好充分优化,如解决慢查询,持久化阻塞和合理的数据结构等,当仍然不满足时,可以先考虑Redis Cluster等分布式方案,再考虑读写分离方案。
由于Redis是典型的单线程架构,所有的读写操作都在一条主线程中完成,因此如果主线程拥塞了,那将是业务系统的噩梦。
造成阻塞的原因如下:
5.1 API或数据结构使用不合理
问题定位:1.通过slowlog get { n } 获取最近n条慢查询(Redis默认对于执行超过10毫秒的命令都会记录到一个定长队列(队列默认长度为128)中),发现慢查询后,可以通过使用低算法复杂度的命令或避免大对象数据来优化。
5.2 CPU饱和的问题
CPU饱和是指Redis把单核CPU(由于Redis是单线程,只能使用一个CPU)使用率跑到接近100%。可以使用top命令查看Redis线程的CPU使用率,可参考:https://www.jianshu.com/p/6d7571d82304。对于Redis的使用情况,可通过redis-cli-h{ip}-p{port}--stat命令每秒输出Redis的统计信息。如果此时发现Redis的请求量比较低,那说明使用了高复杂度算法的命令;如果此时发现Redis的请求量已经很高了,而且应用的优化有限,那就需要使用新的Redis来分摊请求和CPU的压力。
5.3 持久化相关的阻塞
对于开启了持久化功能的Redis节点,要检查是否是fork阻塞,AOF刷盘阻塞和HugePage写操作阻塞导致的CPU使用率过高。具体可参考官网:http://www.redis.io/topics/latency
5.4 CPU竞争
当Redis和其他多核CPU密集型服务部署在一起时,其他进程过度消耗CPU时, 将严重影响Redis吞吐量。可以将Redis绑定到一个CPU上,避免CPU切换的开销。
5.5 内存交换
如果系统把Redis的部分内存中的数据换出并序列化到磁盘后,如果请求的数据不在内存中,需要内存交换,那么Redis性能严重下降。解决方式:扩大内存,设置Redis的最大可用内存,降低系统内存交换执行的优先级。
5.6 网络问题
解决方式:增加带宽,同机房部署,机房部署使用专线,改善机器的物理拓扑(同物理机>同机架>跨机架>同机房>同城机房>异地机房,注意:容灾性相反)
6.1.纯内存
Redis将所有数据放在内存中,而内存的相应时长在100纳秒左右。
6.2.非阻塞I/O
Redis使用了epoll多路复用技术,另外Redis自身的事件处理模型将epoll中的连接、 读写、关闭都转换为事件。
6.3.没有线程切换
Redis采用了单线程架构,避免了线程切换、竞态和锁等产生的自我消耗。
Redis提供了批量操作命令(如mget、 mset、hmset等),有效的节约了多次访问的往返时间,但是有些命令没有对应的批量命令(如hgetall没有对应的mhgetall),如果n次调用,就会消耗n次往返时间。Redis针对这一问题提供了Pipeline流水线机制,Redis将一组Redis命令组装到一起,通过一次调用Redis,再将执行结果按照请求顺序返回客户端。
info memory命令可以显示内存的相关指标,主要有used_memory(Redis内部存储所有数据内存占用量),used_memory_ssr(操作系统显示Redis进程占用的物理内存)以及他们的比值mem_fragmentation_ratio。
当mem_fragmentation_ratio>1时,说明有部分内存没有用于数据存储,而是被内存碎片消耗,因此要降低mem_fragmentation_ratio的值,即降低碎片率。
当mem_fragmentation_ratio<1时,是因为操作系统把Redis的内存数据交换到磁盘,因此used_memory_ssr才会大于used_memory,由于硬盘的读写速度和内存差距很大,此时Redis的性能会很差。
8.2.内存的使用划分
8.2.1 Redis的进程自身的内存
这部分数据在几MB级别,很小可以忽略不计。
8.2.2 对象内存
即所有key-value数据使用的内存,包括所有key的消耗和所有value的消耗。
8.2.3 缓存内存
主要包括客户端缓存,复制积压区缓存,AOF缓存。
8.2.3.1 客户端缓存:
指的是所有接入到Redis服务器TCP连接的输入输出缓冲。输入缓冲无法控制(最大空间为1G), 如果超过将断开连接。 输出缓冲通过参数控制(默认为1G)
a.客户端:可以通过设置最大客户端连接数(特别是有慢连接时)来限制大量客户端接入造成的内存消耗
b.主从复制客户端:当主从复制延迟较高或从节点挂载从节点数量过多时,内存消耗将增加。可通过改善网络环境和减少主节点挂载的从节点数来解决。
c.订阅客户端:使用发布/订阅时, 连接客户端使用独立的输出缓冲区,对于订阅消息短时间内量比较大时,输出缓冲区会产生积压溢出。
d 复制积压缓存区
复制积压缓存区主要用来实现主从复制功能,一个主节点只有一个复制积压缓存区,所有从节点共享该缓存区。
e AOF缓冲区
AOF缓冲区用于在Redis重写期间保存最近的写入命令,AOF缓存区的消耗大小由AOF重写时间和写入命令量决定,这部分空间占用量比较小。
8.2.3.2 内存碎片
Redis提供了jemalloc(默认),glibc和tcmalloc三种内存分配器。当存储的数据长短差异较大时,做频繁的更新,删除大量key。主要解决碎片的方法有:
a.数据对齐,尽量采用数字类型或者固定长度字符串等;
b.Redis重启,内存会重新整理。
8.2.3.3 子进程内存消耗
子进程内存消耗主要指执行AOF/RDB重写时Redis创建的子进程内存消耗。
7.3.1 控制内存上限
maxmemory参数设置最大可用内存,一方面方式所用内存超过物理内存,另一方面用于缓存,超过上限时,触发内存回收,以便释放空间。
7.3.2 内存回收
a.删除过期key对象
参考上文
b.内存溢出控制策略
内存达到maxmemory上限时会触发溢出控制策略,Redis提供了6种溢出控制策略:
1)noeviction:即拒绝写只响应读(默认);
2) volatile-lru:根据LRU(Least Recently Used,即最近最少使用),删除过期的key数据,以腾出空间。如果没有可删除的过期key数据,回退到noeviction;
3)allkeys-lru:不考虑过期key数据的LRU算法;
4)allkeys-random:随机删除所有键;
5)volatile-random:随机删除过期键;
6)volatile-ttl:根据键值对象的ttl属性, 删除将要过期的key数据。 如果没有可删除的数据, 回退到noeviction策略。
7.3.3 手动内存回收
可以使用scan+object idletime命令批量查询哪些key长时间未被访问, 然后对key进行清理, 可降低内存占用。
7.4.1 缩减key(key值越短越好)和value的值的大小
主要方法有把key和value序列化成二进制数组数据(可以通过protostuff,kryo等高效序列化工具)或者序列化成json,xml压缩(如GZIP,Snappy压缩工具)再放入Redis,去掉value中不必要的字段。
7.4.2 共享对象池
共享对象池是指Redis内部维护[0-9999]的整数对象池,避免Redis封装RedisObject(Redis存储的数据都使用redisObject来封装)内存消耗。
7.4.3 字符串优化
a.Redis针对字符串采用了预分配机制,防止更新操作需要不断重分配内存和字节数据拷贝,但是同事造成了内存浪费。因此尽量减少字符串频繁修改操作(如append、setrange),直接使用set修改字符串。
7.4.4 字符串重构
避免每份数据作为字符串整体存储, 像json这样的数据可以使用hash结构, 使用二级结构存储能够节省内存。 可以使用hmget、 hmset命令支持字段的部分读取修改, 而不用每次整体存取。
7.4.5 编码优化
Redis通过不同编码实现效率和空间的平衡,编码类型转换在Redis写入数据时自动完成, 这个转换过程是不可逆的, 转换规则只能从小内存编码向大内存编码转换。针对性能要求较高的场景使用ziplist
7.4.6 控制key的数量
利用Redis的数据结构降低外层键的数量,比如把大量键分组映射到多个hash(用field记录副key)结构中降低键的数量。