高性能存储之--快速理解redis(简版)

原创
04/14 14:54
阅读数 89

为什么要用NoSql

随着流量的增加,链路性能会成为系统非常大的挑战,实现链路高性能有以下方案:

对于关系数据库,为解决高QPS带来的数据库压力,可以采用分库分表、读写分离。但还会存在一些挑战,比如IO压力大、索引维护成本高、数据库一致性加锁影响写性能、表结构schema难扩展、全文检索能力差。

针对于这些问题引用NoSql是一个不错的选择。

NoSql数据库可以满足弹性扩缩容、大数据高性能、灵活数据模型等需求。常见的NoSql包括解决缓存问题的Redis、解决聚合索引问题的ES、大数据量下的列式存储的HBase、以及文档存储的MongoDB。

今天我们先从常用解决缓存问题的Redis聊起。

什么是Redis

Redis是一种基于内存的key-value存储数据库,基于内存实现、采用单线程、非阻塞IO多路复用来满足大量数据读写效率要求高的场景。

优点是:查询复杂度O(1),TPS可达10W TPS。 缺点是:基于内存存储,空间有限,数据淘汰可能丢失数据;不支持条件查询。

所以redis常用于缓存数量受限,对数据持久要求不高,读多写少的场景。

常见数据类型

Redis主要支持五种数据结构:

  1. string
  2. hash
  3. list
  4. set
  5. zset

看下不同数据结构的底层支持:

string

value为字符串格式。 使用场景:可以存储账号及账号是否投票。

hash

一个存储空间存储多个key-value数据,底层使用hash表和压缩列表存储。在field少时,使用数组数据结构,数据多时,通过hashmap存储。 使用场景:购物车中,key为用户id,hash 的field是商品id,hash 的value是商品的数量。

list

list是顺序存储多个数据,底层是双向链表和压缩列表实现。数据以入队出队实现,可以数据分页查询。 使用场景:微博粉丝关注列表。

set

set在查询上效率高,只存储key而不是value,保证key不重复,提高了效率。 使用场景:用户之间的共同好友。

zset

zset是在set基础上增加了一个排序字段,实现了数据的有序存储。 使用场景:微博热搜榜单。

数据持久化

为提升数据持久能力,redis支持数据持久化。有两种方式:RDB,AOF; Redis支持rdb-aof混合持久化方式,如果关闭持久化能力,redis就退化成了一个纯内存数据库和memcache没啥区别了。

RDB

备份数据文件,默认开启RDB快照; 数据恢复速度更快;

利用数据快照方式记录数据键值对,在某一时刻生成数据快照文件替换原有文件; 触发方式有两种:自动和手动触发; 自动触发可以设置x秒内数据库修改了y次自动触发RDB的BGSAVE命令,把数据存储到磁盘中; 原理是主进程fork一个子进程完成数据持久,不影响主线程工作,实现了IO最大化。RDB存储方式几乎不影响数据库性能,所以一般默认开启RDB持久化方式; redis每次宕机重启后,能恢复到上一次rdb快照记录的数据,但数据安全性低,也就出现了AOF;

AOF

追加日志文件,默认关闭;

AOF包含三种日志机制持久化命令,always、everysec、no。 no:不进行数据fsync,将flush的时机交给操作系统决定,执行速度快; everysec:后代线程每秒执行一次数据存储; always:通过每写入一条日志进行一次fsync,数据持久性最好,但速度最慢;

默认策略是everysec,实现了取舍。

主进程的append写入命令追加到aof缓冲区,兼具了安全性和性能考虑,对磁盘进行文件同步sync操作。对磁盘进行文件同步sync操作,当aof文件大小超过阈值后,会进行aof重写。 重写是redis会fork一个子进程,读取aof文件,将指令合并执行,生成新文件替换老文件,压缩写入一个临时文件,子进程不影响主进程,这个过程不影响文件的可用性。

由于aof是主线程将命令写入缓冲区,所以性能上是有损耗的

内存淘汰机制

前面提到过,redis使用的是内存,所以会有总的容量限制,缓存的key需要进行清理。

在redis中有个过期字典,保存着key的过期时间,当数据到了过期时间后,会通过策略进行数据清理。

淘汰策略有两种方式:定期删除与惰性删除;

定时删除是redis默认每隔100ms随机抽取一些设置了过期时间的key,检查其是否过期,如过期,则删除,这里无需遍历所有的key检查,随机抽取即可。

为清理那些没机会遍历到的key,可以依靠惰性删除了,当查询一个key发现其过期了,就会被清理掉,保证过期的key不会被查询到。

这个设计是不是很巧妙,架构处处是取舍啊

如果所有的key都没有过期,但是内存满了,就需要进行内存淘汰了。

redis常用的淘汰策略是allkeys-lru 和 volatile-lru。

高可用实现

单实例总是有可用性风险,redis怎么实现高可用的呢?主要依靠持久化、复制、哨兵、集群。

持久化:持久化实现高可用的底层逻辑是备份,将数据备份到磁盘,保障进程重启后数据不丢失; 复制:复制是分布式存储系统常用的高可用手段,实现了数据的多机备份,通过负载均衡和故障转移与故障恢复实现了,数据的高可用。哨兵和集群都是基于它实现的; 哨兵:官方提供的一种redis实例的高可用解决方案,在复制的基础之上,实现自动化故障转移与故障恢复。但是单实例存储能力有限; 集群:通过集群可以解决无法负载均衡和存储能力有限问题,实现了完善的高可用;

主从切换

了解redis高可用,我们需要先了解下redis主从切换的过程。

哨兵结构:

如果主节点挂了,哨兵集群的主从切换过程:

  1. 判断主节点下线
  2. 选举新主
  3. 选举哨兵leader
  4. 主从切换

判断主节点下线

某个哨兵通过心跳监控到主节点下线后,会给其他哨兵发送确认命令,其他哨兵根据自己的判断,回复Y or N。根据所有哨兵的投票少数服从多数情况,判断节点下线。

看,redis的实现处处透露着简单

选举新主

哨兵们在挂掉节点的从节点中找到心跳保活最好、复制进度最高、ID编号最小的节点成为主节点。

选举哨兵leader

每次主从切换操作的主节点下线、从节点成主,都是由哨兵leader执行的。第一个判断主节点挂掉的哨兵会申请成为哨兵leader,成为哨兵leader需满足:赞成票大于半数。

主从切换

哨兵leader执行主从切换,需要完成以下通知:

  1. 通知其他哨兵新节点的地址;
  2. 通知所有从节点新的主节点地址,从节点收到通知之后申请主从同步;
  3. 通知客户端连接新的主节点。

总的来说,哨兵就是通过过半投票方式做了很多负责的决策。

缓存常见问题

我们看下在系统架构中,如何使用redis缓存:

使用缓存时,常见的问题主要有:缓存穿透、缓存雪崩、缓存击穿。

缓存穿透

缓存穿透是key不存在缓存中,导致每次请求都打到了数据库上,不能起到缓存保护数据库的目的,会被一些不法分子利用,对数据库进行攻击。

解决思路:

  1. 对于那些在缓存和数据库中都没有的无效key缓存下来,并设置一个过期时间,可以解决请求的key变化不频繁的情况;
  2. 构建一个布隆过滤器,记录全部的数据,通过请求和滤波器的对比判断是否存在,不存在则直接返回错误信息,无需查询缓存和数据库;
  3. 接口层添加校验,比如用户鉴权和参数校验等方式。

缓存雪崩

缓存雪崩是因为大量key设置了相同的过期时间,导致同一时间大量key失效,请求打到数据库,造成压力。

