高性能架构模式(三):高性能缓存架构,使用缓存提高性能的时候,需要注意什么?

一.缓存

 提升系统的性能,其中的一种方式提高存储系统的性能,如:数据库集群、NoSQL 存储。但是,这并不是“银弹”,在某些场景下,高性能的存储系统依旧没有处理,如:

  • 需要经过复杂运算后得出的数据,存储系统无能为力
  • 读多写少的数据,对于存储系统来说,收益较低

 缓存就是为了弥补存储系统在这些复杂业务场景下的不足,其基本原理是将可能重复使用的数据放到内存中,一次生成、多次使用,避免每次使用都去访问存储系统。
 缓存主要是利用内存来提升访问的速度,从而减轻存储系统的压力,但同时也给架构引入了更多复杂性。架构设计时如果没有针对缓存的复杂性进行处理,某些场景下甚至会导致整个系统崩溃(业务数据错乱等)。本文重点分析使用缓存时,需要注意的一些问题,以 Redis 为例。

1.缓存穿透

 百度百科对缓存穿透的定义如下:

缓存穿透是指查询一个一定不存在的数据,由于缓存是不命中时需要从数据库查询,查不到数据则不写入缓存,这将导致这个不存在的数据每次请求都要到数据库去查询,进而给数据库带来压力。

 对应到我们的实际开发中,主要有两种场分别是:

  • 存储数据不存在

因为数据确实不存在,那么每次请求就都会实际到达存储系统,给存储系统带来压力,从而发挥不了缓存的保护作用。
这种情况的处理方式,可以采用缓存空值来处理(二次请求就会走缓存,如果没有二次请求的话,压力依旧在存储系统);也可以对请求进行过滤(采用一些措施来判断请求的数据存不存在,如:redis 的 bitmap)。

  • 缓存数据生成耗费大量时间或者资源

这种情况下存储系统中存在数据,但生成缓存数据需要耗费较长时间或者耗费大量资源。如果刚好在业务访问的时候缓存失效了,那么也会出现缓存没有发挥作用,访问压力全部集中在存储系统上的情况。
有些业务数据,我们是没办法全量缓存,只能缓存部分,查询的时候,使用分页查询,这样就能获取较好的收益。这种数据,如果面对爬虫,就会不停的生成缓存数据(耗费大量时间或者资源),从而拖慢整个数据库,并且也会淘汰一部分数据(如:LRU 算法),降低缓存的收益。
这种情况,并没有很好的解决方案,不能为了应对爬虫,而把所有的数据都缓存起来,同时,爬虫的时机也不确定,通常的应对方案,有以下两种:
(1)识别爬虫然后禁止访问,但这可能会影响 SEO 和推广。
(2)做好监控,发现问题后及时处理,因为爬虫不是攻击,不会进行暴力破坏,对系统的影响是逐步的,因此,监控发现问题后是有时间进行处理的。


2.缓存雪崩

 百度百科对缓存雪崩的定义如下:

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

 当某一时刻,大量的 key 过期淘汰,不仅缓存自身要进行数据的淘汰,还会导致大量的请求直接查询存储系统,意图重新建立缓存,在高并发的情况下,会对存储系统造成巨大的性能压力。这些压力又会拖慢整个系统,严重的会造成数据库宕机,从而形成一系列连锁反应,造成整个系统崩溃。

 缓存雪崩的常见解决方法有两种:更新锁机制和后台更新机制。

  • 更新锁机制

对缓存更新操作进行加锁保护,保证只有一个线程能够进行缓存更新,未能获取更新锁的线程要么等待锁释放后重新读取缓存,要么就返回空值或者默认值。在分布式集群的业务系统要实现更新锁机制,需要用到分布式锁,如 ZooKeeper。

  • 后台更新机制

由后台线程来更新缓存,而不是由业务线程来更新缓存,缓存本身的有效期设置为永久,后台线程定时更新缓存。后台更新机制有不同的策略,如:
(1)定时更新 + 频繁读取
 永不过期的 key,一般是不会被删除的,但是,如果Redis的内存淘汰策略做了一些调整(LRU算法),那么,永不过期的key,也可能会被“踢掉”,因此,需要频繁的去读取对应的key,来保证业务的正确性(以为业务线程不会重建缓存)。
(2)消息队列通知机制
 业务线程发现缓存失效后,通过消息队列发送一条消息通知后台线程更新缓存。

  • key 的过期时间为某一范围内的随机值

这样的话,就在一定程度上避免了同一时刻,大量key过期的现象。


3.缓存击穿(缓存热点)

 百度百科对缓存击穿的定义如下:

缓存击穿是指热点 key 在某个时间点过期的时候,而恰好在这个时间点对这个 Key 有大量的并发请求过来,从而大量的请求打到 db。

 虽然缓存本身的性能较高,但是,对于一些热点数据,即使缓存命中,当前缓存服务器的压力依旧很大;在缓存失效的一刻,更要注意大量请求直接访问我们的存储系统。
 对于热点 key 可能造成缓存击穿的行为,常见的解决方式有:

  • 缓存副本设计

