问题一:缓存穿透,指缓存中没有数据,数据库中也没有数据。在进行数据的访问时,通过数据的key读取数据,但是该key对应数据在数据库中没有,在缓存中也没有,造成每次通过该key读取数据都会进行数据库操作,且每次读取都为null的情况。在大型项目中,这种无效的数据库操作会增加数据库的读压力。
示例代码:
public Object getData(String key){
if(redisTemplate.hasKey(key)){
return redisTemplate.opsForValue().get(key);
}
Object object = dataDao.selectSysCompanyById(key);
if(object != null){
redisTemplate.opsForValue().set(key,object);
}
return object;
}
如上代码,在项目当中读取数据时,首先都会先读取缓存,如果缓存中不存在数据,则从数据库读取数据,如果从数据库读取到数据,那么将数据缓存并返回。
当请求的key对应的数据,在缓存中不存在,同时在数据库中也不存在时,就会出现每次都会执行数据库操作,但是每次都是返回null的无效数据库操作,这种情况就是典型的缓存穿透。
解决方案一:缓存空对象,即对于在数据库中和缓存中都不存在的数据,第一次读取到为null时,在缓存中就存一个空值。读取缓存的时候,如果缓存中存在,且为空对象,直接返回即可。
public Object getData(String key){
if(redisTemplate.hasKey(key)){
if(redisTemplate.opsForValue().get(key) == null){
//存在缓存且为空,直接返回
return null;
}
return redisTemplate.opsForValue().get(key);
}
Object object = dataDao.selectSysCompanyById(key);
if(object != null){
redisTemplate.opsForValue().set(key,object);
}else{
//缓存空对象
redisTemplate.opsForValue().set(key, null);
}
return object;
}
解决方案二:使用布隆过滤器,所谓布隆过滤器,我们可以理解其为一个集合,这个集合只存储数据的key而不存储数据值,主要用来判断一个key存在还是不存在。布隆过滤器的介绍见https://zhuanlan.zhihu.com/p/72378274。
google的guava实现了单机版的(即基于JVM)的布隆过滤器,我们的示例使用guava来呈现,在实际的项目中,如果已经达到了要使用布隆过滤器,那么基本上是要自己实现一个分布式的布隆过滤器,可以先研究google的guava的实现,来自行尝试实现一个自己的布隆过滤器。
使用布隆过滤器解决缓存穿透,其主要思想是,将数据的key全部存入布隆过滤器,在进行数据读取时,先判断key在布隆过滤器中是否存在,如果存在则执行读取操作,如果不存在,直接返回。
也就是说,在创建数据或者将数据同步到缓存服务时,需要将数据的key存储到布隆过滤器的集合中,用以作为读取数据时的判断依据。
public Object getDataWithBloom(String key) {
//initBloomFilter : 初始化布隆过滤器,将数据库中所有的key都写入到布隆过滤器
BloomFilter<String> bloomFilter = initBloomFilter();
if(!bloomFilter.mightContain(key)){
//存在缓存且为空,直接返回
return null;
}
if(redisTemplate.hasKey(key)){
return redisTemplate.opsForValue().get(key);
}
Object object = dbMapper.selectSysCompanyById(key);
if(object != null){
redisTemplate.opsForValue().set(key,object);
}
return object;
}
问题二:缓存击穿,所谓缓存击穿,是指指定key的数据在数据库中存在,但是缓存中还没有写入该key对应的数据,在高并发场景下,多个线程同时通过该key读取数据时,会因为高并发访问的原因导致同一个key对应的数据会从数据库被多次读取。正常情况下是同一个key的数据,只能允许一个线程从数据库读取一次并缓存到缓存服务器Redis之后,其他的线程读取数据时直接从缓存服务读取。
示例代码:
public Object getData(String key){
if(redisTemplate.hasKey(key)){
return redisTemplate.opsForValue().get(key);
}
Object object = dataDao.selectSysCompanyById(key);
if(object != null){
redisTemplate.opsForValue().set(key,object);
}
return object;
}
比如上述代码实现,就存在缓存击穿的风险,在高并发场景下,如果同时有100个线程请求同一个key的数据,且该数据在数据库中存在,但是在缓存中不存在。那么在某一个线程从数据库读取数据并写入到缓存之前,所有执行了到2行代码的线程都会执行一次数据库的select操作和缓存的写入操作。如第一个线程还未读取数据写入缓存,就有50个线程执行完了第2行代码,那么这50个线程都会执行一次数据库的select操作,同时也都会做一次缓存写入操作,这就是典型的缓存击穿现象。
解决方案:互斥锁,单机情况下,使用JVM中的同步代码块synchronized或者ReentrantLock,分布式情况下,使用分布式锁。
比如:
public Object getData(String key) {
//initBloomFilter : 初始化布隆过滤器
BloomFilter<String> bloomFilter = BloomFilter.create(Funnels.stringFunnel(Charset.defaultCharset()),10000,0.0001);
if(!bloomFilter.mightContain(key)){
//存在缓存且为空,直接返回
return null;
}
if(redisTemplate.hasKey(key)){
return redisTemplate.opsForValue().get(key);
}
//加锁
RLock lock = redissonClient.getLock(key);
lock.lock();
Object object = null;
try {
if(redisTemplate.hasKey(key)){
//阻塞的线程获得锁后,直接从缓存读取返回
return redisTemplate.opsForValue().get(key);
}
//第一个线程读库
object = dbMapper.selectSysCompanyById(key);
if(object != null){
//第一个线程写缓存
redisTemplate.opsForValue().set(key,object);
}
}finally {
//释放锁
lock.unlock();
}
return object;
}
问题三:缓存雪崩,所谓缓存雪崩,就是指缓存服务不具备高可用性导致大面积缓存失效的情况。
解决方案:缓存集群,数据量不大,小公司,采用Redis主从+哨兵模式的解决方案,海量数据,大公司,采用Redis Cluster模式搭建缓存集群。
如果发生了雪崩,应急方案为进行服务熔断,通过限流慢慢对缓存进行预热。
问题四:缓存与数据库数据一致性问题,所谓数据一致性,就是指在解决数据进行更新时,如何对数据进行操作才能保证数据库中的数据和缓存中的数据一致的问题。
发生数据一致性问题的场景一:
发生数据一致性问题的场景二:
解决方案:先删除缓存,再更新数据库。但是单纯的先删除缓存,再更新数据库,高并发场景下依然解决不了问题。如下图:
解决方案一:延迟双删:
解决方案二:串行化: