缓存模式与实现

缓存特点

  • 读取速度快,减少重复计算时间,以空间换时间
  • 越是读多写少,越适合使用缓存

缓存模式

Cache Aside Pattern

使用最普遍的缓存模式,应用同时操作缓存和数据库。存在低概率的数据不一致

Spring声明式缓存整合Redis#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