将热点 key 复制多份缓存副本,将请求分散到多个缓存服务器上,减轻缓存热点导致的单台缓存服务器压力。

  • 使用一个线程去更新缓存

(1)利用 nginx “并发回源”的机制来实现。
(2)自己实现更新锁机制(同缓存雪崩的更新锁机制)

  • 后台更新机制

同缓存雪崩后台更新机制的实现


4.缓存并发

 项目中,对于缓存的构建,一般都是先查缓存,当缓存内容不存在的时候,业务线程会重新去构建缓存,但是,这种方式能保证构建缓存的安全吗?如下以优惠卷列表暂时为例:

// 如果优惠券列表缓存存在,从缓存中查询
if (redisclient.isExist("优惠券列表Key")){
	return redisclient.lrange ("优惠券列表Key" ,0,-1) ;
 
// 缓存不存在,从DB查询优惠券列表
List couponList= getListFromDb ();

// 把DB的查询结果循环插入到缓存
for (Coupon coupon : couponList){
	redisclient.rpush ("优惠券列表Key", coupon)
}

return couponList;

 为了避免多个线程都重现去建立缓存,因此,可以使用锁来解决这个问题,如:redis 分布式锁

// 如果优惠券列表缓存存在,从缓存中查询
if (redisclient.isExist ("优惠券列表Key")){
	return redisclient.1range ("优惠券列表Rey" ,0,-1);
}
// 缓存不存在,从DB查询优惠券列表
List couponList = getListFromDb ();

// 争夺分布式锁,过期时间1秒
if (redisclient.set ("分布式锁Key", "oK", 1, NX) != null){
    try{
        //把DB的查询结果循环插入到缓存
        for (Coupon coupon : couponList) {
    	redisclient.rpush ("优惠券列表Key", coupon);
         }
     }
     finally{
        //释放分布式锁
        redisclient.remove ("分布式锁key");
    }
}

return couponList;

 其基本逻辑如下:

1.查询缓存,如果缓存存在,返回结果
2.缓存不存在,查询数据库
3.争夺分布式锁
4.成功获得锁,把查询数据库的结果循环放入缓存
5.释放分布式锁

 这样是不是就万无一失了呢?我们假定缓存不存在,刚好有两个线程 A 和 B 一后一先进入到代码块。
 第一阶段,线程A刚开始查询优惠券缓存,线程B正尝试获取分布式锁:
在这里插入图片描述
 第二阶段,由于缓存不存在,线程A开始查询数据库,线程B成功获得锁,开始更新缓存:
在这里插入图片描述
 第三阶段,线程A尝试获得分布式锁,而线程B已经释放分布式锁:
在这里插入图片描述
 第四阶段,线程A获得了锁,又一次更新缓存,而线程B已经成功返回:
在这里插入图片描述
 这样,就发生了两个线程都去重构缓存,如果,重构缓存使用的是示例中向 List 中添加元素的方式,就会造成元素重复。
 这种局面如何破解呢?其实不难,只需在线程成功得到锁以后,再次判断缓存的存在:

//如果优惠券列表缓存存在,从缓存中查询
if (redisclient.isExist ("优惠券列表Key"))
	return redisclient.1range ("优惠券列表Key" ,0,-1) ;
 
//缓存不存在,从DB查询优惠券列表
List couponList = getListFromDb ();

//争夺分布式锁,过期时间1秒
if(redisclient.set ("分布式锁key", "OK", 1, NX) != null){
    try{
        //获得分布式锁,再次判断优惠券缓存的存在
        if (redisclient.isExist("优惠券列表Key")){
    	return couponList;
         }
        //把DB的查询结果循环插入到缓存
        for (Coupon coupon : couponList) {
    	redisclient.rpush ("优惠券列表key", coupon);
         }
    } finally{
        //释放分布式锁
        redisclient.del("分布式锁key");
    }
}

return couponList;

 修改后的逻辑:

1.查询缓存,如果缓存存在,返回结果
2.缓存不存在,查询数据库
3.争夺分布式锁
4.成功获得锁,再次判断缓存的存在
5.如果缓存仍旧不存在,把查询数据库的结果循环放入缓存
6.释放分布式锁

 这种二次判断存在性的机制有一个专门的名字,叫做双重检测。该方法在线程安全的单例模式中也常常被用到。


5.缓存预热

 缓存预热指系统上线后,将相关的缓存数据直接加载到缓存系统,而不是等待用户访问才来触发缓存加载。这种方式,可以提升首次访问的速度,避免了存在某一时刻大量创建缓存给存储系统带来压力的风险。但也带来了系统启动时间变长的问题。
 缓存预热一般都是使用“后台更新机制”来实现。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值