尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

Redis缓存设计与常见问题实战:穿透、击穿、雪崩与解决方案

Redis缓存设计与常见问题实战:穿透、击穿、雪崩与解决方案 Redis是目前最流行的内存数据库广泛应用于缓存、会话存储、排行榜、分布式锁等场景。在缓存架构中缓存穿透、缓存击穿、缓存雪崩是三大经典问题如果处理不当可能导致数据库被瞬间打挂。本文系统讲解这三大问题的产生原因和解决方案同时覆盖缓存更新策略、分布式锁实现和Redis集群的关键知识点。一、缓存穿透缓存穿透是指查询一个数据库中根本不存在的数据由于缓存中也没有每次请求都会穿透到数据库。如果被恶意攻击者利用用大量不存在的key发起请求数据库将承受巨大压力。解决方案有两种缓存空值和布隆过滤器。缓存空值是在查询不到数据时将空结果也缓存起来设置较短的过期时间后续相同请求直接命中缓存。布隆过滤器则是在查询缓存前先通过布隆过滤器判断key是否可能存在如果过滤器判断不存在则直接返回不查询数据库。# 缓存空值方案Python伪代码def get_user(user_id):cache_key fuser:{user_id}user redis.get(cache_key)if user is not None:return None if user else json.loads(user)# 缓存未命中查数据库user db.query_user(user_id)if user is None:# 缓存空值5分钟过期redis.setex(cache_key, 300, )else:redis.setex(cache_key, 3600, json.dumps(user))return user布隆过滤器的优点是空间效率极高10亿数据只需约1.2GB内存缺点是存在误判率可能将不存在的数据判断为存在且不支持删除。Redis 4.0以上版本可以通过布隆过滤器插件RedisBloom直接使用。实际项目中对于参数可枚举的场景如用户ID、商品ID缓存空值更简单可靠对于海量不可枚举key的场景布隆过滤器更合适。二、缓存击穿缓存击穿是指某个热点key在缓存过期的瞬间大量并发请求同时到达由于缓存已失效所有请求都穿透到数据库导致数据库压力骤增。与缓存穿透不同击穿查询的是确实存在的数据只是缓存刚好过期。解决方案有三种互斥锁、逻辑过期、热点key永不过期。# 互斥锁方案def get_hot_data(key):data redis.get(key)if data:return json.loads(data)# 缓存未命中尝试获取互斥锁lock_key flock:{key}if redis.set(lock_key, 1, nxTrue, ex10):try:# 拿到锁查数据库并回写缓存data db.query(key)redis.setex(key, 3600, json.dumps(data))return datafinally:redis.delete(lock_key)else:# 没拿到锁等待后重试import timetime.sleep(0.1)return get_hot_data(key)互斥锁方案的核心是只让一个请求去查数据库其他请求等待重试。逻辑过期方案则是在缓存值中存储逻辑过期时间缓存本身不设置物理过期当发现逻辑过期时异步线程去更新缓存当前请求返回旧数据。这种方案不会阻塞请求但存在短暂的数据不一致。热点key永不过期是最简单的方案适用于数据变化不频繁的场景配合消息队列或定时任务主动更新缓存。三、缓存雪崩缓存雪崩是指大量缓存key在同一时间集中过期或者Redis实例宕机导致大量请求同时穿透到数据库数据库承受巨大压力甚至崩溃。解决方案包括过期时间加随机值、多级缓存、Redis高可用集群、服务熔断降级。过期时间加随机值是最简单有效的方法在基础过期时间上加上一个随机偏移如0到300秒避免大量key同时过期。问题产生原因核心解决方案适用场景缓存穿透查询不存在的数据缓存空值 / 布隆过滤器恶意攻击、参数不可控缓存击穿热点key过期瞬间高并发互斥锁 / 逻辑过期 / 永不过期秒杀、热点商品缓存雪崩大量key同时过期 / Redis宕机随机过期时间 / 多级缓存 / 集群高可用全量缓存、单点Redis四、缓存更新策略缓存与数据库的一致性是缓存设计中的核心难题。常见的更新策略有四种Cache Aside旁路缓存、Read Through、Write Through、Write Behind。Cache Aside是最常用的模式读时先查缓存未命中则查数据库并写入缓存写时先更新数据库再删除缓存。为什么是删除缓存而不是更新缓存因为并发场景下更新缓存可能导致数据覆盖删除缓存更安全下次读取时会自动加载最新数据。# Cache Aside 写操作def update_user(user_id, new_data):# 1. 先更新数据库db.update_user(user_id, new_data)# 2. 再删除缓存注意是删除不是更新redis.delete(fuser:{user_id})# 延迟双删极端并发场景下进一步降低不一致概率def update_user_double_delete(user_id, new_data):redis.delete(fuser:{user_id})db.update_user(user_id, new_data)import timetime.sleep(0.5) # 延迟时间需大于一次读操作的耗时redis.delete(fuser:{user_id})需要注意的是先更新数据库再删除缓存仍然存在极小概率的不一致窗口如果在删除缓存之前有一个读请求刚好命中了旧缓存。但这种情况概率极低因为数据库更新通常比缓存读取慢。对于强一致性要求的场景可以使用分布式锁或基于binlog的异步更新如Canal监听MySQL binlog异步删除或更新缓存。五、Redis分布式锁分布式锁是Redis的重要应用场景。实现分布式锁的核心命令是SET key value NX EX即key不存在时才设置同时设置过期时间。value应设置为唯一标识如UUID释放锁时通过Lua脚本保证“判断是否自己的锁”和“删除锁”的原子性。Redis分布式锁存在的问题包括锁过期时间难以确定、主从切换可能导致锁丢失。Redisson框架提供了更完善的分布式锁实现支持看门狗自动续期、可重入、公平锁等特性。# 基于Lua脚本的安全释放锁UNLOCK_SCRIPT if redis.call(get, KEYS[1]) ARGV[1] thenreturn redis.call(del, KEYS[1])elsereturn 0enddef acquire_lock(lock_key, timeout10):token str(uuid.uuid4())if redis.set(lock_key, token, nxTrue, extimeout):return tokenreturn Nonedef release_lock(lock_key, token):redis.eval(UNLOCK_SCRIPT, 1, lock_key, token)六、Redis集群与高可用生产环境中Redis不能单点部署。Redis Sentinel哨兵模式提供主从切换和故障自动转移适合读多写少、数据量不大的场景。Redis Cluster是官方的分布式集群方案采用16384个哈希槽分片支持水平扩展适合大数据量和高并发场景。Cluster模式下多key操作如事务、pipeline受限于key必须在同一个槽位可以通过hash tag如{user}:1强制相关key分布到同一节点。结语Redis缓存设计的核心是在性能和一致性之间找到平衡。穿透、击穿、雪崩三大问题各有针对性的解决方案实际项目中往往需要组合使用。缓存更新策略推荐Cache Aside模式分布式锁推荐使用Redisson等成熟框架。建议在项目初期就规划好缓存架构包括key命名规范、过期时间策略、监控告警避免线上出问题后再临时补救。
返回列表