缓存设计与优化

栏目: 数据库 · 发布时间: 4年前

内容简介:###1.LRU等算法剔除:例如 maxmemory-policy###3.主动更新:开发控制生命周期###解决方法1:缓存空对象(设置过期时间)

缓存的受益与成本

1.受益

  • 加速读写

    • CPU L1/L2/L3 Cache、浏览器缓存、Ehcache缓存数据库结果

  • 降低后端负载

    • 后端服务器通过前端缓存降低负载:业务端使用 Redis 降低后端 MySQL 的负载

2.成本

  • 数据不一致:缓存层和数据层有时间窗口不一致问题,和更新策略有关

  • 代码维护成本:多了一层缓存逻辑

  • 运维成本:例如Redis Cluster

3.使用场景

  • 降低后端负载

    • 对高消耗的SQL:join结果集/分组统计结果缓存

  • 加速请求响应

    • 利用Redis/Memcache优化IO响应时间

  • 大量写合并为批量写

    • 入计数器先Redis累加再批量写DB

缓存的更新策略

###1.LRU等算法剔除:例如 maxmemory-policy

淘汰策略 含义
noeviction 当内存使用达到阈值的时候,所有引起申请内存的命令会报错
allkeys-lru 在主键空间中,优先移除最近未使用的key
volatile-lru 在设置了过期时间的键空间中,优先移除最近未使用的key
allkeys-random 最主键空间中,随机移除某个key
volatile-random 在设置了过期的键空间中,随机移除某个key
volatile-ttl 在设置了过期时间的键空间中,具有更早过期时间的key优先移除

2.超时剔除:例如expire

###3.主动更新:开发控制生命周期

4.两条建议

  • 低一致性数据:最大内存和淘汰策略

  • 高一致性:超时剔除和主动更新结合,最大内存和淘汰策略兜底

缓存粒度问题

  • 通用性:全量属性更好

  • 占用空间:部分属性更好

  • 代码维护:表面上全量属性更好

缓存穿透优化

  • 含义:查询一个不存在的数据,由于缓存是不命中时需要从数据库查询,查不到数据则不写入缓存,这将导致这个不存在的数据每次请求都要到数据库去查询

  • 产生原因

    • 业务代码自身问题

    • 恶意攻击、爬虫

  • 发现问题

    • 业务的响应时间,受到恶意攻击时,普遍请求被打到存储层,必会引起响应时间提高,可通过监控发现。

    • 业务本身问题

    • 相关指标:总调用数、缓存层命中数、存储层命中数

###解决方法1:缓存空对象(设置过期时间)

  • 含义:当存储层查询不到数据后,往cache层中存储一个null,后期再被查询时,可以通过cache返回null。

  • 缺点

    • cache层需要存储更多的key

    • 缓存层和数据层数据“短期”不一致

  • 示例代码

public String getPassThrough(String key) {
    String cacheValue = cache.get(key);
    if( StringUtils.isEmpty(cacheValue) ) {
        String storageValue = storage.get(key);
        cache.set(key , storageValue);
        if( StringUtils.isEmpty(storageValue) ) {
            cache.expire(key , 60 * 5 );
        } 
        return storageValue;
    } else {
        return cacheValue;
    }
}

###解决方法2:布隆过滤器拦截(适合固定的数据)

  • 将所有可能存在的数据哈希到一个足够大的bitmap中,一个不存在的数据会被这个bitmap拦截掉,从而避免了对底层存储系统的查询压力

缓存雪崩优化

  • 含义:由于cache服务器承载大量的请求,当cache服务异常脱机,流量直接压向后端组件,造成级联故障。或者缓存集中在一段时间内失效,发生大量的缓存穿透

解决方法1:保证缓存高可用性

  • 做到缓存多节点、多机器、甚至多机房。

  • Redis Cluster、Redis Sentinel

  • 做二级缓存

解决方法2:依赖隔离组件为后端限流

  • 使用Hystrix做服务降级

解决方法3:提前演练(压力测试)

解决方法4:对不同的key随机设置过期时间

无底洞问题

  • 问题描述:添加机器时,客户端的性能不但没提升,反而下降

  • 问题关键点

    • 更多的机器 != 更高的性能

    • 更多的机器 = 数据增长与水平扩展

    • 批量接口需求:一次mget随着机器增多,网络节点访问次数更多。网络节点的时间复杂度由O(1) -> O(node)

  • 优化IO的方法

    • 命令本身优化:减少慢查询命令:keys、hgetall、查询bigKey并进行优化

    • 减少网络通信次数

      • mget由O(keys),升级为O(node),O(max_slow(node)) , 甚至是O(1)

    • 降低接入成本:例如客户端长连接/连接池、NIO等

