学习笔记:cache 和spring cache 技术---本地缓存-分布式缓存,缓存穿透,雪崩,和热点key的问题

title: 学习笔记:cache 和spring cache 技术—本地缓存-分布式缓存,缓存穿透,雪崩,和热点key的问题
author: Eric liu
tags: []
categories:

  • hexo

JVM缓存(本地缓存)

将数据缓存在JVM中,使用Map或者Guava的Table来保存数据。

考虑因素: 使用内存缓存时,需考虑缓存数据消耗多大内存。

优势:

  • 无网路交互,减少网络抖动对服务的影响;
  • 查询速度快;

场景:

适用数据基本不怎么变化的数据上

分布式缓存

将数据缓存在缓存中间件中,例如,redis、memcached。

推荐使用redis:

  • redis支持的数据持久化;
  • redis支持多种数据结构存储;
  • redis有专业团队维护;

redis 与memcached

(1)memcached:键值对 redis 还可以支持更多形式

(2)同样是内存数据库,redis 可以持久化,虽然redis是基于内存的存储系统,但是他本身是支持内存数据的持久化,而且主要提供两种主要的持久化策略,RDB快照和AOF日志,memcache不能

(3)性能 :redis 单线程io复用,,只有IO操作来说,性能好,也有一些计算,如排序聚合,但是计算的时候影响吞吐量

memcached 多线程,非阻塞io复用有对全局变量加锁 性能有损耗,

(4)内存管理机制不同。

memcached 是提前将 分配的内存切分成规定大小的块,然后使用的使用 用多少分配多少,有一个空闲列表进行统计。 不会用内存碎片,但是存在内存浪费

redis,会把剩余内存大小存在内存块中,Redis使用现场申请内存的方式来存储数据,会在一定程度上存在内存碎片。

在redis中,并不是所有的数据都一一直存储在内存中的,这是和memcached相比最大的一个区别
Redis只会缓存所有的key端的信息,如果redis发现内存的使用量超过某一个值,将触发swap的操作,redis根据相应的表达式计算出那些key对应value需要swap到磁盘,然后再将这些这些key对应的value持久化到磁盘中,同时再内存清除。同时由于redis将内存中的数据swap到磁盘的时候,提供服务的主线程和进行swap操作的子进程会共享这部分内存,所以如果更新需要swap的数据,redis将阻塞这个操作,直到子线程完成swap操作后才可以进行修改
https://www.cnblogs.com/hanfei-1005/p/5692455.html

(5)数据一致性 memcached 有cas 保证,redis 提供了事务

参考文档:http://blog.csdn.net/u013256816/article/details/51146314

本地缓存+分布式缓存

在jvm以及redis中均缓存数据,服务优先从jvm获取,miss后从redis中获取

具体使用:
目前ugc 使用 本地缓存 guava 和 redis 分布式缓存。

  使用spring cache的注解使用,通过名字区分指定使用哪个缓存。  g- 开头为 使用本地缓存,在guava的创建方法里判断 如果非g-开头 return null, 然后去redis 缓存中创建缓存

  针对失效时间没有做特殊处理 如失效后加锁 或失效前预处理等,因为量级不大 且没有 某时刻的大流量。

  ugc 业务中 查询固定的 和不太常改变的 使用本地缓存,文章等放在分布式缓存中。

缓存预热

在缓存初始化时,缓存中是没有任何缓存数据的,需先将数据缓存后,缓存服务才算完全启动。预热方式:

  • miss后,实时查询,然后更新缓存数据;

    1. 缺点1:多个tomcat实例同时查询数据并跟新缓存,在一段时间内缓存近似于失效;
    2. 缺点2:在高并发场景下,无法限制对数据库访问速度;
  • 通过task或接口预先加载服务,然后开启缓存服务;

    1. 优势1:在初始化服务时,限制加载数据的速度;
    2. 优势2:批量查询数据库,减少与数据库之间的网络交互;

数据一致性问题

数据库与缓存同时变更

当用户发生数据变更时,优先更新数据库数据。在更新数据库数据成功后,再更新缓存中数据。尽量避免缓存数据与数据库数据不一致的情况。

数据库先变更、缓存保持最终一致性

当数据库数据发生变更后,将变更后的key值放入到异步刷新缓存队列中。后台线程根据队列中数据,刷新缓存数据。

先刷缓存会造成

缓存失效机制及处理方式

先刷后返

缺点:

  • 如果查询的数据始终不存在,导致每次查询都请求DB,缓存作用失效。例如,id=0的supplier数据。通过在缓存中,缓存一个默认数据;
  • 在高并发的情况下,多个应用请求同时更新缓存,对缓存系统存在压力。通过加锁的方式来解决;
  • 在进行刷新的时,线程会被阻塞。在高并发的情况下,会耗尽tomcat的线程资源;

先返后刷

在高并发下,异步队列会对下游系统产生压力。例如,10K的客户端同时请求服务端,单个客户端的请求QPS是200,且每次请求的key不同及缓存中不存在数据,则每次都将key写入到数据库中,则数据库扛不住。因此,先将数据写入本地中,先本地幂等,然后在异步的写入到数据库中及缓存中。因此,每次入异步队列的时候,都查询redis中是否已经将这个key放入异步刷新队列中。如果已经放入待刷新队列中,则不再再次入队列。

#缓存穿透的问题

问题:

  • 缓存穿透是指查询一个根本不存在的数据,缓存层和存储层都不会命中,
  • 缓存穿透将导致不存在的数据每次请求都要到存储层去查询,失去了缓存保护后端存储的意义。

原因:代码问题, 爬虫,攻击,大量空命中

场景:查询某个文章,给了一个错误的文章id。一直查询不到。

