redis实现高性能缓存架构

有些业务经常访问数据库表数据,但是访问数据库表是有IO消耗的,特别是成百上千万的访问量时,系统更加受不住,会造成一部分用户获取不到响应,交互体验差。
这时候就需要提升系统性能,以便改善响应速率,最高效方便的就是缓存,现在就使用redis实现高性能缓存,将我们业务中最常用的数据缓存到redis中,则我们不需要访问数据库,直接获取内存中的缓存,效率高很多。
就是访问save和update方法时,将数据放到redis里缓存起来,访问delete方法时删掉redis中的数据,访问get方法的时候直接从redis中获取数据,没有的时候才从数据库中获取数据。
来看一下一个最简版的缓存实现

demo1
public class NewsServiceImpl implements NewsService {

    @Autowired
    private RedisTemplate redisTemplate;
    @Autowired
    private Redisson redisson;
    @Autowired
    private NewsDao newsDao;

    private final static String CACHE_NEWS_PRE = "cache:newes";

    @Override
    public NewsEntity get(long id) {
        String news = (String)redisTemplate.opsForValue().get(CACHE_NEWS_PRE + id);
        if (StringUtils.isNotBlank(news)){
            NewsEntity entity = JSON.parseObject(news, NewsEntity.class);
            return entity;
        }
        NewsEntity dbEntity = newsDao.getOne(id);
        if (dbEntity != null) redisTemplate.opsForValue().set(CACHE_NEWS_PRE + id, dbEntity);
        return dbEntity;
    }
    @Override
    public NewsEntity insert(NewsEntity entity) {
        NewsEntity news = newsDao.save(entity);
        redisTemplate.opsForValue().set(CACHE_NEWS_PRE + news.getId(), JSONObject.toJSONString(news));
        return news;
    }
    @Override
    public NewsEntity update(NewsEntity entity) {
        NewsEntity news = newsDao.save(entity);
        redisTemplate.opsForValue().set(CACHE_NEWS_PRE + news.getId(), JSONObject.toJSONString(news));
        return news;
    }
    @Override
    public void delete(long id) {
        newsDao.deleteById(id);
        redisTemplate.delete(CACHE_NEWS_PRE + id);
    }
}

这种代码只能应付偶尔使用缓存的情况,高并发高访问下就不中用了,那就改进一下。
明眼的一看就知道有线程安全问题,缓存读写不一致。缓存也没有做管理,一直占用内存,如果新浪微博的数据,缓存一直不做清理,上亿的数据,redis不得炸了,先来处理下这个问题,当我们读写这个数据时,给它设置超时时间,那么一直被访问的数据就会被保留在redis中,不被访问的慢慢的就会过期了。

demo2
// 所有设置缓存的地方都加了超时时间,我假设定了1天超时
@Override
public NewsEntity get(long id) {
    String news = (String)redisTemplate.opsForValue().get(CACHE_NEWS_PRE + id);
    if (StringUtils.isNotBlank(news)){
        NewsEntity entity = JSON.parseObject(news, NewsEntity.class);
        redisTemplate.expire(CACHE_NEWS_PRE + id, CACHE_NEWS_EXPIRE, TimeUnit.SECONDS);
        return entity;
    }
    NewsEntity dbEntity = newsDao.getOne(id);
    if (dbEntity != null) redisTemplate.opsForValue().set(CACHE_NEWS_PRE + id, dbEntity, CACHE_NEWS_EXPIRE, TimeUnit.SECONDS);
    return dbEntity;
}
缓存击穿

很经典的一个问题,顾名思义,就击穿了缓存,在缓存中差不多数据,当有大量的请求打过来时就打了数据库中,会导致数据库压力变大甚至崩溃。来看下场景就容易知道怎么解决了。

情景1:国人心血来潮,都来访问新浪,这时新浪会建立大量的缓存在redis中,过了段时间,缓存超时时间到了,大家又来访问,但是这时缓存都已经失效了,这样大家的访问就都落在了数据库里,这时就要阻止缓存的同时失效,才不至于击穿缓存。

demo3
// 随机生成3小时内差异的超时时间
public int getCacheNewsExpire() {
    return CACHE_NEWS_EXPIRE + new Random().nextInt(3*60*60);
}
@Override
public NewsEntity get(long id) {
    String news = (String)redisTemplate.opsForValue().get(CACHE_NEWS_PRE + id);
    if (StringUtils.isNotBlank(news)){
        NewsEntity entity = JSON.parseObject(news, NewsEntity.class);
        redisTemplate.expire(CACHE_NEWS_PRE + id, getCacheNewsExpire(), TimeUnit.SECONDS);
        return entity;
    }
    NewsEntity dbEntity = newsDao.getOne(id);
    if (dbEntity != null) redisTemplate.opsForValue().set(CACHE_NEWS_PRE + id, dbEntity, getCacheNewsExpire(), TimeUnit.SECONDS);
    return dbEntity;
}
缓存穿透

