Redis做缓存时,出现的缓存击穿、缓存穿透、缓存雪崩等问题及解决方案
一.前言
Redis可以用于做缓存层框架,当请求数据时,先从缓存中读取,可以减少SQL数据库读取的压力。本文会讲解一下使用缓存时会出现的问题,如缓存击穿、缓存穿透、缓存雪崩。
二.缓存击穿
缓存中没有但数据库有的数据(一般是缓存时间到期),这时候由于并发用户特别多,同时读缓存没读到数据,就同时去读数据库,对数据库CPU和内存造成巨大压力,严重时会造成数据库宕机,从而形成一系列的连锁反应,造成系统崩溃。
解决方案:
1.给缓存设置永不过期。
但这样缺点很明显,需要我们手动更新缓存,缓存数据的更新会有延迟。
2.加互斥锁。
通俗地讲,就是当有一万个用户访问这一条数据,但是只有一个用户才能访问数据库去拿到这个数据,等这个用户拿到数据后,再把这条数据存到缓存中。剩下的用户要等待到这条数据加入缓存后,才去访问缓存获取数据。
// 定义一个标识确保线程同步 private static readonly object locker = new object(); [Route("get")] [HttpGet] public string Get(string key) { const int cacheTime = 30; // 从Redis里面取数据 string cacheValue = _redis.StringGet(key); if (cacheValue != null) { return cacheValue; } else { lock (locker) { cacheValue = _redis.StringGet(key); if (cacheValue != null) { return cacheValue; } else { //这里一般是 sql查询数据。 cacheValue = GetValueFromDB(); //重新给key设置缓存 _redis.StringSet(key, cacheValue, cacheTime); } } return cacheValue; } }
第一次访问的线程进到相关位置后会给locker对象加锁,等缓存重新设置后才解锁。其它线程运行到相关位置就只能挂起,等待locker对象解锁才能继续运行。
加互斥锁只能够减少数据库的压力,如果是高并发的情况下,加了互斥锁,在缓存重新创建之前,1000条请求会有999条被挂起,会造成用户等待超时。
3.给每个缓存数据添加一个缓存标记,记录缓存是否失效,如果失效的话,就更新缓存数据。
public string Get(string key) { const int cacheTime = 30; //缓存标记。 string cacheSign = key + "_sign"; string sign = _redis.StringGet(cacheSign); //获取缓存值 string cacheValue = _redis.StringGet(key); if (sign != null) { return cacheValue; //未过期,直接返回。 } else { _redis.StringSet(cacheSign, "1", cacheTime); ThreadPool.QueueUserWorkItem((arg) => { //这里一般是 sql查询数据。 cacheValue = GetValueFromDB(); //日期设缓存时间的2倍 _redis.StringSet(key, cacheValue, cacheTime); }); return cacheValue; } }
缓存数据的生存时间要比它对应的缓存标记的生存时间要长。这样,当缓存标记过期了,还是可以从缓存数据中读取旧的缓存数据,直到另外开启的线程更新缓存,才能得到新的缓存数据。这个方案可以在一定程度上提高系统的吞吐量。
三.缓存穿透
数据库中没有的数据,缓存当然也不会有,而用户不断发起请求去查询这个数据,这样就会绕过缓存,每次都会直接去数据库进行查询。绕过缓存去查数据库,这就是我们常说的缓存命中率的问题。
解决方案是,如果数据库不存在该数据,在缓存中也设置一条空数据,这样第二次访问的时候,就不会绕过缓存,去读取数据库,把压力转到缓存上。
[Route("get")] [HttpGet] public string Get(string key) { const int cacheTime = 30; //获取缓存值 string cacheValue = _redis.StringGet(key); if (cacheValue != null) { return cacheValue; } else { cacheValue = GetValueFromDB(); //数据库查询不到,为空。 if (cacheValue == null) { cacheValue = string.Empty; //如果发现为空,设置个默认值,也缓存起来。 } _redis.StringSet(key, cacheValue, cacheTime); return cacheValue; } }
四.缓存雪崩
指缓存中的大量数据在同一个时间点到期,而在这个时间点,恰好有大批量的访问,造成数据库压力过大。
和缓存击穿不同的是,缓存击穿指的是同一条数据被高并发查询,而缓存雪崩指的是大批量缓存数据同时失效,很多数据查不到才同时去查询数据库。
解决方案,缓存数据的生存时间设置要随机,防止同一个时间点出现大量缓存数据失效的情况发生。
五.总结
1.用Redis做缓存,遇到的问题和其它各类缓存机制都大同小异,遇到的问题也大致为上述问题,需要提前预防。
2.做缓存的目的是放在数据库压力过大。
3.不同业务场景需要设置的缓存层都不同,需要根据不同情况来处理。