一、缓存问题与解决

缓存穿透

缓存穿透是指查询缓存和DB中都不存在的数据

通过id查询,id一般大于0,攻击者会故意传id为-1去查询,由于缓存是不命中则从DB中获取数据,这将会导致每次缓存都不命中数据导致每个请求都访问DB,造成缓存穿透

缓存穿透示例:

    public Station findProjectStation(Long stationId) {
        //从缓存中查询
        Station station = (Station)redisTemplate.boundHashOps("project_station").get(stationId);
        if(station==null){
            //缓存中没有,从数据库查询
            Station st = stationMapper.selectByPrimaryKey(stationId);
            if(st!=null){ 
                station = st;
                redisTemplate.boundHashOps("project_station").put(stationId,station);
            }
        }
        return station;
    }

解决方案:

​1.接口层增加校验,如鉴权校验

2.id做校验,id<=0的直接拦截;

​3.从缓存取不到的数据,在数据库中也没有取到,就将key-value对写为key-空对象。以此防止攻击者反复用同一个id暴力攻击。

4. 使用缓存预热,缓存预热就是将数据提前加入到缓存中,当数据发生变更,再将最新的数据更新到缓存。

5. 利用互斥锁,缓存失效的时候,先去获得锁,得到锁了,再去请求数据库。没得到锁,则休眠一段时间重试

6. 利用布隆过滤器,内部维护一系列合法有效的key。迅速判断出,请求所携带的Key是否合法有效。如果不合法,则直接返回

7. 设置短的过期时间

解决缓存穿透示例:

    public Station findProjectStation(Long stationId) {
        //从缓存中查询
        Station station = (Station)redisTemplate.boundHashOps("project_station").get(stationId);
        if(station==null){
            //缓存中没有,从数据库查询
            Station st = stationMapper.selectByPrimaryKey(stationId);
            if(st!=null){
                station = st;
                redisTemplate.boundHashOps("project_station").put(stationId,station);
            }else {
                redisTemplate.boundHashOps("project_station").put(stationId,new Station());
                // 随机时间
                redisTemplate.boundHashOps("project_station").expire(60, TimeUnit.SECONDS);
            }
        }
        return station;
    }

缓存击穿

缓存击穿是指缓存中没有但数据库中有的数据

由于某个时刻并发用户量非常大,同时读缓存没读到数据,又同时去数据库获取数据,引起数据库压力瞬间增大,造成过大压力。

可能产生缓存击穿示例:

    public List<Station> findStationList() {
        //从缓存中查询
        List<Station> stationList = (List<Station>)redisTemplate.boundValueOps("station_list").get();;
        if(stationList==null){
            //缓存中没有,从数据库查询
            Example example=new Example(Station.class);
            Example.Criteria criteria = example.createCriteria();
            criteria.andEqualTo("type","1");
            List<Station> findStationList = stationMapper.selectByExample(example);
            if(findStationList!=null){
                stationList = findStationList;
                redisTemplate.boundValueOps("station_list").set(stationList);
            }
        }
        return stationList;
    }

解决方案:

​1.设置热点数据永远不过期。

​2.缓存预热

3.数据不设置过期时间,在缓存的对象上添加一个属性标识过期时间,每次获取到数据时,校验对象中的过期时间属性,如果数据即将过期,则异步发起一个线程主动更新缓存中的数据,当然也可能拿到过期的值,看具体需求。

4.加锁。本地锁与分布式锁。

解决示例:

    public List<Station> findStationList() {
        //从缓存中查询
        List<Station> stationList = (List<Station>) redisTemplate.boundValueOps("station_list").get();
        ;
        if (stationList == null) {
            //缓存中没有,加锁从数据库查询
            synchronized (this) {
                Example example = new Example(Station.class);
                Example.Criteria criteria = example.createCriteria();
                criteria.andEqualTo("type", "1");
                List<Station> findStationList = stationMapper.selectByExample(example);
                if (findStationList != null) {
                    stationList = findStationList;
                    redisTemplate.boundValueOps("station_list").set(stationList);
                }
            }
        }
        return stationList;
    }

