跳转至

缓存

思路

通过缓存将复杂的操作的结果临时存储,提升性能。

功能

  • 支持并发操作,如采用数据结构ConcurrentHashMap
  • 支持 TTL,即支持过期时间;
  • 支持 每个记录的TTL,即不同的记录可以设置不同的过期时间;
  • 支持定义缓存的大小(记录数/字节数),防止内存溢出;
  • 支持自动的数据加载(互斥),即获取不到时的处理方法;

对于分布式场景,需要通过 Redis 集中系统实现分布式缓存。

缓存清除策略

  • LRU:最近最久未使用

  • LFU:最近少频率,对于热点数据命中率更高,但需要记录额外字段信息(访问次数);

一致性问题

缓存最好设置 TTL,至少最终一致性。

缓存更新的套路 | 酷 壳 - CoolShell

更新缓存的的Design Pattern有四种:Cache aside, Read through, Write through, Write behind caching

以下也没有考虑事务,比如更新数据库成功,更新缓存失败如何处理?

更新场景,如何保证缓存和数据库的一致性

错误:

  • 缓存采用更新,不管跟数据库的先后顺序,都会出现并发时缓存跟数据库的不一致问题;
  • 线程 A 更新数据库(X = 1)
  • 线程 B 更新数据库(X = 2)
  • 线程 B 更新缓存(X = 2)
  • 线程 A 更新缓存(X = 1)

  • 先删缓存,再更新数据库

  • 两个并发操作,一个是更新操作,另一个是查询操作,更新操作删除缓存后,查询操作没有命中缓存,先把老数据读出来后放到缓存中,然后更新操作更新了数据库。于是,在缓存中的数据还是老的数据。

思路:

  • Cache Aside :先更新数据库,再删缓存
  • 写-读-写问题:第一个写导致缓存失效,读操作没有命中缓存,到数据库中取数据;第二个写操作,写完数据库后,让缓存失效,然后之前的那个读操作再把老的数据放进去,所以会造成脏数据;
    • 概率极低:发生在读缓存时缓存失效,而且并发着有一个写操作,读操作必需在写操作前进入数据库操作,而又要晚于写操作更新缓存
  • 如果删缓存失败,存在不一致:
    • 设置过期时间,等待;或者 重试;或者 事务回滚数据库
  • Read/Write Through:缓存代理数据库的更新操作,对外表现为原子性操作(缓存更新和数据库更新)
  • Write behind(Write back):同步更新缓存,异步更新数据库
  • 可能会丢失更新

因此,一般采用先更新数据库,再删缓存的策略:事务性保证,如果更新缓存失败

  • 异步重试(引入消息队列,消息队列跟缓存同时失败概率不大);
  • 缓存过期时间(一定时间的数据不一致性);

缓存雪崩

某个集中的时间,缓存全部失效,查询全部到数据库,压力大

  • 根因:
  • 集中创建缓存,相同的缓存失效时间;

  • 缓存服务宕机,导致全部访问数据库;

  • 解决:

  • 创建不同的缓存失效时间
  • 设置熔断机制,针对数据库流量进行限制;

缓存击穿

缓存中没有而数据库中有(一般缓存到期),访问量大的情况下,同时去读取数据库

  • 根因:热点 Key 缓存到期;
  • 解决方案:
  • 针对热点 Key,设置永不过期或者自动续期
  • 加锁,分布式锁;(或者每个实例加锁,因为实例数(进程数)不会很多)

缓存穿透

数据中不存在的数据,每次访问都会直接访问数据库

  • 根因:无法对数据库不存在的key做判断;
  • 解决:
  • 对于不在数据库中的情况,也进行缓存,过期时间设置短点;

    • 无法防止恶意攻击
  • 参数校验,过滤无效值;

  • 布隆过滤器:存在少量误判,以及更新同步(分布式)的问题;

缓存系统