和缓存击穿类似的另一个问题就是缓存穿透,和缓存击穿有一点不一样,缓存穿透是把redis缓存和数据库都穿透了,即两边都查不到数据,那这个会造成什么问题呢?看下情景:

情景2:实不相瞒,我是个黑客,我现在想要攻击实现了上面缓存方法的网站,我生成不合规id(保证不在数据库中)来访问网站,用压测工具瞬时生成上百万的请求达到网站中,数据库都得被打崩。

解决:给空数据建立缓存,过期时间就几分钟就好了,免得空缓存一直留在redis中占用内存。

demo4
private final static int CACHE_NEWS_EXPIRE = 60*60*24;
private final static String CACHE_NEWS_NULL = "{}";

@Override
public NewsEntity get(long id) {
    String news = (String)redisTemplate.opsForValue().get(CACHE_NEWS_PRE + id);
    if (StringUtils.isNotBlank(news)){
        if (CACHE_NEWS_NULL.equals(news)){
            // 如果查到的是空缓存,则延迟空缓存时间
            redisTemplate.expire(CACHE_NEWS_PRE + id, getNullCacheNewsExpire(), TimeUnit.SECONDS);
            return null;
        }
        NewsEntity entity = JSON.parseObject(news, NewsEntity.class);
        //对查到的缓存做延时失效
        redisTemplate.expire(CACHE_NEWS_PRE + id, getCacheNewsExpire(), TimeUnit.SECONDS);
        return entity;
    }
    NewsEntity dbEntity = newsDao.getOne(id);
    if (dbEntity != null) {
        redisTemplate.opsForValue().set(CACHE_NEWS_PRE + id, dbEntity, getCacheNewsExpire(), TimeUnit.SECONDS);
    }else {
        // 数据库里没有的也要做缓存,防止缓存穿透问题
        redisTemplate.opsForValue().set(CACHE_NEWS_PRE + id, CACHE_NEWS_NULL, getNullCacheNewsExpire(), TimeUnit.SECONDS);
    }
    return dbEntity;
}

public int getNullCacheNewsExpire() {
    return 120 + new Random().nextInt(60);
}

当遇到高并发时,还是有严重的问题,如下图,所有访问都打到查数据库的地方,都没有缓存可返回,会造成数据库的压力。
redis实现高性能缓存架构_第1张图片
所以要防止这种情况发生,我们要解决一下下面的问题:

  1. 不能让所有访问都打到数据库中
  2. 在这个时候当第一个请求访问到数据库,建立了缓存,其他的请求要能够查询到缓存数据返回
demo5
@Override
public NewsEntity get(long id) {
    NewsEntity redisCache = getRedisCache(CACHE_NEWS_PRE + id);
    if (redisCache != null){
        return redisCache;
    }
    RLock lock = redisson.getLock(CACHE_NEWS_PRE + id);
    lock.lock();
    try{
        // 双重检测机制,通过锁同步,当第一个请求完成时redis就会有缓存,到时候直接走缓存就能获取数据了,不用走db
        NewsEntity redisCacheDoubleCheck = getRedisCache(CACHE_NEWS_PRE + id);
        if (redisCacheDoubleCheck != null){
            return redisCacheDoubleCheck;
        }
        // 查询数据库
        NewsEntity dbEntity = newsDao.getOne(id);
        if (dbEntity != null) {
            redisTemplate.opsForValue().set(CACHE_NEWS_PRE + id, dbEntity, getCacheNewsExpire(), TimeUnit.SECONDS);
        }else {
            // 数据库里没有的也要做缓存,防止缓存穿透问题
            redisTemplate.opsForValue().set(CACHE_NEWS_PRE + id, CACHE_NEWS_NULL, getNullCacheNewsExpire(), TimeUnit.SECONDS);
        }
        return dbEntity;
    }catch (Exception e){
        e.printStackTrace();
    }finally {
        lock.unlock();
    }
    return null;
}

public NewsEntity getRedisCache(String key){
    String news = (String)redisTemplate.opsForValue().get(key);
    if (StringUtils.isNotBlank(news)){
        if (CACHE_NEWS_NULL.equals(news)){
            // 如果查到的是空缓存,则延迟空缓存时间
            redisTemplate.expire(key, getNullCacheNewsExpire(), TimeUnit.SECONDS);
            return new NewsEntity();
        }
        NewsEntity entity = JSON.parseObject(news, NewsEntity.class);
        //对查到的缓存做延时失效
        redisTemplate.expire(key, getCacheNewsExpire(), TimeUnit.SECONDS);
        return entity;
    }
    return null;
}
双写不一致问题

这段代码还是解决不了双写不一致的问题,什么是双写不一致呢?就是数据库和缓存的值不一样。
这时有两个线程,,每个线程都有两个步骤,写数据库,更改缓存,如下:
线程A:
A.1 : 写数据进数据库
A.2 : 更新缓存
线程B:
B.1 : 写数据进数据库
B.2 : 更新缓存

但是当执行顺序按照 A.1 -> B.1 -> B.2 -> A.2 时,数据库保存的就是线程B的值,缓存保存的就是线程A的值 ,这就出现了不一致的情况。
这时要给写加锁