解决雪崩问题:

  1. 集群部署,防止单机出现问题使整个缓存服务没法使用;
  2. 对缓存进行实时监控,当访问速度小于阈值时,通过机器或者服务替换进行恢复;
  3. 增加DB的访问读写开关,当请求变慢到超越阈值的时候,关闭读开关,通过部分或者所有的请求failfast立即返回,等待恢复访问之后再打开开关;
  4. 热点数据设置为永不失效,有更新的情况就更新缓存即可;
  5. 设置数据的不同失效时间,比如设置随机的缓存失效时间,减少大规模失效带来的问题。

缓存击穿

缓存击穿是大量请求访问某个热点key,在热点key失效后,这些请求打到db。

解决缓存击穿的方法:

  1. 使用互斥锁:只让一个线程构建缓存,其他线程等待缓存构建完成,直接去缓存读取数据即可;
  2. 布隆过滤器:快速判断元素是否在集合中,当访问不存在的缓存时,返回信息避免数据库挂掉;
  3. 异步构建缓存:通过Redis等数据库维护一个失效时间,当过期的时候马上获取新的数据到cache缓存。

大key治理

大key是redis缓存另一个杀手,由于大key存储空间大,访问和存储对io、cpu、内存都有比较高的压力。

查找大key 的方法:

  • Redis-cli --bigkeys是redis-cli自带的一个命令。它对整个redis进行扫描,寻找较大的key,并打印统计结果;
  • Redis-rdb-tools工具,常用命令是 rdb -c memory dump.rdb (其中dump.rdb为Redis实例的持久化文件,可通过bgsave生成);
  • rdb_bigkeys工具
  • 使用方法:./rdb_bigkeys --bytes 1024 --file bigkeys.csv --sep 0 --sorted --threads 4 /home/redis/dump.rdb
  • /home/redis/dump.rdb 为实际的文件路径
  • 上述命令分析dump.rdb文件中大于1024bytes的KEY, 由大到小排好序, 以CSV格式把结果输出到bigkeys.csv的文件中;
  • Redis 4.0引入了memory usage命令和lazyfree机制,对大key的发现和删除做了改进。

大key 问题的解决:

  • 单个key的存储value值过大
  • 对象分拆为几个key-value对象,使用multiGet获取值,分散操作的压力;
  • 将数据存储在hash中,每一个field是一个具体的属性,使用hget、hmget获取value,使用hset、hmset来更新部分的属性;
  • hash、set、zset、list存储过多的数据
  • 根据一定的规则进行分散存储到多个Redis实例中。对于榜单类的数据缓存可以保留前几百条,如果需要其他数据就去数据库之中去获取。

热点key问题

如果缓存设计上存在热点key,就会对缓存使用带来风险,导致流量集中处理,缓存服务可能宕机。

热key的发现:

  • 提前预判热key的出现,比如在秒杀商城和抢票等场景下,根据已有经验去做热数据的预估,当然这种方法在有些场景下无法进行预估;
  • 客户端的收集
  • 在操作Redis之前加入一行代码进行统计,当然缺点是容易造成对客户端代码的入侵;
  • 在proxy层进行收集
  • 有些集群搭建有proxy层的入口,但没有这层设计的Redis架构就失效了;
  • 使用Redis自带的指令
  • monitor 命令,统计热key的数据;分析工具redis-fania ;hotkeys参数,redis-cli -hotkeys;
  • 抓包评估 抓包方式的开发和维护成本较高。

热key的解决:

  • 使用二级缓存 通过ehcache,或者hashmap来处理,把数据加载到系统的二级缓存中;
  • 备份热key 通过多个服务机器去存储热key的数据备份,在请求到来的时候,随机选取一台Redis服务器来处理访问的请求。

总结

今天我们了解了redis,看到了redis在设计上有非常多的巧妙之处,架构设计就是以最简单的方式解决复杂的问题,也体现了架构是“取舍”的哲学。

后续我们会介绍其他的nosql,比如es、hbase、mongoDB等。

展开阅读全文
打赏
0
0 收藏
分享
加载中
更多评论
打赏
0 评论
0 收藏
0
分享
返回顶部
顶部