Redis使用细节(持续更新中)
Redis使用细节
分布式锁
因为Redis是单线程的,所以可以用setnx来模拟锁的获取释放从而实现分布式锁
在用setnx实现分布式锁时,会出现一些问题
- 业务超时解锁,导致并发问题。业务执行时间超过了锁超时的时间
- redis主从切换临界点问题,主从切换后,A持有的锁还没有同步到新的主节点,B在新的主节点获取到了锁
- redis集群脑裂,导致出现多个主节点
大key和热key
大key定义
-
String类型:value的字节数大于10KB即为大Key
-
Hash/Set等复杂结构类型:元素个数大于5000个或总value字节数大于10M即为大key
大key危害
- 读取成本高
- 容易导致慢查询
- 主从复制异常,服务阻塞,无法正常响应请求
消除大key的方法
1.拆分
将大key拆分为小key,例如一个String拆分成多个String
2.压缩
将value压缩后写入Redis,读取时解压后再用,注意选择一个合适的算法
3.集合类结构hash消除的方法
- 拆分,用hash取余,位掩码的方式决定放在哪个key中
- 区分冷热:比如榜单列表场景使用zset,只缓存前10页数据,后续的数据走db
热key定义
用户访问一个key的QPS特别高,导致server出现CPU负载突增或者不均的情况,热key没有明确的标准,一般QPS超过500就有可能被识别为热Key
解决热Key的方法
1.设置Localcache
在访问Redis前,在业务侧设置Localcache,降低访问Redis的QPS,如果Localcache过期或者未命中,则从Redis中将数据更新到LocalCache
2.拆分
将一个热key复制写入多份,访问的时候访问多个key,但是value是同一个,但是代价是更新时需要更新多个key
慢查询场景
容易导致慢查询的操作
-
批量操作一次性传入过多的key/value,如mset/hmset等操作,建议单批次不要超过100
-
zset大部分命令都是O(logn),当大小超过5k以上时,简单的zadd/zerm也可能导致慢查询
-
操作大key
缓存穿透和缓存雪崩
缓存穿透:热点数据查询绕过缓存,直接查询数据库
缓存雪崩:大量缓存同时过期
缓存穿透的危害:
- 查询一个一定不存在的数据,这样的所有请求都会打到db上
- 缓存过期时,一个热key过期,也会有大量的请求同时击穿至db
减少缓存穿透的方法
- 缓存空值,在查询到在缓存和数据库中都不存在,则可以缓存一个空值
- 布隆过滤器,使用一个算法来存储合法key
避免缓存雪崩的方法
- 将缓存失效的时间分散开,比如在原有的失效时间基础上增加一个随机值
- 使用缓存集群,避免单机宕机造成的缓存雪崩
缓存击穿问题
缓存击穿也叫热点key问题,被高并发访问以及缓存重建业务较复杂的key突然失效了,会对数据库带来巨大冲击。
解决方法:
互斥锁和逻辑过期
1.互斥锁就是在做缓存重建时进行加锁,其余线程进行带有休眠的重试
缺点:
线程需要等待,性能受影响
会有死锁风险
2.逻辑过期,不设置真正的过期时间,而是设置一个字段作为逻辑过期
获取互斥锁后开启一个新线程,进行缓存重建,同时原始线程返回一个旧的信息
其余线程发现过期后获取互斥锁失败后不进行等待,直接也返回旧数据
缺点:
不能保证一致性
数据库和缓存如何保持一致性
无论是先更新数据库还是先更新缓存,都有可能无法保证数据库的一致性
于是引出旁路缓存策略(cache aside pattern)
更新数据时,不更新缓存,而是删除缓存中的数据。然后到读取数据时,发现缓存中没有数据之后,再从数据库中读取数据,更新到缓存中
该策略可以分为两个策略,读策略和写策略
写策略是更新数据库或删除缓存
读策略是缓存未命中后读取数据库的数据和回写数据
先删除缓存,再更新数据库会造成数据不一致的问题
而先更新数据库,再删除缓存虽然也可能会存在数据不一致的问题
但是因为写缓存的速度远远比写数据库的速度要快,所以缓存不一致的情况很少发生
所以,最好的方案就是先更新数据库,再删除缓存
删除相对于更新的好处除了数据一致性的问题,还因为每次更新数据库都更新缓存,无效写比较多。
然后给缓存数据加上过期时间,防止意外的缓存与数据库的数据不一致
但是该方法在每次更新数据库时,缓存的数据会被删除,这样会对缓存的命中率带来影响
除了旁路更新策略以外
还有write behind caching pattern
调用者只操作缓存,由其它线程异步的将缓存数据持久化到数据库,保证最终一致
不过目前主流的还是旁路缓存策略
然后我们还要保证数据库与缓存操作的原子性
在单体系统里,将缓存和数据库操作放在同一个事务
如果不使用事务的话,要保证操作一致性,可以采用重试机制
Redis分布式锁误删问题
在使用的时候设置值为该线程所产生的唯一的一个id,当这个线程阻塞后导致锁过期时,新的线程再重新加锁,原来的线程执行完毕之后判断当前锁是不是本线程产生的锁,如果不是则就不删除,这样就防止误删问题,不过为了加锁时间长于业务执行的时间,我们需要别的解决方法
Redission分布式锁
这里重点讲解一下ReentrantLock,可重入锁
解决锁的重入以及重试问题,并且防止业务用时超过加锁时间
加锁和解锁是用Lua脚本原子性的实现的
锁的可重试
先尝试获取锁,然后申请锁的耗时如果大于等于最大等待时间,则申请锁失败
可以订阅锁释放事件,通过awit方法阻塞等待锁释放,防止无效的锁资源申请问题,一旦锁释放会发消息通知等待的线程进行竞争
锁的续期机制
Redission提供了一个续期机制,只要一旦加锁成功,就会启动一个watchdog,每隔一段时间就会检查一下是否持有锁,如果持有,就会延长锁的时间
缺点就是采用多个Redis时会出现一些问题
锁的可重入机制
多记录一个加锁次数,每次访问使得锁的重入次数加1