缓存雪崩

缓存雪崩是指缓存数据大批量到过期时间,而查询数据量巨大,引起数据库压力过大甚至宕机

缓存中如果大量缓存在一段时间内集中过期了,这时候会发生大量的缓存击穿现象,所有的请求都落在了DB上,由于查询数据量巨大,引起DB压力过大甚至导致DB宕机

解决方案:

​1.缓存数据的过期时间设置随机,防止同一时间大量数据过期现象发生。如果Redis是集群部署,将热点数据均匀分布在不同的Redis库中也能避免全部失效的问题

​2.设置热点数据永远不过期。

​3.使用缓存预热
 
4.使用互斥锁,但是该方案将导致吞吐量明显下降

三者区别

缓存穿透:通常请求携带有参数,不断发起请求。

缓存击穿:通常是某个时刻并发大量请求,并发查询同一条数据。

缓存雪崩:缓存不同的数据大批量到过期时间,很多数据都查不到从而查数据库。

缓存预热

缓存预热是将数据提前加入到缓存中,当数据发生变更,再将最新的数据更新到缓存。

实现缓存预热方法:

创建RedisInit 类,实现InitializingBean接口并重写afterPropertiesSet方法,然后编写逻辑,启动项目时就会自动执行相应逻辑。

import org.springframework.beans.factory.InitializingBean;

@Component
public class RedisInit implements InitializingBean {

    @Autowired
    private IStationService stationService;

    /**
     * 缓存预热
     *
     * @throws Exception
     */
    @Override
    public void afterPropertiesSet() throws Exception {
        stationService.saveStationListToRedis();
    }
}

二、缓存淘汰机制

Redis可以对存储在Redis中的缓存数据设置过期时间,但是并非key过期时间到了就一定会被Redis给删除。

Redis可以设置最大缓存即最大内存,当内存已使用率到达,则开始清理缓存

maxmemory <bytes> :设置最大内存 maxmemory 500mb

删除策略

设置了expire的key缓存过期了,但是服务器的内存还是会被占用,这是因为redis是基于删除策略进行删除

Redis删除策略主要有三种:

1.定期删除

Redis默认是每隔 100ms 就随机抽取一些设置了过期时间的 Key,检查其是否过期,如果过期就删除。

2.惰性删除

定期删除由于是随机抽取可能会导致很多过期 Key 到了过期时间并没有被删除。在从缓存获取数据的时候,redis会检查这个key是否过期了,如果过期就删除这个key。也就是说在查询的时候将过期key从缓存中清除。

3.主动删除

当内存满了,依据配置的淘汰策略进行删除

内存淘汰机制

仅使用定期删除 + 惰性删除机制存在一个严重的隐患:如果定期删除留下了很多已经过期的key,并且长时间都没有使用过这些过期key,会导致过期key无法被惰性删除,从而导致过期key一直堆积在内存里,最终造成Redis内存块被消耗殆尽。

Redis内存淘汰机制应运而生。Redis内存淘汰机制提供了八种不同的内存淘汰策略,4.0前有6种。

volatile-lru : 从已设置过期时间的缓存中,淘汰最近最少使用的数据,推荐使用策略

volatile-ttl: 在设置了过期时间的缓存中,挑选将要过期的数据,直接淘汰

volatile-random: 从已设置过期时间的缓存中,随机淘汰数据

allkeys-lru: 所有数据,淘汰最近最少使用的,即清除最少用的旧缓存,然后保存新的缓存,推荐使用

allkeys-random: 在所有的缓存中随机删除,不推荐

noeviction: 旧缓存永不过期,新缓存设置不了,不淘汰,内存满了返回错误,默认策略

volatile-lfu: 从已设置过期时间的缓存中,淘汰使用频率最低的数据

allkeys-lfu: 所有数据,淘汰使用频率最低的数据