热点Key的重建优化

  • 热点Key(访问量比较大) + 较长的重建时间(重建过程中的API或者接口比较费时间)

  • 导致的问题:有大量的线程会去查询数据源并重建缓存,对存储层造成了巨大的压力,响应时间会变得很慢

1.三个目标

  • 减少重建缓存的次数

  • 数据尽可能一致

  • 减少潜在危险:例如死锁、线程池大量被hang住(悬挂)

2.两种解决方案

  • 互斥锁(分布式锁)

    • 第一个线程需要重建时候,对这个Key的重建加入分布式锁,重建完成后进行解锁

    • 这个方法避免了大量的缓存重建与存储层的压力,但是还是会有大量线程的阻塞

    jedis.set(lockKey, requestId, SET_IF_NOT_EXIST, SET_WITH_EXPIRE_TIME, expireTime)
    ​
    String get(String key) {
        String SET_IF_NOT_EXIST = "NX";
        String SET_WITH_EXPIRE_TIME = "PX";
        
        String value = jedis.get(key);
        if( null == value ) {
            String lockKey = "lockKey:" + key;
            if( "OK".equals(jedis.set(lockKey , "1" , SET_IF_NOT_EXIST , 
                          SET_WITH_EXPIRE_TIME , 180)) ) {
                value = db.get(key);
                jedis.set(key , value);
                jedis.delete(lockKey);
            } else {
                Thread.sleep(50);
                get(key);
            }
        }
        return value;
    }
  • 永远不过期

    • 缓存层面:不设置过期时间(不使用expire)

    • 功能层面:为每个value添加逻辑过期时间,单发现超过逻辑过期时间后,会使用单独的线程去重建缓存。

    • 还存在一个数据不一致的情况。可以将逻辑过期时间相对实际过期时间相对减小

缓存的受益与成本

1.受益

  • 加速读写

    • CPU L1/L2/L3 Cache、浏览器缓存、Ehcache缓存数据库结果

  • 降低后端负载

    • 后端服务器通过前端缓存降低负载:业务端使用Redis降低后端MySQL的负载

2.成本

  • 数据不一致:缓存层和数据层有时间窗口不一致问题,和更新策略有关

  • 代码维护成本:多了一层缓存逻辑

  • 运维成本:例如Redis Cluster

3.使用场景

  • 降低后端负载

    • 对高消耗的SQL:join结果集/分组统计结果缓存

  • 加速请求响应

    • 利用Redis/Memcache优化IO响应时间

  • 大量写合并为批量写

    • 入计数器先Redis累加再批量写DB

缓存的更新策略

###1.LRU等算法剔除:例如 maxmemory-policy

淘汰策略 含义
noeviction 当内存使用达到阈值的时候,所有引起申请内存的命令会报错
allkeys-lru 在主键空间中,优先移除最近未使用的key
volatile-lru 在设置了过期时间的键空间中,优先移除最近未使用的key
allkeys-random 最主键空间中,随机移除某个key
volatile-random 在设置了过期的键空间中,随机移除某个key
volatile-ttl 在设置了过期时间的键空间中,具有更早过期时间的key优先移除

2.超时剔除:例如expire

###3.主动更新:开发控制生命周期

4.两条建议

  • 低一致性数据:最大内存和淘汰策略

  • 高一致性:超时剔除和主动更新结合,最大内存和淘汰策略兜底

缓存粒度问题

  • 通用性:全量属性更好

  • 占用空间:部分属性更好

  • 代码维护:表面上全量属性更好

缓存穿透优化

  • 含义:查询一个不存在的数据,由于缓存是不命中时需要从数据库查询,查不到数据则不写入缓存,这将导致这个不存在的数据每次请求都要到数据库去查询

  • 产生原因

    • 业务代码自身问题

    • 恶意攻击、爬虫

  • 发现问题

    • 业务的响应时间,受到恶意攻击时,普遍请求被打到存储层,必会引起响应时间提高,可通过监控发现。

    • 业务本身问题

    • 相关指标:总调用数、缓存层命中数、存储层命中数

###解决方法1:缓存空对象(设置过期时间)

  • 含义:当存储层查询不到数据后,往cache层中存储一个null,后期再被查询时,可以通过cache返回null。

  • 缺点

    • cache层需要存储更多的key

    • 缓存层和数据层数据“短期”不一致

  • 示例代码