方法:

缓存空对象

  • 空值做缓存,即缓存层中存了更多的键,这就需要更多的内存空间 ,可以对其设置一个较短的过期时间,让其自动清除。
  • 优点是实时性高,代码维护简单。

可以缓存到本地内存中,空对想用一个静态变量。这样不会造成 造成占用内存。

#缓存雪崩的问题

问题:热点key问题,这里指 缓存层直接失效的问题。

方法:集群,隔离组件 把重要资源隔离。让每种资源都单独运行在自己的线程池中。

而Hystrix 是解决依赖隔离的利器

热点key

问题: 热点key 缓存过期或者失效 造成段时间大量访问数据库

原因:

​ 一般使用,缓存 + 过期时间的策略,加速接口的访问速度,减少了后端负载,同时保证功能的更新

但是有两个问题:

​ (1) 这个key是一个热点key(例如一个重要的新闻,一个热门的八卦新闻等等),所以这种key访问量可能非常大。

​ (2) 缓存的构建是需要一定时间的。(可能是一个复杂计算,例如复杂的sql、多次IO、多个依赖(各种接口)等等)

从而在缓存失效的瞬间,有大量线程来构建缓存

热点key 问题解决 一:如何解决失效时 大量并发

1.加锁

(1)单机,synchronized ,spring cache 有sync 关键字

(2)分布式,分布式加锁(redis,添加一个key_mutex , “1” , 如果添加上了 相当于获取锁,如果这个存在说明其他人在用锁,获取失败

    String get(String key) {  
       String value = redis.get(key);  
       if (value  == null) {  
        if (redis.setnx(key_mutex, "1")) {  
            // 3 min timeout to avoid mutex holder crash  
            redis.expire(key_mutex, 3 * 60)  
            value = db.get(key);  
            redis.set(key, value);  
            redis.delete(key_mutex);  
        } else {  
            //其他线程休息50毫秒后重试  
            Thread.sleep(50);  
            get(key);  
        }  
      }  
    }  

缺点:挤满线程池

2.不过期

redis 设置物理不过期,

异步-逻辑过期:存值中设置timeout,如果发现timeout 过期,后台异步线程构建缓存

缺点:于性能非常友好,唯一不足的就是构建缓存时候,其余线程(非构建缓存的线程)可能访问的是老数据,

    String get(final String key) {  
            V v = redis.get(key);  
            String value = v.getValue();  
            long timeout = v.getTimeout();  
            if (v.timeout <= System.currentTimeMillis()) {  
                // 异步更新后台异常执行  
                threadPool.execute(new Runnable() {  
                    public void run() {  
                        String keyMutex = "mutex:" + key;  
                        if (redis.setnx(keyMutex, "1")) {  
                            // 3 min timeout to avoid mutex holder crash  
                            redis.expire(keyMutex, 3 * 60);  
                            String dbValue = db.get(key);  
                            redis.set(key, dbValue);  
                            redis.delete(keyMutex);  
                        }  
                    }  
                });  
            }  
            return value;  
        }  

3.过期前 刷缓存

(1)

在value内部设置1个超时值(proTimeout), proTimeout比实际的redis
timeout小。当从cache读取到proTimeout发现它已经过期时候. 然后 加分布式锁,设置一个短暂的过期时间。保证有一个线程在刷缓存,其他的正常使用。

如果这个线程刷缓存出了问题没成功,短暂的过期时间 过后 锁解开,下一个线程会机型刷缓存。

原创 伪代码

    String get(String key) {  
       String v = redis.get(key);  
       if (v  == null) {  
        if (redis.setnx(key_mutex, "1")) {  
            // 3 min timeout to avoid mutex holder crash  
            redis.expire(key_mutex, 1 * 60)  
            value = db.get(key);  
            redis.set(key, value);  
            redis.delete(key_mutex);  
        } else {  
            //其他线程休息50毫秒后重试  
            Thread.sleep(50);  
            get(key);  
        }  
      }  
     else{
       if (v.get(timeout) <= now()) {
         if (  redis.setnx(key_mutex, "1")  ) {
           // 1 min timeout to avoid mutex holder crash  
            redis.expire(key_mutex, 1 * 60)  
            value = db.get(key);  
            redis.set(key, value);  
            redis.delete(key_mutex);  
            }   
         }
         //如果没到提前的时间 或者有线程在刷,则继续取
         return v.get(value);
       }  
     }

缓存数据的设计 也可以是多值,

(不推荐,内存占用多)两个key,一个key用来存放数据,另一个用来标记失效时间

比如key是aaa,设置失效时间为30s,则另一个key为expire_aaa,失效时间为25s。

比如一个key是aaa,失效时间是30s。查询DB在1s内。

  • put数据时,设置aaa过期时间30s,设置expire_aaa过期时间25s;
  • get数据时,multiget aaa 和 expire_aaa,如果expired_aaa对应的value != null,则直接返回aaa对应的数据给用户。如果expire_aaa返回value == null,则后台启动一个任务,尝试add expire_aaa,并设置超时过间为3s。这里设置为3s是为了防止后台任务失败或者阻塞,如果这个任务执行失败,那么3秒后,如果有另外的用户访问,那么可以再次尝试查询DB。如果add执行成功,则查询DB,再更新aaa的缓存,并设置expire_aaa的超时时间为25s。

如果是冷数据,30秒都没有人访问,那么数据会过期。

如果是热门数据,一直有大流量访问,那么数据就是一直热的,而且数据一直不会过期。

(2)其他失效前 刷缓存的方式

a.定期从DB里查询数据,再刷到redis 里 有点扯,不适用 常变化的 缓存

b.缓存失效 加锁查

  • 0
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值