缓存特点
- 读取速度快,减少重复计算时间,以空间换时间
- 越是读多写少,越适合使用缓存
缓存模式
Cache Aside Pattern
使用最普遍的缓存模式,应用同时操作缓存和数据库。存在低概率的数据不一致
定义
- 应用服务器读取数据先查看是否有缓存,有缓存直接取缓存,无缓存查询数据库,并将数据写入缓存
- 应用服务器更新数据,先更新数据库的数据,再删除相应的缓存数据
删除缓存失败
删除缓存失败将导致数据不一致
解决方案:缓存延时双删、异常重试再删缓存
数据不一致的情况
要获取的数据没有对应缓存
线程1 | 线程2 |
---|---|
查询数据库数据为1 | |
更新数据库的值为2 | |
删除缓存 | |
数据放入缓存,值为1 |
最终数据库的值为2,缓存的值为1
解决方案:缓存延时双删、减少缓存ttl、应用加锁(可彻底解决,但严重降低性能)、线程封锁(可彻底解决,降低性能)
为什么删除缓存,不更新缓存
- 懒加载思想,更新的缓存可能不会被使用,且有可能需要更新多个缓存数据,要额外的计算
- 可能出现并发更新缓存,导致缓存与数据库不一致
线程1 | 线程2 |
---|---|
更新数据库值为1 | |
更新数据库值为2 | |
更新缓存值为2 | |
更新缓存值为1 |
最终缓存的值为1,数据库的值为2
先删缓存,再更新数据库,数据不一致的情况
线程1 | 线程2 |
---|---|
删除缓存 | |
查询数据库数据为2 | |
数据放入缓存,值为2 | |
更新数据库的值为1 |
最终缓存的值为1,数据库的值为2
发生概率高于先更新数据库,再删缓存的不一致的情况
解决方案:减少缓存ttl、应用加锁(可彻底解决,但严重降低性能)、线程封锁(可彻底解决,降低性能)
线程封锁解决方案
- 读取数据,若数据有缓存,则直接返回缓存结果;若无缓存,则把带有数据ID,数据操作(读取数据库数据+更新缓存)的任务消息放到一个JVM队列中
- 更新数据,则把带有数据ID,数据操作(删除缓存+更新缓存)的任务消息放到上一步同一个JVM队列中
- 由一个工作线程从JVM队列中获取任务并执行任务
- 若是多机器集群,则需要保证负载均衡的策略是根据数据ID的hash作路由
- 优化点,队列中多个连续更新缓存的请求,只需要做最后一次更新缓存
线程封锁方案缺点
- 数据更新操作多,导致读取数据的请求长时间等待或超时
Read-Through
应用服务器只向缓存系统发出读数据请求,不与数据库进行交互。缓存不存在时,由缓存系统负责从数据库读取数据,并加入到缓存
Write-Through
应用服务器只向缓存系统发出写数据请求,不与数据库进行交互。缓存系统会把数据写入缓存和数据库中,且作为一个事务来完成
Write-Behind
应用服务器只向缓存系统发出写数据请求,不与数据库进行交互。缓存系统会先把数据写入缓存,一段时间后再批量更新到数据库
缓存实现
本地缓存
- HashMap或ConcurrentHashMap
- Ehcache
- Guava Cache
- Caffeine
分布式缓存
- Redis
- Memcached
MySQL与Redis对比
使用Redis作缓存,能承受更高并发访问
数据库 | 单机QPS |
---|---|
MySQL | 2000 |
Redis | 10000~100000 |