1.1 常见考点
1、Redis 为何这么快?
Redis 是一款基于内存的数据结构存储系统,它之所以能够提供非常快的读写性能,主要是因为以下几个方面的原因:
- 基于内存存储:Redis 所有的数据都存储在内存中,而内存的访问速度比磁盘要快得多。因此,Redis 可以提供非常快的读写性能,特别是在处理大量数据时。
- 数据结构优化:Redis 提供了多种数据结构,如字符串、列表、哈希、集合、有序集合等,并且对这些数据结构进行了优化,使得它们能够在内存中高效地存储和操作。
- 单线程模型:Redis 采用单线程模型,即所有的操作都在一个线程中进行。这种模型避免了锁的竞争和多线程上下文切换的开销,从而提高了性能。
- 事件驱动模型:Redis 采用事件驱动模型,即当有客户端请求时,Redis 会将请求放入事件队列中,然后通过单线程依次处理这些请求。这种模型可以充分利用多核 CPU 的优势,提高并发处理能力。
- 高效的内存管理:Redis 采用了自己的内存管理机制,可以根据数据的使用情况动态地调整内存分配,从而避免了内存碎片和内存泄漏的问题。
3、缓存三大问题以及解决方案?
缓存的三大问题通常指的是缓存穿透、缓存击穿和缓存雪崩。每个问题都有其特定的场景和解决方案:
-
缓存穿透:
- 问题:查询不存在的数据,因为缓存不会保存这样的结果,每次查询都要去数据库查找,导致数据库压力增大。
- 解决方案:使用布隆过滤器预判断数据是否存在;或者即使数据不存在也将查询结果(比如一个空对象)缓存起来。
- 例子:一个电商平台上,有些商品已经下架或不存在。用户查询这些商品时,因为它们不存在,所以每次查询都会直接访问数据库。为了解决这个问题,可以在缓存层前使用布隆过滤器,将所有有效商品ID加入布隆过滤器。当查询请求来时,先检查布隆过滤器,如果过滤器显示不存在,则直接返回不存在,不再查询数据库。
-
缓存击穿:
- 问题:热点数据过期的瞬间,大量请求击中数据库。
- 解决方案:设置热点数据永不过期;或者使用互斥锁,当缓存失效时,不是所有请求都去数据库加载,而是只让一个请求去数据库加载并回填到缓存。
- 例子:在一个视频网站中,某热门剧集的最新一集发布时,大量用户几乎在同一时间请求这集的内容。当缓存的这集内容过期时,所有用户的请求都会直接访问数据库。这时可以使用互斥锁,即当第一个请求发现缓存中没有数据时,它会去数据库中加载数据,并且在这个过程中,其他请求会等待,直到第一个请求将数据加载到缓存中。
-
缓存雪崩:
- 问题:大量缓存同时过期,导致所有请求都落到数据库上,可能引起数据库宕机。
- 解决方案:缓存数据的过期时间加上一个随机值,避免同时过期;或者使用分布式缓存和限流策略。
- 例子:在一个新闻网站,可能有成千上万条新闻内容被缓存,而这些缓存可能设置了相同的过期时间。如果在某一时刻,大量缓存同时过期,突然有大量的请求打到数据库上。为了防止这种情况,可以在设置缓存过期时间时加上一个随机值,让每条缓存的过期时间都有所不同,分散请求压力。
分布式缓存和限流策略
使用分布式缓存和限流策略是解决缓存雪崩问题的一种方法。下面是一个实际的例子来说明这个策略如何应用:
分布式缓存的例子:
假设有一个社交媒体平台,用户的个人信息经常被查询,因此被缓存在Redis这样的分布式缓存系统中。在正常情况下,用户信息的读取请求会被均匀地分散到分布式缓存的多个节点上,这样即使某个节点的缓存过期,也只是一小部分请求会转到数据库,不会造成太大压力。
然而,如果缓存层面出现问题,比如一个节点宕机,或者多个节点上缓存的用户信息同时过期,就可能导致缓存雪崩。为了防止这种情况,平台可能采取在缓存层面增加更多的节点,使得每个节点上缓存的数据量更少,分散风险。此外,为每个用户信息的缓存设置不同的过期时间,可以防止很多缓存同时过期。
限流策略的例子:
在同一个社交媒体平台上,平台也可能会面临突然的流量暴增,比如名人发布了新的动态,导致大量用户涌入。为了防止在这种情况下的缓存雪崩,平台可以实施限流策略。
限流策略可以通过各种算法实现,比如漏桶算法或令牌桶算法。例如,平台可以为每个用户的信息访问设置一个限流器,这个限流器每秒只允许一定数量的请求通过。如果请求超出了限制,那么这些请求会被排队或直接拒绝,从而保护后端数据库不会被过多的请求同时击中。
综合分布式缓存和限流策略,社交媒体平台能有效地防止由于缓存问题导致的数据库压力过大,从而确保平台的稳定运行和良好的用户体验。
4、先删后写还是先写后删?
在 Redis 中,先删后写和先写后删都有其适用的场景,具体选择哪种方式需要根据具体的业务需求和数据特点来决定。
先删后写是指在写入数据之前先删除旧数据。这种方式适用于需要更新数据的场景,例如计数器、排行榜等。先删除旧数据可以确保新数据的写入不会受到旧数据的影响,从而保证数据的准确性和一致性。
先写后删是指在写入数据之后再删除旧数据。这种方式适用于需要保留历史数据的场景,例如日志记录、数据备份等。先写入新数据可以确保数据不会丢失,然后再删除旧数据可以释放存储空间,从而节省存储成本。
需要定期对数据进行备份和清理,以确保数据的安全性和可用性。
5、如何保证 Redis 的高并发?
保证Redis高并发主要涉及优化Redis配置、硬件资源、数据结构和访问模式等多个方面。以下是一些保证Redis高并发的策略,以及结合实际项目的例子进行说明:
-
合理配置Redis:
- 使用足够的内存和合理的内存淘汰策略来存储数据。
- 开启持久化功能,比如RDB或AOF,以防止数据丢失。
- 调整配置参数,如增大
maxclients
来允许更多的并发连接,合理设置timeout
来避免不必要的长连接。
例子:假设一个在线零售商店使用Redis来存储用户的购物车信息。为了应对高并发,商店确保Redis有足够的内存来存储所有活跃用户的购物车数据,并且配置了AOF持久化来保证在系统重启后数据不会丢失。同时,商店也增加了
maxclients
的值以支持更多用户同时在线操作购物车。
Redis 的 AOF(Append Only File)持久化是一种用于确保数据安全的技术,它通过记录每个写操作来实现。这些操作被追加到一个日志文件中,以便在 Redis 重启时重放这些操作,从而重建原始数据集。因为 AOF 需要将每个写命令写入磁盘,所以它可能会对性能产生一定影响,特别是在高写入负载的情况下。
-
优化数据结构:
- 使用合适的数据结构,如利用哈希表存储对象而非字符串,可以减少内存使用。
- 使用列表、集合和有序集合来处理复杂的数据类型。
例子:在一个社交网络项目中,为了跟踪用户的好友列表和共同好友等,选择使用集合(Set)数据结构而不是简单的字符串列表。集合的交集和并集操作使得查询共同好友变得非常高效,从而在用户量大幅增加时,Redis依然能保持快速响应。
-
读写分离:
- 使用Redis的主从复制功能,将读操作分散到多个从节点,写操作仍然在主节点。
例子:一个游戏排行榜系统可能会遇到大量的读请求,如查看排行榜,但写请求较少,比如更新用户分数。通过设置多个从节点来处理读请求,主节点只处理写请求,可以显著提高并发处理能力。
-
使用缓存集群:
- 利用Redis集群来分散负载和提供更高的可用性。
例子:一个高流量新闻网站使用Redis集群来存储热点新闻内容,不同的新闻内容被均匀地分配在不同的节点上。即使在高峰时段,用户访问新闻内容的请求也能被快速响应,因为负载是分散的。
-
限流:
- 防止某些操作或用户占用过多资源,实施限流策略。
例子:在线商城在秒杀活动中,为了防止某一时间点的超高并发请求导致服务不可用,可以对每个用户的请求频率进行限制,确保Redis能稳定响应每个请求。
-
避免大Key和大事务:
- 避免存储大Key,因为读取或删除大Key会阻塞Redis。
- 避免在Redis中运行大事务,大事务会占用过多计算资源。
例子:
当我们谈到“大Key”和“大事务”在Redis中可能造成的问题时,我们指的是两个可能影响Redis性能的场景: -
大Key:在Redis中,一个“大Key”通常指的是一个包含大量元素的数据类型实例,比如一个非常大的列表、集合、有序集合或哈希。因为Redis是单线程的,当对一个大Key进行操作时(如删除、查询或修改),它需要消耗更多的CPU时间去处理这个操作,这个过程中无法处理其他请求,从而影响整体的响应时间。
具体例子:假设有一个社交应用,它在一个列表Key中存储了用户的所有动态ID。如果一个非常活跃的用户有成千上万条动态,那么这个列表就会变得非常大。当尝试获取这个用户的所有动态或者删除这个用户的动态列表时,这个大列表会导致Redis处理请求的时间明显变长。为了避免这种情况,可以将用户的动态分散存储在多个列表中,每个列表存储一定数量的动态ID,这样任何单一操作不会阻塞Redis太长时间。
-
大事务:Redis的事务是通过
MULTI
、EXEC
命令组实现的,它可以将多个命令打包然后顺序执行。一个“大事务”包含了大量的命令。在事务执行期间,Redis会将所有命令都加载到内存中,然后顺序执行,这期间不会处理其他任何请求。如果事务中的命令非常多,它会占用大量的CPU时间和内存资源,影响Redis的处理能力。具体例子:在一个电子商务网站中,管理员可能需要更新大量商品的价格。如果管理员将所有的价格更新操作放在一个事务中,比如一次性更新几千个商品,那么在这个事务执行的时候,其他的读写请求就会被延迟处理,导致用户体验下降。一个更好的做法是将这些更新操作分批执行,每个事务只更新少量商品,或者使用Redis的管道化(pipelining)来提高效率。
综上,避免大Key和大事务可以帮助维持Redis的响应性能,特别是在高并发的场景下。通过合理设计数据结构和优化操作命令,可以减少单个操作对系统的影响,提升整体性能。
管道化
例子:
假设你在开发一个在线游戏,游戏服务器需要更新玩家的分数和排名信息。每个玩家完成一局游戏后,服务器都需要执行以下操作:
- 更新玩家的新分数。
- 更新玩家的排名。
- 记录玩家的游戏历史。
如果不使用管道化,你的代码可能会像这样:
redis_connection.set('player:1234:score', '1500')
redis_connection.zadd('leaderboard', {
'player:1234': 1500})
redis_connection.lpush('player:1234:games', 'game_id_5678')
每一行代码都会生成一个网络请求,如果玩家非常多,这些请求的网络延迟累加起来将会非常显著。
而使用管道化,你可以这样做:
pipeline = redis_connection.pipeline()
pipeline.set('player:1234:score', '1500')
pipeline.zadd('leaderboard', {
'player:1234': 1500})
pipeline.lpush<