选型

  • 如果只是本地简单、少量缓存数据使用的,选择Caffeine
  • 如果本地缓存数据量较大、内存不足需要使用磁盘缓存的,选择EhCache
  • 如果是大型分布式多节点系统,业务对缓存使用较为重度,且各个节点需要依赖并频繁操作同一个缓存,选择Redis

单机Caffine库

W-TinyLFU 算法 回收策略,提供了一个近乎最佳的命中率

  • 支持基于容量回收、定时回收和基于引用回收方式;
  • 定时回收支持按照写入时间或者按照访问时间回收;
  • 监控缓存加载/命中情况;

分布式Redis

分布式缓存,Redis 作为单独的服务:

  • 支持多种数据结构;
  • 不同Key设置不同的过期时间;

更多见 redis 简介和用法

混合Ehcache

  • 支持堆内、堆外、磁盘、集群等多级缓存;
  • 堆内有GC,堆外有序列化/反序列化,磁盘有IO
  • 支持配置缓存容量(记录数、字节数);
  • 支持使用磁盘来对缓存内容进行持久化保存

限制:

  • 多级缓存,按照堆内缓存 < 堆外缓存 < 磁盘缓存 < 集群缓存的顺序进行组合;
  • 多级缓存中的容量设定必须遵循堆内缓存 < 堆外缓存 < 磁盘缓存 < 集群缓存的原则;

  • 多级缓存中不允许磁盘缓存与集群缓存同时出现;

集群模式

  • 将多实例节点互相连接,当更新时进行节点间同步;
  • 支持 RMI 组播,JMS消息,Cache Server,JGroup等方式;

缓存库

OSChina J2Cache(两级缓存)

第一级缓存使用内存(同时支持 Ehcache 2.x、Ehcache 3.x 和 Caffeine);

第二级缓存使用 Redis(推荐)/Memcached。

数据读取: L1 -> L2 -> DB

数据更新

  • 从数据库中读取最新数据,依次更新 L1 -> L2 ,发送广播清除某个缓存信息
  • 接收到广播(手工清除缓存 & 一级缓存自动失效),从 L1 中清除指定的缓存信息

Region: 不同的数据会有不同的 TTL 策略:

  • 因为Java 本地缓存不可以针对不同的 key 设置不同的 TTL 时间;
  • 不同的 region 来存放不同的缓存数据,指定数据量和TTL时间;

Ali JetCache(两级缓存)

基于Java的缓存系统封装,提供统一的API和注解来简化缓存的使用。

原生的支持TTL、两级缓存、分布式自动刷新,还提供了Cache接口用于手工缓存操作。

当前有四个实现:

  • 分布式:RedisCache、TairCache(not opensource);
  • 单机:CaffeineCache(in memory) , LinkedHashMapCache(in memory);

支持对一组 Key 设置不同的过期时间(每个 @Cached 注解可以设置不同的 TTL)。

Spring CacheManager

示例: spring cache

底层的缓存框架支持:

  • redis:引入 spring redis start 默认集成 lettuce 不适用 Jedis;
  • caffine, e2cache 等;
  • composite cache:复合 cache,使用多级缓存;

CacheManager

  • 按照 cache name 区分 Cache,不同的 Cache 可以有不同的配置(如 TTL 等);

  • 不支持针对每个记录设置不同的过期时间,可以对一组值设置不同的过期时间

  • 即使用相关注解时,指定使用哪个 cacheNamecacheManager,获取到Cache

注解

@EnableCaching

// 类上指定默认的 cache 
@CacheConfig(cacheNames = "my-redis-cache1", cacheManager = "redisCacheManager")

// 方法上定义,缓存返回结果,如果缓存存在,则不执行方法(一般用在获取方法)
@Cacheable
// 方法上定义,缓存返回结果,方法总会执行(一般用在新增方法)
@CachePut
// 删除缓存(一般用作更新或删除,保证一致性)
@CacheEvict