浅谈Redis跟MySQL的双写问题解决方案
项目中有遇到这个问题,跟mysql中的数据不一致,研究一番发现这里面细节并不简单,特此记录一下。
写在前面
严格意义上任何非原子操作都不可能保证一致性,除非用阻塞读写实现强一致性,所以缓存架构我们追求的目标是最终一致性。
缓存就是通过牺牲强一致性来提高性能的。
这是由cap理论决定的。缓存系统适用的场景就是非强一致性的场景,它属于cap中的ap。
以下3 种缓存读写策略各有优劣,不存在最佳。
三种读写缓存策略
cache-aside pattern(旁路缓存模式)
cache-aside pattern,即旁路缓存模式,它的提出是为了尽可能地解决缓存与数据库的数据不一致问题。
读 :从缓存读取数据,读到直接返回。如果读取不到的话,从数据库加载,写入缓存后,再返回响应。
写:更新的时候,先更新数据库,然后再删除缓存。
read-through/write-through(读写穿透)
read/write through pattern 中服务端把 cache 视为主要数据存储,从中读取数据并将数据写入其中。cache 服务负责将此数据读取和写入 db,从而减轻了应用程序的职责。
因为我们经常使用的分布式缓存 redis 并没有提供 cache 将数据写入db的功能,所以使用并不多。
写:先查 cache,cache 中不存在,直接更新 db。cache 中存在,则先更新 cache,然后 cache 服务自己更新 db(同步更新 cache和db)。
读:从 cache 中读取数据,读取到就直接返回 。读取不到的话,先从 db 加载,写入到 cache 后返回响应。
write behind pattern(异步缓存写入)
write behind pattern 和 read/write through pattern 很相似,两者都是由 cache 服务来负责 cache 和 db 的读写。
但是,两个又有很大的不同:read/write through 是同步更新 cache 和 db,而 write behind caching 则是只更新缓存,不直接更新 db,而是改为异步批量的方式来更新 db。
很明显,这种方式对数据一致性带来了更大的挑战,比如cache数据可能还没异步更新db的话,cache服务可能就挂掉了,反而会带来更大的灾难。
这种策略在我们平时开发过程中也非常非常少见,但是不代表它的应用场景少,比如消息队列中消息的异步写入磁盘、mysql 的 innodb buffer pool 机制都用到了这种策略。
write behind pattern 下 db 的写性能非常高,非常适合一些数据经常变化又对数据一致性要求没那么高的场景,比如浏览量、点赞量。
旁路缓存模式解析
cache aside pattern 的一些疑问
旁路缓存模式是我们平时中使用最多的。下面根据上面介绍的旁路缓存模式,我们可以有以下几个疑问。
为什么写操作是删除缓存,而不是更新缓存
答:线程a先发起一个写操作,第一步先更新数据库。线程b再发起一个写操作,第二步更新了数据库,由于网络等原因,线程b先更新了缓存,线程a更新缓存。
这时候,缓存保存的是a的数据(老数据),数据库保存的是b的数据(新数据),数据不一致了,脏数据出现啦。如果是删除缓存取代更新缓存则不会出现这个脏数据问题。
实际上要写操作的时候更新缓存也是可以的,不过我们需要加一个锁/分布式锁来保证更新cache的时候不存在线程安全问题。
在写数据的过程中,为什么要先更新db在删除缓存
答:比如说请求1 是写操作,要是先删除缓存a,请求2是读操作,先读缓存a,发现缓存被删除了(被请求1删除了),然后去读数据库,但是此时请求1还没来得及把数据及时更新,那么请求2读的就是旧数据,并且请求2还会把读到的旧数据放到缓存中,造成了数据的不一致。
其实要先删缓存,再更新数据库也是可以,如采用延时双删策略
休眠1秒,再次淘汰缓存 这么做,可以将1秒内所造成的缓存脏数据,再次删除。不一定是1秒,看你业务决定的,不过不推荐这种做法,因为在这1秒内可能发生因素很多,它的不确定性太大。
在写数据的过程中,先更新db,后删除cache就没有问题了么?
答: 理论上来说还是可能会出现数据不一致性的问题,不过概率非常小。
假设这会有两个请求,一个请求a做查询操作,一个请求b做更新操作,那么会有如下情形产生
(1)缓存刚好失效
(2)请求a查询数据库,得一个旧值
(3)请求b将新值写入数据库
(4)请求b删除缓存
(5)请求a将查到的旧值写入缓存 ok,如果发生上述情况,确实是会发生脏数据。
然而,发生这种情况的概率并不高
发生上述情况有一个先天性条件,就是步骤(3)的写数据库操作比步骤(2)的读数据库操作耗时更短,才有可能使得步骤(4)先于步骤(5)。
可是,仔细想想,数据库的读操作的速度远快于写操作的(不然做读写分离干嘛,做读写分离的意义就是因为读操作比较快,耗资源少),因此步骤(3)耗时比步骤(2)更短,这一情形很难出现。
还有其他造成不一致的原因么?
答: 如果删除缓存过程中失败了就会造成不一致问题
如何解决?
使用canal去订阅数据库的binlog,获得需要操作的数据。另起一个程序,获得这个订阅程序传来的信息,进行删除缓存操作。
cache aside pattern 的缺陷
缺陷1:首次请求数据一定不在 cache 的问题
解决办法:可以将热点数据提前放入cache 中。
缺陷2:写操作比较频繁的话导致cache中的数据会被频繁被删除,这样会影响缓存命中率 。
数据库和缓存数据强一致场景 :更新db的时候同样更新cache,不过我们需要加一个锁/分布式锁来保证更新cache的时候不存在线程安全问题。可以短暂地允许数据库和缓存数据不一致的场景 :更新db的时候同样更新cache,但是给缓存加一个比较短的过期时间,这样的话就可以保证即使数据不一致的话影响也比较小。
到此这篇关于浅谈redis跟mysql的双写问题解决方案的文章就介绍到这了,更多相关redis mysql双写内容请搜索以前的文章或继续浏览下面的相关文章希望大家以后多多支持!
上一篇: 使用CSS连接数据库的方式