前言
这一篇章我们来聊聊Redis经典问题
一、缓存问题
1.1缓存穿透
缓存穿透是指在高并发下查询key不存在的数据,会穿过缓存查询数据库。导致数据库压力过大而宕机
解决方案:对查询结果为空的情况也进行缓存,缓存时间(ttl)设置短一点,或者该key对应的数据insert了之后清理缓存。
问题:缓存太多空值占用了更多的空间
使用布隆过滤器。在缓存之前在加一层布隆过滤器,在查询的时候先去布隆过滤器查询 key 是否存在,如果不存在就直接返回,存在再查缓存和DB。
布隆过滤器
实际上是一个很长的二进制向量和一系列随机hash映射函数。
布隆过滤器的原理是,当一个元素被加入集合时,通过K个Hash函数将这个元素映射成一个数组中的K个点,把它们置为1。检索时,我们只要看看这些点是不是都是1就(大约)知道集合中有没有它了:如果这些点有任何一个0,则被检元素一定不在;如果都是1,则被检元素很可能在。这就是布隆过滤器的基本思想。
缓存雪崩
当缓存服务器重启或者大量缓存集中在某一个时间段失效,这样在失效的时候,也会给后端系统(比如DB)带来很大压力。突然间大量的key失效了或redis重启,大量访问数据库,数据库崩溃解决方案:
1、key的失效期分散开,不同的key设置不同的有效期
2、设置二级缓存(数据不一定一致)3、高可用(脏读)
缓存击穿
对于一些设置了过期时间的key,如果这些key可能会在某些时间点被超高并发地访问,是一种非常“热点”的数据。这个时候,需要考虑一个问题:缓存被“击穿”的问题,这个和缓存雪崩的区别在于这里针对某一key缓存,前者则是很多key。缓存在某个时间点过期的时候,恰好在这个时间点对这个Key有大量的并发请求过来,这些请求发现缓存过期一般都会从后端DB加载数据并回设到缓存,这个时候大并发的请求可能会瞬间把后端DB压垮。
解决方案:
1、用分布式锁控制访问的线程使用redis的setnx互斥锁先进行判断,这样其他线程就处于等待状态,保证不会有大并发操作去操作数据库。
2、不设超时时间,volatile-lru 但会造成写一致问题当数据库数据发生更新时,缓存中的数据不会及时更新,这样会造成数据库中的数据与缓存中的数据的不一致,应用会从缓存中读取到脏数据。可采用延时双删策略处理。
保证数据的最终一致性(延时双删)
1、先更新数据库同时删除缓存项(key),等读的时候再填充缓存2、2秒后再删除一次缓存项(key)
3、设置缓存过期时间 Expired Time 比如 10秒或1小时4、将缓存删除失败记录到日志中,利用脚本提取失败记录再次删除(缓存失效期过长 7*24)
升级方案
通过数据库的binlog来异步淘汰key,利用工具(canal)将binlog日志采集发送到MQ中,然后通过ACK机制确认处理删除缓存。
1.2数据并发竞争
并发指的是多个redis的client同时set 同一个key引起的并发问题
第一种方案:分布式锁+时间戳
1.整体技术方案这种情况,主要是准备一个分布式锁,大家去抢锁,抢到锁就做set操作。加锁的目的实际上就是把并行读写改成串行读写的方式,从而来避免资源竞争
2.Redis分布式锁的实现
主要用到的redis函数是setnx()用SETNX实现分布式锁
第二种方案:利用消息队列
在并发量过大的情况下,可以通过消息中间件进行处理,把并行读写进行串行化。把Redis的set操作放在队列中使其串行化,必须的一个一个执行。
1.3Hot Key
当有大量的请求(几十万)访问某个Redis某个key时,由于流量集中达到网络上限,从而导致这个redis的服务器宕机。造成缓存击穿,接下来对这个key的访问将直接访问数据库造成数据库崩溃,或者访问数据库回填Redis再访问Redis,继续崩溃。
如何处理热Key:
1、变分布式缓存为本地缓存发现热key后,把缓存数据取出后,直接加载到本地缓存中。可以采用Ehcache、Guava Cache都可以,这样系统在访问热key数据时就可以直接访问自己的缓存了。(数据不要求时时一致)2、在每个Redis主节点上备份热key数据,这样在读取时可以采用随机读取的方式,将访问压力负载到每个Redis上。
3、利用对热点数据访问的限流熔断保护措施每个系统实例每秒最多请求缓存集群读操作不超过 400 次,一超过就可以熔断掉,不让请求缓存集群,直接返回一个空白信息,然后用户稍后会自行再次重新刷新页面之类的。(首页不行,系统友好性差)通过系统层自己直接加限流熔断保护措施,可以很好的保护后面的缓存集群。
1.4Big Key
大key指的是存储的值(Value)非常大,常见场景:
热门话题下的讨论
大V的粉丝列表
序列化后的图片
没有及时处理的垃圾数据
大key的处理:
优化big key的原则就是string减少字符串长度,list、hash、set、zset等减少成员数。
1、string类型的big key,尽量不要存入Redis中,可以使用文档型数据库MongoDB或缓存到CDN上。如果必须用Redis存储,最好单独存储,不要和其他的key一起存储。采用一主一从或多从。
2、单个简单的key存储的value很大,可以尝试将对象分拆成几个key-value,使用mget获取值,这样分拆的意义在于分拆单次操作的压力,将操作压力平摊到多次操作中,降低对redis的IO影响。
3、hash, set,zset,list 中存储过多的元素,可以将这些元素分拆。(常见)
二、分布式锁
2.1利用Watch实现Redis乐观锁
乐观锁
基于CAS(Compare And Swap)思想(比较并替换),是不具有互斥性,不会产生锁等待而消耗资源,但是需要反复的重试,但也是因为重试的机制,能比较快的响应。因此我们可以利用redis来实现乐观锁。
setnx实现原理
共享资源互斥
共享资源串行化
单应用中使用锁:(单进程多线程)synchronized、ReentrantLock
分布式应用中使用锁:(多进程多线程)
分布式锁是控制分布式系统之间同步访问共享资源的一种方式。利用Redis的单线程特性对共享资源进行串行化处理
Redisson分布式锁的使用
Redisson是架设在Redis基础上的一个Java驻内存数据网格(In-Memory Data Grid)。Redisson在基于NIO的Netty框架上,生产环境使用分布式锁。
加锁机制
如果该客户端面对的是一个redis cluster集群,他首先会根据hash节点选择一台机器。发送lua脚本到redis服务器上。
lua的作用:保证这段复杂业务逻辑执行的原子性
2.2分布式锁特性
互斥性
任意时刻,只能有一个客户端获取锁,不能同时有两个客户端获取到锁。Redis : setnx set key value NX 如果key存在就不设置
同一性
锁只能被持有该锁的客户端删除,不能由其它客户端删除。Redis : lua 实现原子性
可重入性
持有某个锁的客户端可继续对该锁加锁,实现锁的续租容错性锁失效后(超过生命周期)自动释放锁(key失效),其他客户端可以继续获得该锁,防止死锁expire 设置超时时间set key value NX PX