一、概述
缓存可以大幅改善许多高读取应用程序工作负载,降低延迟性和提高吞吐量。可以将高频数据或需要经过复杂计算的结果集放入缓存以实现低延迟访问,从而提高应用程序性能。缓存服务还具体比较快速且易于实施的优点,只需很少成本即可为系统性能提供明显改善。
尽管内存中的缓存具有诸多优势并且非常简单,但如果出现缓存一致性问题,有可能造成非常严重的影响
不同的数据和业务场景具有不同的一致性要求。例如:货币兑换汇率,在兑换结算时要求实时且确切汇率,不适合使用缓存
二、缓存与数据库一致性策略
1.CacheAside模式:先写DB再删Cache
应用程序发起读请求,如果数据在缓存不存在,则从数据库查询,并将数据写入缓存。
由读取数据的应用程序将数据按需加载到缓存中。
1.1读写DB和缓存流程
读请求:
命中缓存:直接返回Redis缓存数据,无数据库请求
未命中:从数据库查询数据,并将查询结果写入Redis
CUD写请求:
更新数据库数据
删除缓存中的数据,缓存将在下次未命中缓存时更新
1.2数据一致性问题
缓存删除之前读取数据:
假设进程A已成功更新MySQL中的值,但在删除Redis缓存数据之前,另一个进程B尝试读取相同KEY的值。然后B将命中缓存(还未删除),此时B将读取到脏数据
缓存删除失败:
假设进程A在尝试删除Redis中缓存数据异常结束(不正确的控制语句或异常),则缓存中数据将不会被正常删除,之后所有其他进程将读取到脏数据
缓存延迟写入旧值:
假设进程A读请求缓存未命中,然后A查询MySQL并获取返回结果
由于不可知原因,进程A写入Redis操作卡住一段时间
另一个进程B更新MySQL并删除Redis中的数据
之后A恢复并将其旧的查询结果保存到Redis
所有后续读请求都会读取脏数据
1.3为什么删除而不更新缓存
删除是幂等操作(对于分布式系统是一个非常好的特性)
多线程请求Update,可能由于不可预知的原因,导致数据脏写,造成数据不一致
删除简单:简单优于复杂,不需要考虑加锁
为缓存设置一个TTL(time-to-live),能实现缓存数据定期更新,删除不会打乱TTL时间
以Redis为例:HDEL删除Key时间复杂度为O(1),HSET更新为O(N)N为Field/Value(字段和值)对数量
1.4CacheAside应用场景
按需加载:不需要预先写入缓存,按需读取并写缓存
低频更新:更新频率很低的数据,如字典类数据
2.CacheAside模式:先删Cache再写DB
与“先写DB再删除”缓存相比,仅删除缓存和写DB操作先后的区别
2.1读写DB和缓存流程
读请求:
未命中:从数据库查询数据,将查询结果写入Redis,并返回
CUD写请求:
更新数据库数据
2.2数据一致性问题
删除缓存成功、写入DB延迟
写进程A删除缓存成功
写入DB由于各种原因延迟
读进程B未命中缓存(已删除),从DB读取旧数据并写入缓存
进程A写入DB成功,此时缓存数据已过时
在下次写操作之前不会再被删除
2.3推荐使用“先删Cache后写DB”
缓存效率高于RDBMS:缓存操作一般非常快,而写DB出问题的可能性更大
可异步删除缓存:可使用异步线程来删除缓存,如引入MQ更能确保缓存能正常删除
3.双删缓存和延迟双删缓存
3.1读写DB和缓存流程
读请求:
未命中:从数据库查询数据,将查询结果写入Redis
CUD写请求:
删除缓存中的数据
更新数据库数据
删除缓存中的数据或延迟一定时间再删除一次缓存
3.2延迟删除
Delay而非Sleep并不是将每次写请求进程都强制sleep(N毫秒),而是另外再新建一个线程或队列来实现延迟删除。
使用java.util.concurrent.DelayQueue来实现延迟删除
java.util.concurrent.ScheduledExecutorService实现定时删除
使用MQ实现DelayQueue功能
使用RedisZSet特性实现延迟队列
3.2DoubleDelete双删总结
很大程度上能解决缓存删除失败问题
能有效避免删除缓存后被其它进程写入脏数据的问题
4.同步双写
4.1读缓存流程
未命中:通知CacheService从数据库读取数据并写入缓存
4.2同步双写
更新DB数据后,将数据同步写入Redis缓存
4.3同步双写数据一致性:
多线程执行顺序问题(线程同步):
线程A和线程B开始并发更新DB和Cache
线程A修改value=1,线程B修改value=2
线程A更新缓存延迟,线程B完成缓存更新:缓存value=2
线程A完成缓存更新:缓存value=1
4.4同步双写优化
在同一事务中同步将数据写入DB和Cache,在更新Cache后,提交更新DB事务,由MySQL行锁来确保数据一致性。
注意:
DB和分布式Cache事务需要自己实现
同步写可能造成性能问题
#开启事务begin(transaction)tryupdateDB(user);#更新DBupdateRedis(key);#更新缓存