自底向上看缓存一致性
AI摘要: 本文介绍了Redis缓存一致性问题及解决方案。在分布式系统设计中,缓存与数据库的同步是保证性能和数据一致性的关键。
两个”设备”之间的速度鸿沟是计算机中永恒不变的话题,无非就是加缓存干干干!局部性原理是计算机科学中最伟大的定理。为了深入了解计算机内部各种地方的缓存问题,这里通过自低向上的角度对三种不同的案例来对比分析
-
CPU级别:CPU —>三级缓存(L1~3)—> 内存;
-
数据库内部级别:内存 —>buffer pool —>磁盘;
-
数据库外部级别:用户 —> redis —> 数据库;
CPU和内存之间的缓存:三级缓存
CPU通过L1、L2、L3三级缓存来解决内存速度慢的问题。CPU可以通过两种方式来写入内存:写回和写直达。
-
写直达:顾名思义,写入缓存中,然后在写入内存中,简单直接,但是效率偏低
-
写回:CPU写入缓存中,但是不写入内存中,并将数据标记为Dirty,表示此时缓存和内存数据不一致。只有当缓存被替换的时候,才一并将缓存中的数据写入内存中。这样大大减少了写内存的次数,提高了效率
在多核CPU中,上述的缓存一致性问题将会变得更加复杂。多核CPU中。每个核都有自己独有的L1、L2缓存,只有L3缓存是所有的核共享。因此不同核之间的L1、L2缓存的一致性无法保证。为了解决这个问题,CPU采用两个机制来同步不同核的L1、L2缓存:写传播和事务的串行化
-
写传播:将数据的写操作结果传播到其他核的缓存上
-
事务的串行化:当核1修改变量i为100时候,会通过传播告知核3和核4,如果此刻核2同时修改i为200,那么也会传播到核3和核4.。此时核3和核4得到的变量i的值将是不确定的(可能是100、也可能是200)。因此需要实现事务的串行化、当两个核都需要修改一个变量时候,必须按照一定顺序实现,为此,可以引入锁的概念,只有拿到锁的核才有资格修改变量,没锁的核只能等待。
写传播思想的具体实现机制为**总线嗅探,**每个核都监听总线上的一切活动。核1修改变量i为100,就往总线上发出一个传播,其他的核(2~3)如果发现自己的L1缓存上也存有变量i的值,就会立刻修改自己的L1缓存。可以看出,每次写操作都需要传播,无意加大了CPU的负担。此外,写传播并无法保证事务的串行化。
事务的串行化需要通过MESI协议来实现。所谓MSEI,即Modifed(已修改)、Exclusive(独占)、Shared(共享)、Invalidated(已失效),通过这四种状态来标记某个Cache line。
具体的状态流转参考小林Coding, 这里不再继续描述,整个过程还是非常有意思的,可以通过有限状态机来描述。
数据库内部的缓存:Buffer Pool
在MySQL的内部,同样由于磁盘IO速度缓慢,所以InnoDB引擎抄一抄L1~3缓存,引入一个Buffer Pool,如果读取的数据已经在Buffer Pool中存在,那么缓存命中,直接返回。在写入数据的时候,先写入Buffer Pool中,并标记为脏数据。
Buffer Pool与L13缓存稍有不一致的地方,L13缓存是在数据页被替换的时候才写入内存中,而MySQL为了保证数据安全,采用一个后台线程在一定时机将脏数据页写入磁盘。此外,如果在后台线程还未刷入磁盘时,mysql发生了宕机,此时数据丢失岂不是凉凉?所谓速度慢,无非加缓存,所谓宕机,无非写入磁盘。所以MySQL再加一层写入磁盘的机制(套娃),MySQL的每次更新操作都是先写入redo log中,然后在写入buffer pool中。由于redo log是顺序读写,因此写入速度非常快。
什么?你说如果redo log写入的时候,数据依然是在内存的Page Cache中,此时没有写入磁盘中,如果发生故障,内存的数据发生丢失,依然无法保证数据安全。其实,如果发生这种断电,那就是操作系统级别的故障,无解,直接找财务结下工资走人。
数据库外部的缓存:Redis
Redis常常作为MySQL的缓存,用来存储热点数据,防止MySQL压力过大而崩溃。与CPU的情况类似,Redis和数据库也会面临缓存不一致问题。
但是和CPU和MySQL内部缓存不一样的是,当修改数据时,前者是采用延迟写回策略(CPU是脏页被替换才写入内存、MySQL内部是有一个后台线程控制时机来写入磁盘),Redis虽然作为MySQL的缓存,在修改的时候采用的是写直达策略,需要同时修改Redis和MySQL,两者操作的顺序是否会导致问题?
-
先更新数据库,再更新Redis
-
先更新Redis,再更新数据库
在并发的环境下,这两种操作都可能会出现缓存和数据库不一致情况,两种情况的具体分析参考小林Coding。
稍微思考一下,如果CPU采用的也是写直达策略,那么也会遇到上面的缓存和内存不一致的问题。
如果改成删Redis和更新数据库:
-
先删Redis,再更新数据库
-
先更行数据库,再删Redis
具体分析过程参考小林Coding,写出来有点麻烦,总之结果是,「先更新数据库 + 再删除缓存」的方案,是可以保证数据一致性的。
如果是先删缓存,再更新数据库,业界的解决方案为:延迟双删策略
redis.delKey(X)
db.update(X)
Thread.sleep(N)
redis.delKey(X)
反正不管是哪种方式,redis都有过期策略来兜底,经过足够长的时间,总会更新的,和数据库之间至少是保持了弱一致性