Redis的击穿、雪崩、穿透
正常使用redis的流程是:收到请求后,会去redis中查找key,查不到再去数据库中找,在数据库中找到之后就返回并放到缓存当中,查不到就抛出异常。但是在这个过程中会出现一些意外情况,比如击穿、雪崩、穿透。
1、缓存击穿
1、击穿是什么?
高并发状态下,当一个非常热点的key,在某一时间突然失效(或者一个冷门的key突然被大量访问,比如双十一交易量暴增,微博上明星的热点事件等等),大量并发穿破缓存访问数据库,会对数据库造成巨大压力,并且大量线程将key放回redis中,也会造成性能浪费。
2、如何解决?
两种方法:设置过期时间和加锁。
1、设置过期时间
比如对日常访问量答的key设置永不过期,冷门key设置一个过期时间。
但是这个方法解决不了冷门key突然被大量访问的问题。
2、加锁
当再redis中查询不到key的时候,加一个同步锁,只有一个线程能访问到。这个线程将key放进redis之后,其他线程被释放。
代码:
import Redis
import MySQL
import threading
# Redis.redis 是一个定义好的python访问r饿dis数据库的类
lock = threading.Lock()
def func(key):
res1 = Resdis.redis.read(key)
if res1 == None:
with lock(): # 加同步锁
res2 = Resdis.redis.read(key) # 再次取数据
if res2 != None:
return res2
res3 = MySQL.read(key) # 去数据库取数据
if res3 == None:
return None # 取不到说明数据库中没有,抛出异常
Redis.redis.write(key, res3)
return res3 # 取到返回数据并放进redis
else:
return res1
1、为什么加锁之后要再访问一次redis?
当第一个线程将了key放进redis之后,后面的线程锁随即释放,如果这里不访问redis而是直接访问数据库,依旧会对数据库造成巨大压力,这里再次访问redis可以立即拿到数据,不需要访问数据库。
2、为什么是同步锁?
如果部署了集群,假如是10个缓存,那么对于数据库来说,应对10个请求是没问题的。
分布式锁可以更好,但同步锁足够解决问题。
2、缓存雪崩
1、雪崩是什么?
与击穿不同,雪崩是指数据库中所有数据某一时刻全部过期或者缓存服务器宕机,导致大量请求访问数据库,给数据库造成巨大压力的情况。
2、如何解决?
1、针对所有key同一时间全部失效的情况,可以在解决击穿的基础上,对key设置随机过期时间。
伪代码:
expire = int(random.random() * 1000) # 生成一个三位数的随机数
Redis.redis.write(key, res3, expire)
2、针对服务器突然宕机或断电的情况,需要提前做好部署,集群哨兵模式或者多机房。
3、缓存穿透
1、什么是穿透?
请求的数据缓存中没有,数据库也没有。比如有攻击者使用很大的id或者负数id发送大量请求的时候,会对数据库造成巨大压力。
2、如何解决?
三种方式,一种是参数校验,一种是缓存空对象,一种是布隆过滤器。
数据校验
收到请求后对参数做个校验,过滤掉无效参数,比如很大的id或者负数id。
但是这种方法的缺点是没办法判断很大的id是恶意攻击还是正常id恰好没有。
缓存空对象
就是在去数据库查询的时候,不管有没有查到数据,都存在redis中,没有查到就存一个空对象,可以一定程度缓解数据库短时间内被大量攻击的情况。
布隆过滤器
布隆过滤器是一种比列表、集合等更高效的数据结构。
思路:在redis之前设置一个过滤器。每次访问前先去过滤器查询,查询不到再去数据库查询,数据库中也没有就将数据放进过滤器,再次访问在过滤器中访问到,返回空值。这个方法被称为黑名单过滤器,即存储数据库中没有的key。
还有一种白名单过滤器,就是将数据库中存在的数据放进过滤器。请求过来,如果过滤器中存在,再进行后续操作,如果过滤器中不存在,直接返回空值。
伪代码:
bloomFiliter = []
# 黑名单
def getResquest(key):
if key in bloomFiliter:
return None
return func(key)
# 白名单
def getResquest(key):
if key in bloomFiliter:
return func(key)
return None
# 布隆过滤器不是列表,是一种数据结构,这里是用列表表示布隆过滤器解决缓存穿透的思路。
4、一句话总结
把redis比喻为防弹衣,MySQL比喻为身体,穿透就是子弹穿透了防弹衣打在身体上,雪崩就是很多颗子弹透过防弹衣在身体上爆炸了,穿透就是子弹把防弹衣和身体都打穿了。
伪代码
import Redis
import MySQL
import threading
import random
# Redis MySQL 定义好的python访问数据库的类
lock = threading.Lock() # 线程锁
def findKey(key):
res1 = Redis.redis.read(key) # 第一次从redis中查询数据
if res1 == None:
with lock(): # 没查询到,加线程锁,防止大量请求访问数据库
res2 = Redis.read(key) # 再次查询,防止阻塞的请求大量访问数据库,这两行可以防止缓存击穿问题
if res2 == None:
res3 = MySQL.select(key)
expire = int(random.random() * 1000)
Redis.write(key, res3, expire)
# 不管从数据库查询的结果如何,都存入redis,并设置过期时间,可以防止缓存雪崩问题
return res3
else:
return res2
else:
return res1 # 查询到,直接将数据返回
bloomFilter = [] #此处以黑名单为例
def bloomFilterGetID(key):
if key in bloomFilter:
return None # id 存在于黑名单,直接返回,可以防止缓存穿透问题
return findKey(key)