diff --git "a/source/_posts/\346\212\200\346\234\257\346\216\242\347\264\242\344\271\213\346\227\205\343\201\256\347\254\254\344\270\200\347\257\207-\347\274\223\345\255\230\345\222\214DB\347\232\204\346\225\260\346\215\256\344\270\200\350\207\264.md" "b/source/_posts/\346\212\200\346\234\257\346\216\242\347\264\242\344\271\213\346\227\205\343\201\256\347\254\254\344\270\200\347\257\207-\347\274\223\345\255\230\345\222\214DB\347\232\204\346\225\260\346\215\256\344\270\200\350\207\264.md" new file mode 100644 index 0000000..100b8c6 --- /dev/null +++ "b/source/_posts/\346\212\200\346\234\257\346\216\242\347\264\242\344\271\213\346\227\205\343\201\256\347\254\254\344\270\200\347\257\207-\347\274\223\345\255\230\345\222\214DB\347\232\204\346\225\260\346\215\256\344\270\200\350\207\264.md" @@ -0,0 +1,78 @@ +--- +layout: techtrek1 +title: 技术探索之旅の第一篇-缓存和DB的数据一致 +date: 2023-08-18 21:04:13 +tags: [技术探索之旅, 连载] +--- + +本文主要连载`技术探索之旅`系列。因为目前在做有关社交方面的功能需求,出现了在并发情况下用户更新文章之后而`缓存`没有和数据一致的情况,这一篇写一下我的解决方案和两种解决思路。 + + + +# 缓存和DB的数据一致 + +先说一下`缓存和DB的数据不一致的场景` + +通常情况下这种情况是在并发的情况下会出现,假定以下场景为并发下出现 + +1. A线程请求更新目标数据 +2. B线程获取目标数据 +3. A线程在还没有更新数据到DB之前B线程数据已经从缓存拿到了数据,从而导致短暂性数据不一致(在A线程完成更新操作之后可以保持一致) +4. A线程在还没有更新数据到Redis之前先删除了Redis的数据,而B线程此时没拿到缓存数据,进而从DB拿到了数据(旧数据),在B线程拿到DB数据时再写入到缓存,从而导致数据在过期之前或下次更新之前存在长期不一致 + +先说方案,我目前一共是想到两种方案,分别是`两步删除(两步延迟删除)`以及`先改后删(Binlog Canal补偿)`也就是主从 这两种,关于[阿里开源的Binlog Canal请点此参考](https://github.com/alibaba/canal)这里指的是最终一致,而并非强一致,我们通常在选用缓存技术的时候是为了增强系统的吞吐量,故而要保证强一致就需要确定他们的原子性,这样反而适得其反,所以这里不将这种方案计入其中。 + +上面的问题反映了一个问题就是,因为A线程要写DB的情况存在,所以导致B线程的吞吐量(运行效率)一定会更快于A线程,所以才会出现脏数据(数据不一致)的情况,而我觉得如果想解决这个问题的方案就是最后一次写入缓存的数据一定是当前DB的最新数据 + +## 两步删除(两步延迟删除) + +先说两步删除,这里先不说两步延迟删除,这个我认为是一种容灾手段,其目的并不是解决数据不一致的核心逻辑。 + +### 两步删除 + +#### 实现逻辑 + +1. 在A线程请求更新目标数据之前先把缓存中的数据删除 +2. 更新DB的数据 +3. 再次删除缓存中的数据(防止A线程在执行第二步期间出现B线程并发获取缓存数据时,因为缓存在第一步被删除,这时候会从DB获取,然后直接更新到缓存,这样就出现了缓存不一致的情况) +4. 等到下一次B线程请求的时候缓存中没有数据这时候会去DB中获取数据,然后将其写入到DB进而保证数据的一致性 + +#### 结论 + +##### 优点 + +- 可以保证数据的最终一致性(先排除极端情况,一会儿在两步延迟删除中会说到极端情况的解决方案) + +##### 缺点 + +- 在并发的情况下A如果还没有更新完DB的数据并且再次删除缓存数据时会出现短暂的读到脏数据的问题 + +### 两步延迟删除 + +这里为什么是两步延迟删除是因为,我们在第二次删除缓存中的数据时可能会出现比如抖动,进而请求超时数据没有删除,或者删除失败,删除报错的场景 + +#### 实现逻辑 + +1. 在A线程请求更新目标数据之前先把缓存中的数据删除 +2. 更新DB的数据 +3. 发送延迟队列(具体延时时间需要根据更新逻辑的业务复杂度决定)异步再次删除缓存中的数据 +4. 等到下一次B线程请求的时候缓存中没有数据这时候会去DB中获取数据(即使在并发情况下出现读到旧数据的情况,我们有延迟队列来保证最终一致),然后将其写入到DB进而保证数据的一致性 + +#### 总结 + +##### 优点 + +- 可以保证数据的最终一致性(有延迟队列的兜底) +- 异步处理第二次删除,相比于`二步删除`在写操作时性能要好一点 + +##### 缺点 + +- 在并发的情况下A如果还没有更新完DB的数据并且再次删除缓存数据时会出现短暂的读到脏数据的问题 +- 延迟队列如果时间间隔太长,那么这个最终一致的时间也就更长 +- 因为增加了消息队列中间件,系统变的更复杂了,故而可维护性更低了 +- 通常情况下我们为了更好的维护消息队列的消费者,导致消息队列这里有删除逻辑,而我们在更新DB的时候也有删除缓存的逻辑,出现了代码耦合 +- 需要处理消息丢失、消息积压的相关问题,这里即使消息被重复消费也无所谓,关于消息队列的相关问题,下次记录一下(下次一定) + +其实我觉得到这里其实差不多了,只是我感觉还有别的办法,一起在这里记录一下。 + +## 先改后删(Binlog Canal补偿) \ No newline at end of file