public String getPassThrough(String key) {
    String cacheValue = cache.get(key);
    if( StringUtils.isEmpty(cacheValue) ) {
        String storageValue = storage.get(key);
        cache.set(key , storageValue);
        if( StringUtils.isEmpty(storageValue) ) {
            cache.expire(key , 60 * 5 );
        } 
        return storageValue;
    } else {
        return cacheValue;
    }
}

###解决方法2:布隆过滤器拦截(适合固定的数据)

  • 将所有可能存在的数据哈希到一个足够大的bitmap中,一个不存在的数据会被这个bitmap拦截掉,从而避免了对底层存储系统的查询压力

缓存雪崩优化

  • 含义:由于cache服务器承载大量的请求,当cache服务异常脱机,流量直接压向后端组件,造成级联故障。或者缓存集中在一段时间内失效,发生大量的缓存穿透

解决方法1:保证缓存高可用性

  • 做到缓存多节点、多机器、甚至多机房。

  • Redis Cluster、Redis Sentinel

  • 做二级缓存

解决方法2:依赖隔离组件为后端限流

  • 使用Hystrix做服务降级

解决方法3:提前演练(压力测试)

解决方法4:对不同的key随机设置过期时间

无底洞问题

  • 问题描述:添加机器时,客户端的性能不但没提升,反而下降

  • 问题关键点

    • 更多的机器 != 更高的性能

    • 更多的机器 = 数据增长与水平扩展

    • 批量接口需求:一次mget随着机器增多,网络节点访问次数更多。网络节点的时间复杂度由O(1) -> O(node)

  • 优化IO的方法

    • 命令本身优化:减少慢查询命令:keys、hgetall、查询bigKey并进行优化

    • 减少网络通信次数

      • mget由O(keys),升级为O(node),O(max_slow(node)) , 甚至是O(1)

    • 降低接入成本:例如客户端长连接/连接池、NIO等

热点Key的重建优化

  • 热点Key(访问量比较大) + 较长的重建时间(重建过程中的API或者接口比较费时间)

  • 导致的问题:有大量的线程会去查询数据源并重建缓存,对存储层造成了巨大的压力,响应时间会变得很慢

1.三个目标

  • 减少重建缓存的次数

  • 数据尽可能一致

  • 减少潜在危险:例如死锁、线程池大量被hang住(悬挂)

2.两种解决方案

  • 互斥锁(分布式锁)

    • 第一个线程需要重建时候,对这个Key的重建加入分布式锁,重建完成后进行解锁

    • 这个方法避免了大量的缓存重建与存储层的压力,但是还是会有大量线程的阻塞

    jedis.set(lockKey, requestId, SET_IF_NOT_EXIST, SET_WITH_EXPIRE_TIME, expireTime)
    ​
    String get(String key) {
        String SET_IF_NOT_EXIST = "NX";
        String SET_WITH_EXPIRE_TIME = "PX";
        
        String value = jedis.get(key);
        if( null == value ) {
            String lockKey = "lockKey:" + key;
            if( "OK".equals(jedis.set(lockKey , "1" , SET_IF_NOT_EXIST , 
                          SET_WITH_EXPIRE_TIME , 180)) ) {
                value = db.get(key);
                jedis.set(key , value);
                jedis.delete(lockKey);
            } else {
                Thread.sleep(50);
                get(key);
            }
        }
        return value;
    }
  • 永远不过期

    • 缓存层面:不设置过期时间(不使用expire)

    • 功能层面:为每个value添加逻辑过期时间,单发现超过逻辑过期时间后,会使用单独的线程去重建缓存。

    • 还存在一个数据不一致的情况。可以将逻辑过期时间相对实际过期时间相对减小


以上所述就是小编给大家介绍的《缓存设计与优化》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

轻松学算法

轻松学算法

赵烨 / 电子工业出版社 / 2016-7 / 69.00元

《轻松学算法——互联网算法面试宝典》共分为12 个章节,首先介绍了一些基础的数据结构,以及常用的排序算法和查找算法;其次介绍了两个稍微复杂一些的数据结构——树和图,还介绍了每种数据结构和算法的适用场景,之后是一些在工作与面试中的实际应用,以字符串、数组、查找等为例介绍了一些常见的互联网面试题及分析思路,便于读者了解这些思路,顺利地通过互联网公司的面试;最后介绍了一些常见的算法思想,便于读者对今后遇......一起来看看 《轻松学算法》 这本书的介绍吧!

JS 压缩/解压工具
JS 压缩/解压工具

在线压缩/解压 JS 代码

XML、JSON 在线转换
XML、JSON 在线转换

在线XML、JSON转换工具

HSV CMYK 转换工具
HSV CMYK 转换工具

HSV CMYK互换工具