一次性解释清楚redis缓存三大问题

通常面试或者学习redis涉及到的redis缓存三大问题为缓存击穿、缓存穿透以及缓存雪崩。

下面就逐一讲解什么是缓存击、穿透以及雪崩,附上它们常见的解决方法。

缓存击穿

什么是缓存击穿?

通俗来讲就是部分key过期失效而导致的严重后果。缓存击穿问题也叫热点Key问题,就是⼀个被高并发访问并且缓存重建业务较复杂的key突然失效了,无数的请求访问会在瞬间给数据库带来巨大的冲击。

解决方案:

1.互斥锁,保证强一致性,但是性能差

简单的来说:

并不是所有的线程都有 “ 资格 ” 去访问数据库,只有持有锁的线程才可以对其进行操作。

不过该操作有一个很明显的问题,就是会出现相互等待的情况

2.逻辑过期,高可用性能优,但不能保证数据绝对一致


不设置TTL
        之前所说导致缓存击穿的原因就是该key的TTL到期了,所以我们在这就不设置TTL了,
而是使用一个字段,例如:expire表示过期时间(逻辑上的)。当我们想让它 “ 过期 ” 的时候,
我们可以直接手动将其删除(热点key,即只是在一段时间内,其被访问的频次很高)。
 
这种方案巧妙在于,异步的构建缓存,缺点在于在构建完缓存之前,返回的都是脏数据。

左边就是利用互斥锁来解决,当线程1查询缓存时,发现不存在该key,此时该线程会尝试获取互斥锁,如果获取成功,该线程就会查询数据库并且重建缓存数据,将数据写入缓存,释放锁;如果获取锁失败,意味着此时已经有线程在重建缓存,那么该线程需要不断等待重试,直到线程1重建缓存完成,成功获取缓存的数据。该方法会导致其余线程一直处于阻塞状态,占用大量线程,对cpu的消耗较高,性能低。

右边是利用逻辑过期的策略来解决,会提前给高并发访问量大的key设置一个逻辑过期时间,当有线程来访问数据时,先判断逻辑时间是否过期,如果过期了,该线程就会获取一个互斥锁,并开启一个新的线程,该线程负责查询数据库并进行缓存重建,同时重置逻辑时间,完成后释放锁,在此期间,所有对该key的请求访问的都是没有更新过的数据(包括线程1)。该方法的好处是,缓存中的热点key并不会过期,会一直存在,只是逻辑上设置了一个过期时间,即所有的请求不会未命中,线程不需要阻塞等待,性能比较高,缺点是一旦逻辑时间过期后,所有的请求访问的都是过期数据。因此该方法适用于那些对于数据实时性要求不高的key。

缓存穿透

简单来讲,就是查询一个不存在的数据,mysql查询不到数据库也不会写入缓存,就会导致每次请求都进入数据库,一旦有人通过这些请求恶意攻击,就会导致数据库瘫痪。

解决方案:

1.缓存空对象

当请求访问到一个不存在的数据时,数据库会创建一个null值的对象缓存到缓存中,当下一次有请求访问该key时,就返回空对象。

缺点是可能会导致数据有一段时间的不一致,假设某次访问是对一个商品的数据请求,但是目前该商品并不存在,缓存和数据库都不存在,那么此时,就会返回一个空对象,并设置一个有效时间TTL,但是,某一时刻商家上架了该商品,数据库有该商品的信息,但是缓存中仍然是空对象,此时,新的请求访问的仍然是null值,直到该null值的有效时间过期,再次请求时数据库才会在缓存中缓存新的数据。

优点时该实现比较简单,不需要过多的编码和逻辑。

2.布隆过滤器

在客户端与Redis之间加了一个布隆过滤器,对请求进行过滤。 
 
     布隆过滤器的大致原理:布隆过滤器中存放二进制位。
     数据库的数据通过hash算法计算其hash值并存放到布隆过滤器中,
     之后判断数据是否存在的时候,就是判断该hash值是0还是1。
 
     但是这是一种概率上的统计,当其判断不存在的时候就一定是不存在;
     当其判断存在的时候就不一定存在。所以有一定的穿透风险

综上所述,可以两种方法结合一起用,这样比较保险。

缓存穿透的解决方案:

