
目录一、策略模式1、旁路缓存模式Cache Aside Pattern2、读写穿透Read-Through/Write-Through3、异步缓存写入Write Behind二、一致性解决方案1、缓存延迟双删2、删除重试机制3、读取biglog异步删除缓存三、总结在开发中一般会使用Redis缓存一些常用的热点数据用来减少数据库IO提高系统的吞吐量先了解一下分布式系统中的一致性概念。强一致性所有节点的数据必须实时同步保证任何时候读取到的数据都是最新的。弱一致性系统允许数据暂时不一致但最终会达到一致状态。最终一致性数据更新后经过一段时间系统会逐步达到一致状态。这个时间不固定但在业务允许的范围内。双写一致性当数据同时存在于缓存Redis和数据库MySQL时两者之间数据一致那么容易出现数据一致性问题的场景是数据写入数据库未更新缓存删除缓存后数据库更新失败一、策略模式缓存可以提升性能、缓解数据库压力但是使用缓存也会导致数据不一致性的问题。有三种经典的缓存使用模式Cache-Aside PatternRead-Through/Write-throughWrite-behind1、旁路缓存模式Cache Aside PatternCache Aside Pattern的提出是为了尽可能地解决缓存与数据库的数据不一致问题流程读取操作先从缓存中读取数据缓存命中返回结果缓存未命中从DB中读取数据并将数据写入缓存。更新操作先更DB再删除缓存中的旧数据。在日常开发中一般使用了Cache Aside Pattern缓存更新策略模式以数据库为主缓存为辅public class CacheAsidePattern { private RedisService redis; private DatabaseService database; // 读取操作 public String getData(String key) { // 从缓存中获取数据 String value redis.get(key); if (value null) { // 缓存未命中从数据库获取数据 value database.get(key); if (value ! null) { // 将数据写入缓存 redis.set(key, value); } } return value; } // 更新操作 public void updateData(String key, String value) { // 更新数据库 database.update(key, value); // 删除缓存中的旧数据 redis.delete(key); } }❓:Cache-Aside在操作数据库时为什么是先操作数据库呢为什么不先操作缓存呢1、先删除缓存后数据库更新失败线程1删除缓存A由于网络问题没有操作数据库失败线程2查询A缓存无数据并把A写入缓存线程1网络堵塞结束修改数据库A为B那么此时缓存是A【旧数据】数据库是B【新数据】脏数据出现啦因此Cache-Aside缓存模式选择了先操作数据库而不是先操作缓存2、先操作数据库再删除缓存方案线程1操作数据库A更新数据为B删除缓存A线程2查询A缓存无数据并把B写入缓存这种方案下在数据库更新成功后到删除Redis缓存数据之前的这段时间中其他线程读取的数据都是旧数据等Redis删除缓存后会重新从数据库中读取最新数据同步到Redis这样可以在一定程度上保证数据的最终一致性但是在极端情况下线程1的缓存删除失败线程2读取的也就是旧数据A而不是新数据B了这种方案也就是旁路缓存模式那么Cache-Aside的优缺点就是优点简单易懂易于实现读性能高因为大部分读操作都会命中缓存缺点更新数据库后缓存可能还没删除存在短暂的不一致删除缓存后如果数据库更新失败会导致数据不一致❓:Cache-Aside在写入请求的时候为什么是删除缓存而不是更新缓存呢线程1操作数据库更新数据为A由于网络问题未更新缓存线程2操作数据库更新数据为B更新缓存为B线程1网络堵塞结束更新缓存为A那么此时缓存是A【旧数据】数据库是B【新数据】脏数据出现啦如果是删除缓存取代更新缓存则不会出现这个脏数据问题因此Cache-Aside缓存模式选择了删除缓存而不是更新缓存适应场景适用于读多写少的场景特别是对数据一致性要求不是特别高的应用2、读写穿透Read-Through/Write-ThroughRead-Through当缓存未命中时自动从数据库加载数据并写入缓存Write-Through当缓存更新时同步将数据写入数据库和旁路缓存模式很像只有写操作不太一样public class ReadWriteThroughPattern { private RedisService redis; private DatabaseService database; // Read-Through public String readThrough(String key) { // 从缓存中获取数据 String value redis.get(key); if (value null) { // 缓存未命中从数据库获取数据 value database.get(key); if (value ! null) { // 将数据写入缓存 redis.set(key, value); } } return value; } // Write-Through public void writeThrough(String key, String value) { // 将数据写入缓存 redis.set(key, value); // 同步将数据写入数据库 database.update(key, value); } }优点保证了数据的强一致性缓存和数据库的数据始终同步。读写操作都由缓存处理数据库压力较小。缺点写操作的延迟较高因为每次写入缓存时都需要同步写入数据库增加了系统的响应时间实现复杂度较高需要额外的缓存同步机制适应场景适合读多写多、且对数据一致性要求较高的场景3、异步缓存写入Write Behind异步缓存就是缓存更新后异步批量写入数据库。这种策略适用于可以容忍一定数据不一致的高性能场景示例代码public class WriteBehindPattern { private RedisService redis; private DatabaseService database; private UpdateQueue updateQueue; // 异步缓存写入 public void writeBehind(String key, String value) { // 将数据写入缓存 redis.set(key, value); // 异步将数据写入数据库 asyncDatabaseUpdate(key, value); } private void asyncDatabaseUpdate(String key, String value) { // 异步操作将更新请求放入队列 updateQueue.add(new UpdateTask(key, value)); } }优点写操作的性能非常高因为只需更新缓存数据库更新是异步进行的适用于对写操作性能要求较高的场景缺点存在数据不一致的风险缓存更新后数据库可能还未更新。实现复杂度较高需要处理异步操作中的异常和重试适应场景大批量数据读取允许短期数据不一致写密集型场景二、一致性解决方案缓存系统适用的场景就是非强一致性的场景它属于CAP中的APCAP理论指的是在一个分布式系统中 Consistency一致性、 Availability可用性、Partition tolerance分区容错性三者不可得兼。没办法做到数据库与缓存绝对的一致性但通过一些方案优化处理是可以保证弱一致性最终一致性的1、缓存延迟双删流程先删除缓存再更新数据库休眠一会比如1秒再次删除缓存但休眠的时间内可能有脏数据且第二次删除也可能失败导致的数据不一致问题延迟双删策略只能保证最终的一致性不能保证强一致性。由于对Redis的操作和Mysql的操作不是原子性操作所以如果想保证数据的强一致性就需要加锁控制如下图所示加锁之后势必会带来系统的吞吐量的下降所以需要衡量利弊来确定是否使用加锁方案优化删除失败就多删除几次呀保证删除缓存成功就可以了所以可以引入删除缓存重试机制2、删除重试机制删除缓存失败则将这些key放入到消息队列中消费消息队列的消息获取要删除的key重试删除缓存操作3、读取biglog异步删除缓存重试删除缓存机制还可以吧就是会造成好多业务代码入侵。方案优化通过数据库的binlog来异步淘汰key以MySQL为例通过canal监听binlog日志感知数据的变动后canal客户端执行删除Redis缓存数据如果缓存数据删除失败那么发送一条MQ消息让canal客户端继续执行删除操作这样可以保证数据的最终一致性但是这样也增加了系统的复杂性三、总结1实际开发中一般使用使用了Cache Aside Pattern缓存更新策略模式此方案最大程度上保证了数据的一致性并且实现也最简单2无论是先操作数据库再删除缓存还是先删除缓存再操作数据库都有可能会出现删除缓存失败的情况所以需要加入删除重试机制3如果想要Redis和Mysql的数据强一致性可以考虑使用加锁的方式实现