最近项目上用到二级缓存,加上之前也使用过J2Cache之类的二级缓存开源框架,因此对二级缓存做一个简单的总结。
- 二级缓存解决什么问题?
目前缓存的解决方案一般有两种:
- 内存缓存(如 Ehcache) —— 速度快,进程内可用
- 集中式缓存(如 Redis)—— 可同时为多节点提供服务
二级缓存主要解决:
- 使用内存缓存时,一旦应用重启后,由于缓存数据丢失,缓存雪崩,给数据库造成巨大压力,导致应用堵塞
- 使用内存缓存时,多个应用节点无法共享缓存数据
- 使用集中式缓存,由于大量的数据通过缓存获取,导致缓存服务的数据吞吐量太大,带宽跑满。现象就是 Redis 服务负载不高,但是由于机器网卡带宽跑满,导致数据读取非常慢
二级缓存结构:
L1: 进程内缓存(caffeine\ehcache)
L2: Redis/Memcached 集中式缓存
数据读取:
- 读取顺序 -> L1 -> L2 -> DB, 通常采用Read Through模式
缓存中数据不存在时,从DB中加载数据时需要加锁,避免缓存击穿。
缓存/DB中都不存在时,需要缓存空数据设置失效时间,避免缓存穿透
- 数据更新:
更新数据库,再更新缓存
多线程场景下容易造成双写数据不一致,如下步骤 :
1.线程A先发起⼀个写操作,第⼀步先更新数据库 2. 线程B再发起⼀个写操作,第⼆步更新了数据库 3. 由于⽹络等原因,线程B先更新了缓存 4. 线程A更新缓存。
缓存保存的是A的数据(⽼数据),数据库保存的是B的数据(新数据)
更新缓存相对于删除缓存还有两点劣势:
如果你写⼊的缓存值,是经过复杂计算才得到的话。更新缓存频率⾼的话,就浪费性能。
在写多读少的情况下,数据很多时候还没被读取到⼜被更新了,这也浪费了性能
删除缓存,更新数据库
1. 线程A发起⼀个写操作,第⼀步del cache 2. 此时线程B发起⼀个读操作,cache miss 3. 线程B继续读DB,读出来⼀个⽼数据 4. 然后线程B把⽼数据设置⼊cache 5. 线程A写⼊DB最新的数据
缓存和数据库的数据不⼀致了。缓存保存的是⽼数据,数 据库保存的是新数据
- 方式三:更新数据库,删除缓存
删除缓存时,需要广播通知删除对应的业务节点删除对应的L1本地缓存。
删除缓存采用常用的ZK监听机制/Redis的订阅机制/消息队列来通知业务节点删除L1缓存。
ZK的监听机制存在弊端,当发生Zk于业务节点发生断连时,没能及时通知到业务侧 【之前的系统就出过生产故障】,而且多引入了业务组件。
开源软件:https://gitee.com/ld/J2Cache