在讨论 Redis 是什么之前,假设我们有这样一个场景,在我们的系统中,有一个商品购买的服务,这个商品服务在某个时间段对外提供 每秒 1w 次查询,但背后的 MySQL 只支持每秒 5000 次查询,这样的话 MySQL 就承受不住这样的流量,就会造成 MySQL 服务宕机。这类的问题也比较常见,比如说双11秒杀商品、春节抢车票等等。
那么问题也就显而易见了,如何让 MySQL 在不被压垮的情况下,又可以让服务对外提供每秒 1w 次的查询呢?
当然有,我们可以通过添加一层中间层,不要让这些请求全部打到MySQL服务上就可以了,而 Redis 也就是这样的一种中间层。
对于 Redis 的架构分析,我们可以分为以下几点:
本地缓存
我们知道,内存的读写速度要远大于磁盘读写,而 MySQL 数据主要存放在磁盘里,如果能将 MySQL 里的数据放到内存中,就可以提高我们的查询性能。
既然这样,我们就可以在商品购买服务的内存中申请一个字典dict/map,key作为商品id,value作为该商品的数据。那么在下次在MySQL 中查询时顺便将数据存放到字典中。这样我们就可以维护这样的一个字典,来满足我们内存查询的需求。
远程缓存
而现实是,我们的系统中的服务往往不止有一个实例,如果每个实例都重复缓存一份本地缓存的话,势必会造成内存的浪费。所以我们可以把本地缓存的字典抽出来,作为一个单独的缓存服务,来对这些实例做统一缓存。
既然这样,问题又出现了,既然有多个服务去读写同一缓存服务,就会引发并发问题,我们该如何解决?
对于这个问题,Redis 是不管对外有多少网络连接,收到读写命令后,都塞到一个线程上,在一个线程上对缓存进行读写,这样的话就避免了并发问题,也避免了线程上下文切换的开销。
多种数据类型
现在的缓存服务中,只有字典这一种数据类型,但是我们在实际开发中,还会用到像List,Set这样的一些数据结构,所以说 Redis 对数据类型做了扩充,比如说先进先出的队列List,还有像应用于排行榜统计的Zset等等。同时还有一些高级数据类型,像GEO,BitMap,HyperLogLog等等,这样就使我们的缓存服务更强了。
内存过期策略
既然支持了多种数据类型,随着我们的内存数据越来越多,而内存相对来说又比较贵,所以说我们需要一种过期机制来保证我们的内存消耗,而 Redis 它允许我们通过对缓存数据设置过期时间,来缓解内存消耗速度。
既然这样,问题就又出现了,如果有大量的key同时过期,而又由于 Redis 是单线程的,过期处理也需要占用主线程时间,那么 Redis 在处理这些删除操作时,会导致阻塞,影响正常的请求响应。
而 Redis 基于这点又做出了优化,引入惰性过期和定时过期策略。
惰性过期(Lazy Expiration):
定义:惰性过期是指 Redis 在访问某个键时,才检查该键是否过期。如果键已经过期,Redis 就会删除它。
优点:这种策略可以减少删除操作的次数,因为只有在需要访问键时,才进行检查。这有助于减少不必要的 CPU 和 I/O 操作,尤其是在一些过期键不常访问的情况下。
缺点:某些过期键可能很久都不被访问,导致它们一直占用内存,直到被访问时才删除。这会导致内存占用较高。
定时过期(Active Expiration):
定义:定时过期是指 Redis 定期扫描过期键,并主动删除它们。这是通过后台线程定时进行的。
优点:可以确保过期键会在一定时间内被清理,避免内存被占用过多,特别是在过期键不被访问的情况下。
缺点:增加了额外的内存扫描和删除操作,可能会对系统性能产生一定的负担,尤其是在大量过期键的情况下。
总的来说,Redis 通过惰性过期和定时过期相结合,能够在高性能和低内存占用之间找到平衡,确保系统稳定运行,同时避免不必要的资源浪费。
内存淘汰策略
现在的缓存服务支持了删除设置了过期时间的缓存数据,能够在性能和内存占用之间平衡。
但是,假设我们内存中数据更多的被常驻缓存使用,如果没有一种合理的决策去处理这些数据的话,势必会造成 Redis 内存溢出,所以为了避免这种情况的发生,Redis 提供了多种内存淘汰策略,具体如下:
noeviction:当达到内存限制时,新的写入操作会返回错误,不会删除任何数据。
volatile-lru:仅在设置了过期时间(TTL)的键中,删除最近最少使用的键。
volatile-ttl:仅在设置了过期时间(TTL)的键中,删除即将过期的键。
volatile-random:仅在设置了过期时间(TTL)的键中,随机删除一些键。
allkeys-lru:删除所有键中最近最少使用的键,无论该键是否设置了过期时间。
allkeys-random:随机删除所有键,无论该键是否设置了过期时间。
allkeys-ttl:删除所有键中即将过期的键。
内存淘汰策略可以帮助 Redis 在内存不足时做出合理的决策,避免内存溢出,并确保重要的数据(如频繁访问的数据或即将过期的数据)能够得到优先保护。
持久化
现在的缓存服务已经解决了内存过大的问题,但是 MySQL 之所以能够不被压垮,是因为前面有 Redis 缓存为它抵挡大部分流量,但是缓存服务一旦重启,那么存放的内存中的数据也就会消失,那么此时所有的流量都会打到 MySQL 上面,造成服务宕机。
所以说,我们需要尽最大程度的去保证 Redis 缓存数据的持久化,确保在缓存重启后不至于啥也没有。
而 Redis 是提供了 RDB 和 AOF 两种持久化机制:
在服务中去加入一个异步线程,定期将全量的内存数据定期持久化到磁盘文件里,而这种将内存数据生成快照保存到文件的方式,就是所谓的 RDB 机制(Redis Database Backup),RDB 机制能够保证缓存服务重启后恢复大部分的数据,之所以是大部分,是因为写入快照之后的数据会丢失。
全量数据备份当然耗时,那我们化整为零,在每次写入数据时,顺手将数据记录到文件缓存中,并每秒将文件缓存刷入磁盘,这种持久化机制叫 AOF 机制(Append Only File),服务启动时跟着文件重新执行一遍就能将大部分数据还原,最大程度保证了数据持久化。当然如果 AOF 文件过大,我们可以采取定期重写压缩即可。
总结
Redis是什么?
Redis 是一种高性能,支持多种数据类型和各种缓存淘汰策略,并提供一定持久化能力的缓存服务。当然,Redis 不光是可以作为缓存,还可以通过插件实现其他功能,比如说 RedisJSON,RedisSearch 等等。
新的问题
现在 Redis 虽然已经达到了高性能服务的标准,但是目前就是个单机服务,虽然高性能是有了,但是高可用以及可扩展性该怎么解决呢?
Redis 提供了集群化部署方式来满足实际生产中需求
1. 主从集群(Master-Slave Replication)
架构:一个主节点(Master)和多个从节点(Slave),主节点处理写操作,从节点负责数据的备份并提供读操作。
特点:
主节点负责写操作,从节点负责同步主节点的数据并提供读取请求。
高可用性:可以通过手动切换主从节点来实现简单的故障恢复,但没有自动故障转移功能。
缺点:
主节点写操作可能成为瓶颈,无法实现自动故障转移。
2. 哨兵集群(Sentinel)
架构:通过 Redis Sentinel 监控主从结构的 Redis 实例,提供高可用性和故障转移功能。
特点:
高可用性:当主节点发生故障时,Sentinel 会自动将某个从节点提升为新的主节点。
监控:Sentinel 会监控 Redis 实例的健康状况,并提供通知。
自动故障转移:当主节点宕机时,Sentinel 自动进行故障转移,将从节点提升为主节点。
缺点:
Sentinel 本身也有单点故障问题,虽然可以配置多个 Sentinel 实例来解决,但仍需注意高可用配置。
3. Cluster 集群(Redis Cluster)
架构:将数据分布到多个 Redis 节点中,通过分片(Sharding)来扩展存储和处理能力。
特点:
数据分片:数据会被分成多个哈希槽,并分布到多个节点上。
自动扩展:可以通过增加节点来水平扩展 Redis 集群。
高可用性:每个分片都有主从节点,当主节点失败时,自动将从节点提升为主节点。
去中心化:没有中心化的管理节点,每个节点都能和其他节点进行通信。
优点:
提供分布式存储和高可用性,能够支持大规模、高吞吐量的应用。
到这里,现在的Redis就是一个高性能,高可用,可扩展性强的缓存服务了。