demo6
@Override
public NewsEntity get(long id) {
    NewsEntity redisCache = getRedisCache(CACHE_NEWS_PRE + id);
    if (redisCache != null){
        return redisCache;
    }
    // 分布式锁
    // RLock lock = redisson.getLock(CACHE_NEWS_PRE + id);
    // lock.lock();
    // 把锁改成读写锁,读读不互斥
    RReadWriteLock readWriteLock = redisson.getReadWriteLock(CACHE_NEWS_PRE + id);
    // 上读锁
    RLock readLock = readWriteLock.readLock();
    try{
        // 双重检测机制,通过锁同步,当第一个请求完成时redis就会有缓存,到时候直接走缓存就能获取数据了,不用走db
        NewsEntity redisCacheDoubleCheck = getRedisCache(CACHE_NEWS_PRE + id);
        if (redisCacheDoubleCheck != null){
            return redisCacheDoubleCheck;
        }
        // 查询数据库
        NewsEntity dbEntity = newsDao.getOne(id);
        if (dbEntity != null) {
            redisTemplate.opsForValue().set(CACHE_NEWS_PRE + id, dbEntity, getCacheNewsExpire(), TimeUnit.SECONDS);
        }else {
            // 数据库里没有的也要做缓存,防止缓存穿透问题
            redisTemplate.opsForValue().set(CACHE_NEWS_PRE + id, CACHE_NEWS_NULL, getNullCacheNewsExpire(), TimeUnit.SECONDS);
        }
        return dbEntity;
    }catch (Exception e){
        e.printStackTrace();
    }finally {
        readLock.unlock();
    }
    return null;
}
@Override
public NewsEntity update(NewsEntity entity) {
    RReadWriteLock readWriteLock = redisson.getReadWriteLock(CACHE_NEWS_PRE + entity.getId());
    // 上写锁
    RLock writeLock = readWriteLock.writeLock();
    NewsEntity news = null;
    try{
        news = newsDao.save(entity);
        redisTemplate.opsForValue().set(CACHE_NEWS_PRE + news.getId(), JSONObject.toJSONString(news), getCacheNewsExpire(), TimeUnit.SECONDS);
    }finally {
        writeLock.unlock();
    }
    return news;
}

给代码加上读写锁,读写互斥,读读不互斥,这样就能处理了双写不一致情况。

缓存雪崩

我们redis单节点能够抗住的并发量撑死也就10万,但是如果每秒内有百万级别的并发时,我们的redis就会抗不住而挂掉,请求打到数据库中,redis都抗不住,数据库更不用说,也挂了,导致整个应用都挂了,这就是缓存雪崩效应了。
这时候的解决方案就很是见仁见智了,架构这东西只有最适合的没有最好的。

  1. 如果是单体架构,好说,加个jvm缓存,怎么也能抗个百万并发,即我们常说的多级缓存。
  2. 如果是分布式架构,加jvm缓存会有其他节点缓存不一致的问题,就需要考虑数据一致性,比如加入MQ,或者发布订阅模式,这时候又要考虑新加入的系统可用性。
  3. 用sentinel或者hstrix做服务限流和服务降级
  4. 增加节点做负载均衡引流
布隆过滤器

缓存的方式有很多种,上面列举的只是个人使用的,也有些公司使用的是布隆过滤器,何为布隆过滤器?
类似Map的存储结构,通过bit位的0和1来表示是否有缓存,比如一个byte就能存储8位的bit,01010110这样,设置一个位数组来存储这些数据,然后通过均匀的hash算法将数据均匀的分布到位数组中,有数据则将对应位置设置为1,hash算法要确保数据尽可能的均匀分布,这样的布隆过滤器才更加准确。

请求落在布隆过滤器上的值是0的话则表示一定没有数据,直接不让请求过,如果值为1的话就可能有数据。
实现布隆过滤器前提是预先把数据加载到布隆过滤器上,redisson也提供了布隆过滤器的实现:

public RBloomFilter initBloomFilterCache(){
    RBloomFilter<Object> idListBloom = redisson.getBloomFilter("idList");
    // 初始化布隆过滤器,参数1:预估数据量,参数2:允许的误差量
    idListBloom.tryInit(10000000L, 0.05);
    // TODO 将数据库中的数据填充到布隆过滤器中
    // while () idListBloom.add(id);
    return idListBloom;
}

布隆过滤器也是有缺点的,它不能删除数据,位数组下标的值为1代表的可能是一个或者多个值的hash缓存位置,没有像map一样用链表维护,所以删了会导致其他缓存的问题,这也就表示如果频繁修改数据,就会导致布隆过滤器上有大量的过期数据,会越来越不准确。

说白了,任何缓存都是需要根据自己的业务做调整,做最适合自己系统的处理,引进缓存是为了让系统更加高性能的,如果因为引进缓存而导致系统卡顿,那就没意义了。不多说了,撸马去了。

你可能感兴趣的:(redis,缓存,redis,架构)