缓存
思路
通过缓存将复杂的操作的结果临时存储,提升性能。
功能
- 支持并发操作,如采用数据结构
ConcurrentHashMap
; - 支持 TTL,即支持过期时间;
- 支持 每个记录的TTL,即不同的记录可以设置不同的过期时间;
- 支持定义缓存的大小(记录数/字节数),防止内存溢出;
- 支持自动的数据加载(互斥),即获取不到时的处理方法;
对于分布式场景,需要通过 Redis 集中系统实现分布式缓存。
缓存清除策略
-
LRU:最近最久未使用
-
LFU:最近少频率,对于热点数据命中率更高,但需要记录额外字段信息(访问次数);
一致性问题
缓存最好设置 TTL,至少最终一致性。
更新缓存的的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 等);
-
不支持针对每个记录设置不同的过期时间,可以对一组值设置不同的过期时间;
- 即使用相关注解时,指定使用哪个
cacheName
和cacheManager
,获取到Cache