(1)缓存 null 值
(2)布隆过滤
(3)增强 id 的复杂度,避免被猜测 id 规律
(4)做好数据的基础格式校验
(5)加强用户权限校验
(6)做好热点参数的限流

缓存雪崩

缓存雪崩是指在同一时段大量的缓存key同时失效或者redis服务器直接宕机,导致大量请求打到数据库,带来巨大压力。

解决方案

一、给不同的key的TTL添加随机值

  1. 操作简单,当我们在做缓存预热的时候,就有可能在同一时间批量插入大量的数据,

  2. 那么如果它们的TTL都一样的话就可能出现大量key同时过期的情况!!!

  3. 所以我们需要在设置过期时间TTL的时候,定义一个范围,追加该范围内的一个随机数。

二、利用redis集群提高服务的可用性

三、给缓存业务添加降级限流策略 (nginx或spring cloud gateway)

四、给业务添加多级缓存(Guava或Caffeine)

以上就是redis面试的最经典的三大缓存问题,解释以及相关的解决方案。

<think>我们面对的问题:如何比对数据库和Redis中的数据并清理Redis中的冗余数据? 冗余数据指的是:Redis中存储了数据库里已经不存在的数据,或者数据库中的数据已经更新而Redis中还是旧数据。 步骤: 1. 确定比对范围:我们需要知道哪些数据需要比对。通常,我们会根据业务逻辑确定一组关键数据(例如用户信息、商品信息等)。 2. 设计比对策略: a. 全量比对:适用于数据量不大的情况,将数据库中的所有关键数据的主键(或唯一标识)与Redis中的对应键进行比对。 b. 增量比对:通过数据库的日志(如binlog)或更新时间戳,只比对发生变化的数据。 3. 清理策略: a. 对于数据库中存在而Redis中不存在:无需处理(或者根据业务需求,可能需要将数据库中的数据加载到Redis,但这里我们只清理冗余,所以不考虑)。 b. 对于Redis中存在而数据库中不存在:删除Redis中的键。 c. 对于两者都存在但数据不一致:根据业务需求,可以选择用数据库的数据覆盖Redis,或者仅记录日志由人工处理(因为不一致可能是由于缓存过期策略导致,不一定需要立即覆盖)。 但是,直接全量比对在大数据量下会有性能问题,因此我们需要更高效的方法。 高效方法: 1. 使用Redis的SCAN命令遍历所有键(或特定模式的键),然后逐一检查数据库是否存在对应的记录。但这样会对数据库造成很大压力(每个键都要查询一次数据库)。 2. 反向操作:从数据库取出所有主键(或唯一标识),然后检查Redis中是否存在这些键。对于不在数据库主键列表中的Redis键,删除。但同样,如果数据库数据量很大,取出所有主键可能有压力,而且需要足够的内存来存储这些主键。 优化思路: 1. 使用增量比对:记录每次数据同步的时间点,只比对上次同步后发生变化的数据。 2. 使用消息队列:当数据库发生变更(增删改)时,发送消息通知,然后更新或删除Redis中对应的键。这样就不需要定期比对了,但需要保证消息不丢失。 然而,如果当前没有这样的消息机制,需要定期清理冗余数据,我们可以采用以下折中方案: 方案(折中): 1. 使用Redis的SCAN命令分批获取键,每次获取一批(例如1000个)。 2. 对于这一批键,我们从数据库中批量查询对应的记录是否存在(例如使用主键的IN查询)。 3. 将数据库中不存在的键收集起来,然后使用UNLINK命令(非阻塞删除)批量删除。 但是,这个方案只能清理那些在数据库中完全不存在的键,对于数据不一致的情况(数据库有但数据不一致)无法处理。因此,我们还需要考虑数据不一致的情况。 对于数据不一致,我们可以: 1. 在比对时,同时检查数据内容是否一致(需要根据业务规则比较)。但是这样需要从数据库和Redis中取出所有数据,网络和计算开销很大。 2. 另一种思路是,我们只清理那些数据库已经不存在的键,而数据不一致的问题通过设置合理的缓存过期时间(或使用缓存更新策略)来解决。 因此,根据问题,我们只清理冗余(即数据库已经不存在的键),而不处理不一致(因为不一致可能不是冗余,而是缓存未更新,这可以通过缓存更新策略解决)。 具体步骤: 1. 使用SCAN命令遍历Redis中所有需要比对的键(比如以某个前缀开头的键)。 2. 分批获取键(例如每次1000个)。 3. 将这些键转换成数据库中的主键(可能需要去掉前缀等操作,具体看键的命名规则)。 4. 在数据库中批量查询这些主键是否存在(使用IN语句)。 5. 对于数据库中不存在的主键,收集对应的Redis键。 6. 使用UNLINK命令批量删除这些键(或者逐条删除,但建议批量删除以减少网络开销,可以使用管道或批量UNLINK,但注意UNLINK本身可以一次删除多个键:`UNLINK key1 key2 ...`)。 注意:为了避免对数据库造成过大压力,可以在每次查询之间加入适当的延迟。 伪代码(Python示例): 假设我们使用redis-py和数据库连接(如pymysql或psycopg2) ```python import redis import pymysql # 连接Redis r = redis.Redis(host='localhost', port=6379, db=0) # 连接数据库 db_conn = pymysql.connect(host='localhost', user='user', password='pass', database='db') db_cursor = db_conn.cursor() # 设置每次SCAN的数量 batch_size = 1000 # 初始化游标 cursor = 0 # 用于存储要删除的键 keys_to_delete = [] # 使用SCAN迭代 while True: # 使用SCAN命令获取一批键 cursor, keys = r.scan(cursor, match='prefix:*', count=batch_size) # 如果没有键,则结束 if not keys: break # 从键中提取主键(假设键的格式为'prefix:primary_key') primary_keys = [key.decode('utf-8').split(':')[1] for key in keys] # 构建数据库查询,检查这些主键是否存在 # 注意:这里需要防止SQL注入,使用参数化查询 format_strings = ','.join(['%s'] * len(primary_keys)) query = f"SELECT id FROM table_name WHERE id IN ({format_strings})" db_cursor.execute(query, primary_keys) # 获取存在的id existing_ids = [row[0] for row in db_cursor.fetchall()] # 假设id是第一个字段 # 将存在的id转换成字符串(如果数据库返回的是整数,而primary_keys是字符串,需要转换) existing_ids_str = set(str(id) for id in existing_ids) # 对比:如果Redis键对应的主键不在数据库中,则标记为删除 for key in keys: # 提取出这个键对应的主键 pk = key.decode('utf-8').split(':')[1] if pk not in existing_ids_str: keys_to_delete.append(key) # 如果当前批次中要删除的键达到一定数量,或者已经是最后一批,则执行删除 if len(keys_to_delete) >= batch_size: # 使用UNLINK删除(非阻塞) r.unlink(*keys_to_delete) # 注意:unlink可以接受多个键 keys_to_delete = [] # 如果游标为0,表示迭代结束 if cursor == 0: break # 删除最后一批 if keys_to_delete: r.unlink(*keys_to_delete) # 关闭连接 db_cursor.close() db_conn.close() ``` 注意: 1. 这个脚本应该在业务低峰期运行,因为SCAN和数据库的批量查询都会对服务造成压力。 2. 键的解析规则需要根据实际情况调整。 3. 数据库查询的构建需要注意防止SQL注入,这里使用了参数化查询,但是通过字符串格式化构建了IN语句,需要确保主键列表不是空的(如果为空,则跳过)。 4. 如果主键是字符串类型,则不需要转换成整数,直接使用字符串比较即可。 另外,对于数据不一致的情况,我们这里没有处理。如果需要处理,可以在比对存在性后,再对存在的键进行数据一致性检查(比较数据库和Redis中的数据),但这会大大增加复杂度和执行时间。 因此,建议只清理冗余(即数据库不存在的键),而数据不一致的问题通过以下方式避免: 1. 在更新数据库的同时更新Redis(使用事务或最终一致性)。 2. 设置合理的缓存过期时间,让不一致的数据自动过期。 引用说明: 根据引用[2],我们可以使用UNLINK命令来安全地删除大Key(非阻塞)。在清理过程中,如果遇到大Key,使用UNLINK可以避免阻塞Redis。 根据引用[1],Redis会在内存超过阈值时进行淘汰,但我们的清理操作是主动的,可以避免内存浪费。 相关问题: 1. 如何减少Redis和数据库数据不一致的情况? 2. 除了定期清理,还有哪些方法可以保证Redis中数据的有效性? 3. 在清理Redis冗余数据时,如何避免对线上服务造成影响?</think>### Redis与数据库数据一致性比对与冗余清理方案 要高效比对数据库和Redis数据并清理冗余,需结合主动同步机制和定期维护策略,以下是完整方案: #### 一、核心比对策略 1. **键空间映射法** 建立数据库主键与Redis键的映射关系: ```python # Python伪代码:生成映射关系 db_keys = db.execute("SELECT id FROM main_table") # 数据库主键集合 redis_keys = r.keys("cache_prefix:*") # Redis键集合 redundant_keys = set(redis_keys) - set(f"cache_prefix:{k}" for k in db_keys) ``` 2. **版本号比对法**(推荐) - 数据库表中添加版本号字段 `version INT DEFAULT 0` - Redis存储带版本号的数据: ```bash HSET user:123 data "{...}" version 15 ``` - 比对逻辑: ```sql -- 数据库查询 SELECT id, version FROM users WHERE id IN (123,456); ``` ```python # Redis比对 pipe = r.pipeline() for id in [123, 456]: pipe.hget(f"user:{id}", "version") redis_versions = pipe.execute() ``` #### 二、数据清理实施 1. **主动清理(推荐方案)** 在数据库操作时同步清理Redis: ```python def update_user(db_id, data): # 1. 更新数据库 db.execute("UPDATE users SET data=?, version=version+1 WHERE id=?", data, db_id) # 2. 立即删除Redis缓存 r.unlink(f"user:{db_id}") # 非阻塞删除[^2] # 3. 可选异步重建缓存 asyncio.run(rebuild_cache(db_id)) ``` 2. **定期扫描清理** 低峰期执行扫描脚本: ```python cursor = 0 batch_size = 500 while True: # SCAN遍历Redis键[^1] cursor, keys = r.scan(cursor, "user:*", count=batch_size) # 批量检查数据库存在性 db_ids = extract_ids(keys) # 从keys提取ID exist_ids = db.query("SELECT id FROM users WHERE id IN %s", db_ids) # 删除冗余键 redundant = set(db_ids) - set(exist_ids) r.unlink(*[f"user:{id}" for id in redundant]) # 批量删除 if cursor == 0: break ``` #### 三、关键优化措施 1. **大Key处理** 使用`UNLINK`替代`DEL`避免阻塞: ```bash UNLINK big_user_key # 异步删除[^2] ``` 2. **增量比对** 通过数据库binlog监听变更: ```python # 监听MySQL binlog示例 for binlog_event in binlog_listener: if event.type == "delete": r.unlink(f"user:{event.id}") ``` 3. **一致性保障** - 为所有缓存键设置TTL:`EXPIRE key 86400` - 使用Redis事务保证原子性: ```python with r.pipeline(transaction=True) as pipe: pipe.multi() pipe.delete("key1") pipe.hset("key2", "field", "value") pipe.execute() ``` #### 四、操作注意事项 1. **规避性能风险** - 避免使用`KEYS *`命令,用`SCAN`替代[^1] - 批量操作控制为500键/次 - 在从节点执行扫描操作 2. **数据安全** ```bash # 先备份再删除 redis-cli --scan --pattern "user:*" | xargs redis-cli dump > backup.rdb ``` 3. **监控指标** - Redis内存变化:`INFO memory` - 键空间统计:`SCARD redundant_keys_set` - 删除延迟:`LATENCY HISTORY UNLINK` > **执行建议**:生产环境优先采用"版本号比对+主动删除"方案,每周结合定期扫描进行兜底检查。对于超大规模数据(>1亿键),建议使用RedisGears等分布式处理工具。 --- ### 相关问题 1. **如何保证Redis和数据库的实时一致性?** 2. **Redis集群环境下数据清理有哪些特殊注意事项?** 3. **除了手动清理,Redis自身有哪些淘汰机制可防止冗余数据堆积?**[^1] 4. **如何监控Redis与数据库的数据差异?** [^1]: Redis 会在 3种场景下对 key 进行淘汰,查询时发现超过阈值懒惰删除:是在执行命令时,检查淘汰 key。定期删除:在定期执行 serverCron 时,检查淘汰 key [^2]: 对Redis中的大Key进行清理,从Redis中删除此类数据。Redis自4.0起提供了UNLINK命令,该命令能够以非阻塞的方式缓慢逐步的清理传入的Key
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值