Redis基础
简介
Redis(Remote Dictionary Server)是一个开源的、基于内存的数据存储和缓存系统。它是一个高性能的键值存储数据库,,以其快速的读写能力、丰富的数据结构和多种应用场景而受到广泛关注。默认的database有16个,可以随意选择0-15。
在互联网发展的初期,关系型数据库因其能够满足较低的访问和并发需求而得到广泛应用。然而,随着应用规模的扩大和对性能的不断提高,关系型数据库的一些局限性逐渐显现。特别是在需要快速读写、支持大规模数据、具备高可用性和可扩展性的情况下,传统关系型数据库开始显得力不从心。因此,NoSQL 数据库应运而生,而 Redis 就是其中的佼佼者之一。
优点
-
高性能: Redis 是一个基于内存的数据库,因此具有快速的读写速度。它的数据存储在内存中,减少了磁盘 I/O 的开销,使得 Redis 能够在毫秒级别处理大量的读写操作。每秒可以执行大约 110 000 个写入操作,或者 81 000 个读操作,其速度远超数据库。
-
多种数据结构: Redis 支持丰富的数据结构,包括字符串、哈希表、列表、集合和有序集合等。这种多样性使得 Redis 能够应对不同的应用场景。
-
持久性选项: Redis 提供了多种持久化选项,可以将数据持久化到磁盘,以防止数据丢失。
-
支持 6 种数据类型
-
原子性操作: Redis 支持原子性操作,保证在多个客户端并发访问时,操作是原子的。这对于实现计数器、锁等功能非常有用。当两个客户同时访问 Redis 服务器时,确保得到的是更新后的值(最新值)。在需要高并发的场合可以考虑使用 Redis 的事务,处理一些需要锁的业务。
-
集群和分布式支持: Redis 支持分布式架构,可以通过主从复制和分片等方式实现高可用性和横向扩展。
-
丰富的功能: Redis 提供了丰富的功能和命令集,包括事务、发布/订阅、Lua 脚本执行等。
缺点
-
数据持久化对性能的影响: 开启持久化选项可能对 Redis 的性能产生一定的影响,特别是在每次写入都要进行持久化时。
-
内存消耗: Redis 的数据存储在内存中,因此对于大规模数据集,内存消耗可能较大,不能用作海量数据的高性能读写,因此Redis适合的场景主要局限在较小数据量的高性能操作和运算上。
-
单线程模型: Redis 使用单线程模型来处理请求,这在一些特定场景下可能导致性能瓶颈。然而,通过多实例部署和使用分布式特性可以一定程度上缓解这个问题。
-
Redis 不具备自动容错和恢复功能,主机从机的宕机都会导致前端部分读写请求失败,需要等待机器重启或者手动切换前端的IP才能恢复。
-
主机宕机,宕机前有部分数据未能及时同步到从机,切换IP后还会引入数据不一致的问题,降低了系统的可用性。
-
Redis 较难支持在线扩容,在集群容量达到上限时在线扩容会变得很复杂。为避免这一问题,运维人员在系统上线时必须确保有足够的空间,这对资源造成了很大的浪费
使用场景
-
缓存: 用户查数据,首先从缓存中查询,如果缓存中没有,就从数据库查,如果查到数据,则返回数据给用户,并且会把数据添加到缓 存中,下次访问这些数据的时候,会直接从缓存中去查,不会再去数据库,减轻了数据库的压力
-
会话缓存:可以使用 Redis 来统一存储多台应用服务器的会话信息。当应用服务器不再存储用户的会话信息,也就不再具有状态,一个用户可以请求任意一个应用服务器,从而更容易实现高可用性以及可伸缩性。
-
实时分析: 存储和分析实时数据,支持快速查询和聚合
-
排行榜: 存储和计算用户分数,用于排行榜应用。
-
消息队列: 作为轻量级的消息队列,支持发布/订阅机制,用于异步任务处理。List 是一个双向链表,可以通过 lpush 和 rpop 写入和读取消息 。不过最好使用 Kafka、RabbitMQ 等消息中间件。
-
计数器: 用于实时计数,如网站访问次数、点赞数等。通过对 String 进行自增自减运算,从而实现计数器功能。Redis 这种内存型数据库的读写性能非常高,很适合存储频繁读写的计数量。
-
查找表: 例如 DNS 记录就很适合使用 Redis 进行存储。查找表和缓存类似,也是利用了 Redis 快速的查找特性。但是查找表的内容不能失效,而缓存的内容可以失效,因为缓存不作为可靠的数据来源。
-
分布式锁:在分布式场景下,无法使用单机环境下的锁来对多个节点上的进程进行同步。可以使用 Redis 自带的 SETNX 命令实现分布式锁,除此之外,还可以使用官方提供的 RedLock 分布式锁实现。
-
其他:Set 可以实现交集、并集等操作,从而实现共同好友等功能。 ZSet 可以实现有序性操作,从而实现排行榜等功能。
基本数据结构
Redis支持多种基本数据结构,包括:
- 字符串(String): 存储文本或二进制数据。
- 哈希表(Hash): 用于存储键值对的无序散列表。
- 列表(List): 有序的字符串元素集合。
- 集合(Set): 无序且唯一的字符串元素集合。
- 有序集合(Sorted Set): 有序的、唯一的字符串元素集合,每个元素都关联一个分数
这些数据结构使得Redis能够灵活应对各种场景,从而成为一种功能强大且广泛应用的内存存储系统。
基本命令
以下是一些基本的Redis命令及其使用方法:
# 连接到Redis服务器
redis-cli
# 设置键值对
SET key value
# 获取键对应的值
GET key
# 删除键值对
DEL key
# 检查键是否存在
EXISTS key
# 设置键的过期时间(秒)
EXPIRE key seconds
# 获取键的剩余过期时间(秒)
TTL key
# 批量设置键值对
MSET key1 value1 key2 value2 ...
# 批量获取键对应的值
MGET key1 key2 ...
# 查看所有键
KEYS *
# 清空数据库
FLUSHDB
- 列表操作
# 在列表左侧添加元素
LPUSH key element
#- 在列表右侧添加元素
RPUSH key element
# 获取列表指定范围的元素
LRANGE key start stop
- 集合操作
# 向集合添加元素
SADD key member
# 获取集合所有成员
SMEMBERS key
- 有序集合操作
# 向有序集合添加元素
ZADD key score member
# 获取有序集合指定范围的元素
ZRANGE key start stop
- 发布订阅
# 订阅频道
SUBSCRIBE channel
# 向频道发布消息
PUBLISH channel message
- 事务
# 开启事务
MULTI
# 执行事务
EXEC
# 放弃事务
DISCARD
持久化
Redis的数据时存在内存中的,当服务器宕机时,Redis中存储的数据就会丢失,为了保证数据在服务器重启后恢复,需要将内存中的数据持久化到硬盘上。
RDB(Redis DataBase)
RDB 是 Redis DataBase 的缩写,即内存块照。快照就是在某一时刻,将Redis中的所有数据,以文件的形式存储起来。这就类似于照片,当你给朋友拍照时,一张照片就能把朋友一瞬间的形象完全记下来。我们可以将快照复制到其它服务器从而创建具有相同数据的服务器副本。如果数据量很大,保存快照的时间会很长。如果系统发生故障,将会丢失最后一次创建快照之后的数据。使用时注意:
-
全量快照
Redis 的数据都在内存中,为了提供所有数据的可靠性保证,它执行的是全量快照,也就是说,把内存中的所有数据都记录到磁盘中,这就类似于给 100 个人拍合影,把每一个人都拍进照片里。这样做的好处是,一次性记录了所有数据,一个都不少。
缺点随着Redis中的数据变多,快照文件写入磁盘的时间也就越长。那么Redis在写入磁盘的时候可能会阻塞主线程,从而影响Redis的性能了。因此Redis提供了
save
和bgsave
两个命令来生产全量的RDB文件 。-
save
在主线程中执行,会导致阻塞;
-
bgsave
创建一个子进程,专门用于写入 RDB 文件,避免了主线程的阻塞,这也是 Redis RDB 文件生成的默认配置。由于不会阻塞主线程,因此一般我们选择bgsave方式,他的具体实现逻辑如下:
如上图所示,快照开始时,主线程会fork出一个用于快照操作的子线程,并且复制一份数据对应的映射页表给子线程。子线程可以通过这个页表访问主线程的原始数据,然后将数据生成快照文件,存储到磁盘中。我们知道存储磁盘的时间是比较长的,当这个时候有请求进行想写数据怎么办呢?这个时候就要用到 写时复制,即当请求需要对键值C进行操作时,主线程会把新数据或修改后的数据写到一个新的物理内存地址上(键值对C’),并修改主线程自己的页表映射。所以,子进程读到的类似于原始数据的一个副本,而主线程也可以正常进行修改。
这既保证了快照的完整性,也允许主线程同时对数据进行修改,避免了对正常业务的影响。
-
-
增量快照
如果一直使用全量同步,一方面时间的推移,磁盘存储的快照文件会越来越多。另一方面如果频繁的进行全量同步,则需要主线线程频繁的fork出bgsvae线程,这样对Redis的性能是会产生影响的,并且也需要持续的对磁盘进行写操作。
这个时候,我们可以采用另一只同步方式:增量快照。所谓增量快照,就是指,做了一次全量快照后,后续的快照只对修改的数据进行快照记录,这样可以避免每次全量快照的开销。在第一次做完全量快照后,T1 和 T2 时刻如果再做快照,我们只需要将被修改的数据写入快照文件就行。但是,这么做的前提是,我们需要记住哪些数据被修改了。你可不要小瞧这个“记住”功能,它需要我们使用额外的元数据信息去记录哪些数据被修改了,这会带来额外的空间开销问题。如下图所示:
如果我们对每一个键值对的修改,都做个记录,那么,如果有 1 万个被修改的键值对,我们就需要有 1 万条额外的记录。而且,有的时候,键值对非常小,比如只有 32 字节,而记录它被修改的元数据信息,可能就需要 8 字节,这样的画,为了“记住”修改,引入的额外空间开销比较大。这对于内存资源宝贵的 Redis 来说,有些得不偿失。
所以说,全量快照和增量快照都有各自的优点和缺点,至于实际应用时,则要根据具体情况进行权衡。
AOF (Append Only File)
AOF(Append Only File) 持久化功能则提供了一种更为可靠的持久化方式。 每当 Redis 接受到会修改数据集的命令时,就会把命令追加到 AOF 文件的末尾,当你重启 Redis 时,AOF 文件里的命令会被重新执行一次,重建数据。
AOF持久化默认是关闭的,通过将 redis.conf
中将appendonly no
,修改为appendonly yes
来开启AOF 持久化功能,如果服务器开始了 AOF 持久化功能,服务器会优先使用 AOF 文件来还原数据库状态。只有在 AOF 持久化功能处于关闭状态时,服务器才会使用 RDB 文件来还原数据库状态。
当 AOF 持久化功能处于打开状态时,服务器在执行完一个写命令之后,会以协议格式将被执行的写命令追加到服务器状态的 aof_buf 缓冲区的末尾。
Redis 的服务器进程就是一个事件循环(loop),这个循环中的文件事件负责接受客户端的命令请求,以及向客户端发送命令回复,而时间事件则负责执行像 serverCron 函数这样需要定时运行的函数。
因为服务器在处理文件事件时可能会执行写命令,使得一些内容被追加到 aof_buf 缓冲区里面,所以在服务器每次结束一个事件循环之前,它都会调用 flushAppendOnlyFile 函数,考虑是否需要将 aof_buf 缓冲区的内容写入和同步到 AOF 文件里,这个过程可以用伪代码表示:
def evenLoop () :
while True :
processFileEvents() # 处理文件事件,接受命令请求以及发送命令回复
processTimeEvents() # 处理时间事件,处理请求时可能会有新内容被追加到 aof_buf 缓冲区
flushAppendOnlyFile() # 考虑是否要将 aof_buf 中的内容写入和保存到 AOF 文件里
flushAppendOnlyFile
函数的行为由服务器配置的appendfsync
选项的值来决定,因此我们需要设置同步选项,确保写命令同步到磁盘文件上的时机。有以下同步选项:
-
always
:每个写命令都同步选项会严重减低服务器的性能;
随着服务器写请求的增多,AOF 文件会越来越大。Redis 提供了一种将 AOF 重写的特性,能够去除 AOF 文件中的冗余写命令。当 appendfsync 的值为 always 时,服务器在每个事件循环都要将 aof_buf 缓冲区中的所有内容写入到 AOF 文件,并且同步 AOF 文件,所以 always 的效率是 appendfsync 选项三个值当中最慢的一个,但从安全性来说,always 也是最安全的,因为即使出现故障停机,AOF 持久化也只会丢失一个事件循环中所产生的命令数据。
-
everysec
(默认):每秒同步一次,可以保证系统崩溃时只会丢失一秒左右的数据,并且 Redis 每秒执行一次同步对服务器性能几乎没有任何影响;当 appendfsync 的值为 everysec 时,服务器在每个事件循环都要将 aof_buf 缓冲区中的所有内容写入到 AOF 文件,并且每隔一秒就要在子线程中对 AOF 文件进行一次同步。从效率上来讲,everysec 模式足够快,并且就算出现故障停机,数据库也只丢失一秒钟的命令数据。
-
no
: 让操作系统来决定何时同步,该选项并不能给服务器性能带来多大的提升,而且也会增加系统崩溃时数据丢失的数量当 appendfsync 的值为 no 时,服务器在每个事件循环都要将 aof_buf 缓冲区中的所有内容写入到 AOF 文件,至于何时对 AOF 文件进行同步,则由操作系统控制。因为处于 no 模式下的 flushAppendOnlyFile 调用无须执行同步操作,所以该模式下的 AOF 文件写入速度总是最快的,不过因为这种模式会在系统缓存中积累一段时间的写入数据,所以该模式的单次同步时长通常是三种模式中时间最长的。从平摊操作的角度来看,no 模式和 everysec 模式的效率类似,当出现故障停机时,使用 no 模式的服务器将丢失上次同步 AOF 文件之后的所有写命令数据。
AOF重写
随着服务器写请求的增多,AOF 文件会越来越大。Redis 提供了一种将 AOF 重写的特性,能够去除 AOF 文件中的冗余写命令。原理是首先从数据库中读取键现在的值,然后用一条命令去记录键值对,代替之前记录这个键值对的多条命令。
通过该功能,Redis 服务器可以创建一个新的 AOF 文件,新旧两个 AOF 文件所保存的数据库状态相同,但新 AOF 文件不会包含任何浪费空间的冗余命令,所以新 AOF 文件的体积通常会比旧 AOF 文件的体积小很多。
因为 aof_rewrite 函数生成的新 AOF 文件只包含还原当前数据库状态所必须的命令,所以新 AOF 文件不会浪费任何硬盘空间。
AOF后台重写
上面介绍的 AOF 重写程序 aof_rewrite 函数可以很好的创建一个新 AOF 文件的任务,但是因为这个函数会进行大量的写入操作,所以调用这个函数的线程将被长时间阻塞,服务器将无法处理客户端发来的命令请求。所以 Redis 决定将 AOF 重写程序放到子进程里执行,这样做可以同时达到两个目的:
- 子进程进行 AOF 重写期间,服务器(父进程)可以继续处理命令请求。
- 子进程带有服务器进程的数据副本,使用子进程而不是线程,可以在避免使用锁的情况下,保证数据的安全性。
但是,使用子进程也有一个问题需要解决,因为子进程在进行 AOF 重写期间,服务器进程还需要继续处理命令请求,而新的命令可能会对现有的数据库状态进行修改,从而使得服务器当前的数据库状态和重写后的 AOF 文件所保存的数据库状态不一致。
为了解决数据不一致问题,Redis 服务器设置了一个 AOF 重写缓冲区,这个缓冲区在服务器创建子进程之后开始使用,当 Redis 服务器执行完一个写命令之后,它会同时将这个写命令发送给 AOF 缓冲区和 AOF 重写缓冲区,如图所示。
这样一来可以保证:
AOF 缓冲区的内容会定期被写入和同步到 AOF 文件,对现有 AOF 文件的处理工作会如常进行。
从创建子进程开始,服务器执行的所有写命令都会被记录到 AOF 重写缓冲区里面。
当子进程完成 AOF 重写工作之后,它会向父进程发送一个信号,父进程在接到该信号后,会调用一个信号处理函数,并执行一下工作:
将 AOF 重写缓冲区中的所有内容写入到新 AOF 文件中,这是新的 AOF 文件所保存的数据库状态将和服务器当前的数据库状态一致。
对新的 AOF 文件进行改名,原子地(atomic)覆盖现有的 AOF 文件,完成新旧两个 AOF 文件的替换。
这个信号处理函数执行完毕之后,父进程就可以继续像往常一样接受命令请求了。
在整个 AOF 后台重写过程中,只有信号处理函数执行时会对服务器进程(父进程)造成阻塞,在其他时候,AOF 后台重写不会阻塞父进程,这将 AOF 重写对服务器性能造成的影响降到了最低。
以上就是 AOF 后台重写,也即是 BGREWRITEAOF命令的实现原理。
缓存
缓存穿透
缓存穿透是指查询一个根本不存在的数据, 缓存层和存储层都不会命中, 通常出于容错的考虑, 如果从存储层查不到数据则不写入缓存层。
-
影响:
- 缓存穿透将导致不存在的数据每次请求都要到存储层去查询, 失去了缓存保护后端存储的意义。
- 缓存穿透问题可能会使后端存储负载加大,由于很多后端持久层不具备高并发性,甚至可能造成后端存储宕机。
- 通常可以在程序中统计总调用数、缓存层命中数、如果同一个Key的缓存命中率很低,可能就是出现了缓存穿透问题。
-
缓存穿透原因:
- 自身业务代码或数据出现了问题(例如:set 和 get 的key不一致)
- 一些恶意攻击或爬虫等造成大量不存在数据查询。(爬取线上商城商品数据,超大循环递增商品的ID)
-
解决方案
- 实时监控:
对redis进行实时监控,当发现redis中的命中率下降的时候进行原因的排查,配合运维人员对访问对象和访问数据进行分析查询,从而进行黑名单的设置限制服务(拒绝黑客攻击)
- 实时监控:
-
接口校验
类似于用户权限的拦截,对于id=-3872这些无效访问就直接拦截,不允许这些请求到达Redis、DB上。-
缓存空对象:
指在持久层没有命中的情况下,对key进行set (key,null),并设置一个过期时间
但是value为null 不代表不占用内存空间,空值做了缓存,意味着缓存层中存了更多的键,需要更多的内存空间,比较有效的方法是针对这类数据设置一个较短的过期时间,让其自动剔除。
缓存层和存储层的数据会有一段时间窗口的不一致,可能会对业务有一定影响。例如过期时间设置为5分钟,如果此时存储层添加了这个数据,那此段时间就会出现缓存层和存储层数据的不一致,此时可以利用消息系统或者其他方式清除掉缓存层中的空对象。
如果实在无法忍受不一致的情况,那么需要调整缓存更新策略,查询不到将key的value置空,修改时删除缓存,再次查询不到key写入缓存
-
布隆过滤器:bitmap
在访问缓存层和存储层之前,将存在的key用布隆过滤器提前保存起来,做第一层拦截,当收到一个对key请求时先用布隆过滤器验证是key否存在,如果存在在进入缓存层、存储层。可以使用bitmap做布隆过滤器。这种方法适用于数据命中不高、数据相对固定、实时性低的应用场景,代码维护较为复杂,但是缓存空间占用少。
布隆过滤器实际上是一个很长的二进制向量和一系列随机映射函数。布隆过滤器可以用于检索一个元素是否在一个集合中。
它的优点是空间效率和查询时间都远远超过一般的算法,缺点是有一定的误识别率和删除困难。
布隆过滤器拦截的算法描述:
初始状态时,BloomFilter是一个长度为m的位数组,每一位都置为0。
添加元素x时,x使用k个hash函数得到k个hash值,对m取余,对应的bit位设置为1。
判断y是否属于这个集合,对y使用k个哈希函数得到k个哈希值,对m取余,所有对应的位置都是1,则认为y属于该集合(哈希冲突,可能存在误判),否则就认为y不属于该集合。可以通过增加哈希函数和增加二进制位数组的长度来降低错报率。
-
对比
解决缓存穿透 适用场景 维护成本 缓存空对象 数据命中率不高,数据频繁变化实时性高 代码简单,需要过多内存,数据不一致 布隆过滤器 数据命中率不高,数据相对固定,实时性低 代码维护复杂,内存占用少
-
缓存失效(击穿)
缓存击穿问题也叫热点Key问题,针对某个访问非常频繁的热点数据并且缓存重建业务较复杂的key突然失效了,无数的请求访问会在瞬间给数据库带来巨大的冲击。这样的访问量可能就会导致数据库的崩盘(例如双十一等大量访问数据场景)
-
解决方案:
- 设置热点数据永远不过期(可以判断当前key快要过期时,通过后台异步线程在重新构建缓存)
- 定时任务主动刷新缓存设计
- 设置互斥锁。在并发的多个请求中,只有第一个请求线程能拿到锁并执行数据库查询操作,其他的线程拿不到锁就阻塞等着,等到第一个线程将数据写入缓存后,直接走缓存。
- 接口限流与熔断,降级。重要的接口一定要做好限流策略,防止用户恶意刷接口,同时要降级准备,当接口中的某些服务不可用时候,进行熔断,失败快速返回机制。
- 提前对热点数据进行设置
类似于新闻、某博等软件都需要对热点数据进行预先设置在redis中 - 监控数据,适时调整
监控哪些数据是热门数据,实时的调整key的过期时长
-
热点缓存key重建优化
开发人员使用“缓存+过期时间”的策略既可以加速数据读写, 又保证数据的定期更新, 这种模式基本能够满足绝大部分需求。 但是有两个问题如果同时出现, 可能就会对应用造成致命的危害:
(1). 当前key是一个热点key(例如一个热门的娱乐新闻),并发量非常大。
(2). 重建缓存不能在短时间完成, 可能是一个复杂计算, 例如复杂的SQL、 多次IO、 多个依赖等。
在缓存失效的瞬间, 有大量线程来重建缓存, 造成后端负载加大, 甚至可能会让应用崩溃。
- 解决方案
要解决这个问题主要就是要避免大量线程同时重建缓存。我们可以利用互斥锁来解决,此方法只允许一个线程重建缓存, 其他线程等待重建缓存的线程执行完, 重新从缓存获取数据即可。
- 解决方案
-
如何发现热 Key
-
凭借业务经验,预估热 Key 出现
根据业务系统上线的一些活动和功能,我们是可以在某些场景下提前预估热 Key 的出现的,比如业务需要进行一场商品秒杀活动,秒杀商品信息和数量一般都会缓存到 Redis 中,这种场景极有可能出现热 Key 问题的。优点:简单,凭经验发现热 Key,提早发现提早处理;
缺点:没有办法预测所有热 Key 出现,比如某些热点新闻事件,无法提前预测。
-
客户端进行收集
一般我们在连接 Redis 服务器时都要使用专门的 SDK(比如:Java 的客户端工具 Jedis、Redisson),我们可以对客户端工具进行封装,在发送请求前进行收集采集,同时定时把收集到的数据上报到统一的服务进行聚合计算。优点:方案简单
缺点:
对客户端代码有一定入侵,或者需要对 SDK 工具进行二次开发;
没法适应多语言架构,每一种语言的 SDK 都需要进行开发,后期开发维护成本较高。
-
在代理层进行收集
如果所有的 Redis 请求都经过 Proxy(代理)的话,可以考虑改动 Proxy 代码进行收集,思路与客户端基本类似。优点:对使用方完全透明,能够解决客户端 SDK 的语言异构和版本升级问题;
缺点:
开发成本会比客户端高些;
并不是所有的 Redis 集群架构中都有 Proxy 代理(使用这种方式必须要部署 Proxy)。
-
使用 Redis 自带命令
hotkeys 参数Redis 在 4.0.3 版本中添加了 hotkeys 查找特性,可以直接利用 redis-cli --hotkeys 获取当前 keyspace 的热点 key,实现上是通过 scan + object freq 完成的。
优点:无需进行二次开发,能够直接利用现成的工具;
缺点:
由于需要扫描整个 keyspace,实时性上比较差;
扫描时间与 key 的数量正相关,如果 key 的数量比较多,耗时可能会非常长。
monitor 命令
monitor 命令可以实时抓取出 Redis 服务器接收到的命令,通过 redis-cli monitor 抓取数据,同时结合一些现成的分析工具,比如 redis-faina,统计出热 Key。
优点:无需进行二次开发,能够直接利用现成的工具;
缺点:该命令在高并发的条件下,有内存增暴增的隐患,还会降低 Redis 的性能。
-
Redis 节点抓包分析
Redis 客户端使用 TCP 协议与服务端进行交互,通信协议采用的是 RESP 协议。自己写程序监听端口,按照 RESP 协议规则解析数据,进行分析。或者我们可以使用一些抓包工具,比如 tcpdump 工具,抓取一段时间内的流量进行解析。优点:对 SDK 或者 Proxy 代理层没有入侵;
缺点:
有一定的开发成本;
热 Key 节点的网络流量和系统负载已经比较高了,抓包可能会导致情况进一步恶化。
热 Key 问题解决方案
-
缓存雪崩
缓存雪崩是指在同一时段大量的缓存key同时失效或者Redis服务宕机,导致大量请求到达数据库,带来巨大压力。雪崩的意思有点类似于击穿,但是雪崩是多个key值同时失效导致大量数据全部访问数据库,导致数据库很难不崩盘。这对于数据库而言,就会产生周期性的压力波峰。
- 解决方案:
- 不设置redis缓存的生效时间,那么redis就不会失效(不好)
- 给不同的key的TTL添加随机值,使redis缓存的失效时间都不同,就不会发生大量请求同时被发送到数据库的情况
- 利用定时器不断刷新,每当缓存失效事件到后就重新设置生效时间
- 利用redis集群提高服务的可用性
- 给缓存业务太你家降级限流策略
- 给业务添加多级缓存
缓存更新策略
一般我们先查缓存,查不到再查数据库,然后写缓存
-
内存淘汰
默认redis有自己的内存淘汰策略,利用Redis的内存淘汰机制,当内存不足时自动淘汰部分数据。下次redis查询不到时,先从数据库查询然后更新缓存。但是由于可能会出现永远不会淘汰的key,那么每次查询都能查到数据,如果此时数据库发生变化,不会触发更新操作,因此redis中依旧是旧数据,导致数据不一致,因此一致性较差。
-
超时剔除
为了进一步保证一致性,我们应当给缓存设置超时时间,给缓存数据添加TTL时间,到期后自动删除缓存,下次redis查询不到时,先从数据库查询然后更新缓存。但是由于超时时间可长可短,那么在过期前数据发生变化不会触发redis删除或更新旧数据,因此超时仍有很大可能导致不一致
-
主动更新
编写业务逻辑,在修改数据库的同时,更新缓存。由于变化立即更新,一致性较好。
-
删除缓存还是更新缓存?
每次数据库变化即修改缓存,那么如果用户查询频次较低,那么无效操作太多,为了减少无效的更新,我们可以在变化时删除缓存,再变化时不动缓存,等下次查询不到再写缓存。
-
先操作数据库还是先删除缓存?
考虑到线程安全问题,如果先删除缓存,再操作数据库,由于数据库操作较慢,可能会出现数据库更新的同时,被别的线程抢先完成数据更新和缓存的删除,则导致数据不一致,因此我们应当先保证执行较慢数据库操作的成功再删除缓存,防止别的线程趁虚而入。
-
如果保证缓存和数据库操作的同时成功或失败(原子性)?
单体系统,将缓存与数据库操作放在一个事务。分布式系统,利用TCC等分布式事务方案
-
综上:
- 低一致性需求,内存淘汰和超时剔除即可满足
- 高一致性需求,我们应当在内存淘汰和超时剔除的基础上主动更新保证数据一致性。查询不到时写缓存,变化时主动删除缓存,再变化时不动缓存,等下次查询不到再写缓存。