写请求来了,要更新数据库和缓存,一前一后更新,就可能导致缓存和DB中的数据在一段时间内不一致。
你只要用缓存,就可能会涉及到缓存与数据库双存储双写,你只要是双写,就一定会有数据一致性的问题,那么你如何解决一致性问题?
一般来说,就是如果你的系统不是严格要求缓存+数据库必须一致性的话,缓存可以稍微的跟数据库偶尔有不一致的情况,
如果是强一致性,读请求和写请求串行化,串到一个内存队列里去,这样就可以保证一定不会出现不一致的情况(效率极低。)
串行化之后,就会导致系统的吞吐量会大幅度的降低,用比正常情况下多几倍的机器去支撑线上的一个请求。
还有一种方式就是可能会暂时产生不一致的情况,但是发生的几率特别小,就是先更新数据库,然后再删除缓存。
这种情况不存在并发问题么?
不是的。假设这会有两个请求,一个请求A做查询操作,一个请求B做更新操作,那么会有如下情形产生
(1)缓存刚好失效
(2)请求A查询数据库,得一个旧值
(3)请求B将新值写入数据库
(4)请求B删除缓存
(5)请求A将查到的旧值写入缓存
ok,如果发生上述情况,确实是会发生脏数据。
然而,发生这种情况的概率又有多少呢?
发生上述情况有一个先天性条件,就是步骤(3)的写数据库操作比步骤(2)的读数据库操作耗时更短,才有可能使得步骤(4)先于步骤(5)。可是,大家想想,数据库的读操作的速度远快于写操作的(不然做读写分离干嘛,做读写分离的意义就是因为读操作比较快,耗资源少),因此步骤(3)耗时比步骤(2)更短,这一情形很难出现。
如何解决上述并发问题?
首先,给缓存设有效时间是一种方案。其次,采用异步延时删除策略,保证读请求完成以后,再进行删除操作。
转https://www.cnblogs.com/liufei1983/p/12103523.html
=======================
2018年12月17日 21:55:48 mikechen优知 阅读数:866
一、需求起因
在高并发的业务场景下,数据库大多数情况都是用户并发访问最薄弱的环节。所以,就需要使用redis做一个缓冲操作,让请求先访问到redis,而不是直接访问MySQL等数据库。
这个业务场景,主要是解决读数据从Redis缓存,一般都是按照下图的流程来进行业务操作。
读取缓存步骤一般没有什么问题,但是一旦涉及到数据更新:数据库和缓存更新,就容易出现缓存(Redis)和数据库(MySQL)间的数据一致性问题。
不管是先写MySQL数据库,再删除Redis缓存;还是先删除缓存,再写库,都有可能出现数据不一致的情况。举一个例子:
1.如果删除了缓存Redis,还没有来得及写库MySQL,另一个线程就来读取,发现缓存为空,则去数据库中读取数据写入缓存,此时缓存中为脏数据。
2.如果先写了库,在删除缓存前,写库的线程宕机了,没有删除掉缓存,则也会出现数据不一致情况。
因为写和读是并发的,没法保证顺序,就会出现缓存和数据库的数据不一致的问题。
如来解决?这里给出两个解决方案,先易后难,结合业务和技术代价选择使用。
二、缓存和数据库一致性解决方案
1.第一种方案:采用延时双删策略
在写库前后都进行redis.del(key)操作,并且设定合理的超时时间。
伪代码如下:
public void write(String key,Object data){ redis.delKey(key);
db.updateData(data); Thread.sleep(500); redis.delKey(key); }
具体的步骤就是:
那么,这个500毫秒怎么确定的,具体该休眠多久呢?
需要评估自己的项目的读数据业务逻辑的耗时。这么做的目的,就是确保读请求结束,写请求可以删除读请求造成的缓存脏数据。
当然这种策略还要考虑redis和数据库主从同步的耗时。最后的的写数据的休眠时间:则在读数据业务逻辑的耗时基础上,加几百ms即可。比如:休眠1秒。
设置缓存过期时间
从理论上来说,给缓存设置过期时间,是保证最终一致性的解决方案。所有的写操作以数据库为准,只要到达缓存过期时间,则后面的读请求自然会从数据库中读取新值然后回填缓存。
该方案的弊端
结合双删策略+缓存超时设置,这样最差的情况就是在超时时间内数据存在不一致,而且又增加了写请求的耗时。
2、第二种方案:异步更新缓存(基于订阅binlog的同步机制)
技术整体思路:
MySQL binlog增量订阅消费+消息队列+增量数据更新到redis
读Redis:热数据基本都在Redis
写MySQL:增删改都是操作MySQL
更新Redis数据:MySQ的数据操作binlog,来更新到Redis
Redis更新
1)数据操作主要分为两大块:
一个是全量(将全部数据一次写入到redis)
一个是增量(实时更新)
这里说的是增量,指的是mysql的update、insert、delate变更数据。
2)读取binlog后分析 ,利用消息队列,推送更新各台的redis缓存数据。
这样一旦MySQL中产生了新的写入、更新、删除等操作,就可以把binlog相关的消息推送至Redis,Redis再根据binlog中的记录,对Redis进行更新。
其实这种机制,很类似MySQL的主从备份机制,因为MySQL的主备也是通过binlog来实现的数据一致性。
这里可以结合使用canal(阿里的一款开源框架),通过该框架可以对MySQL的binlog进行订阅,而canal正是模仿了mysql的slave数据库的备份请求,使得Redis的数据更新达到了相同的效果。
当然,这里的消息推送工具你也可以采用别的第三方:kafka、rabbitMQ等来实现推送更新Redis。
以上就是Redis和MySQL数据一致性详解。
================
Redis 3个月前 (2020-04-03) 652 浏览
数据库和缓存读写顺序?
在了解缓存数据一致之前,我们需要先了解数据库和缓存的读写顺序。
最经典的缓存+数据库读写的模式,就是 Cache Aside Pattern。
命中:程序先从缓存中读取数据,如果命中,则直接返回
失效:程序先从缓存中读取数据,如果没有命中,则从数据库中读取,成功之后将数据放到缓存中
更新:程序先更新数据库,然后再删除缓存
关于更新操作,其实问题比较多。通常疑惑的就几种:
1、我们不考虑更新缓存的原因:首先,并发情况下也可能出现数据不是最新的情况。其次,不确定要更新的这个缓存项是否会被经常读取,假设每次更新数据库都会导致缓存的更新,有可能数据还没有被读取过就已经再次更新了,这就造成了缓存空间的浪费。另外,缓存中的值可能是经过一系列计算的,而并不是直接跟数据库中的数据对应的,频繁更新缓存会导致大量无效的计算,造成机器性能的浪费。当然,这种场景也可能用到,结合实际情况选择。
2、先删除缓存,然后再更新数据库
这个方案的问题很明显,假设现在并发两个请求,一个是写请求A,一个是读请求B,那么可能出现如下的执行序列:
请求A删除缓存
请求B读取缓存,发现不存在,从数据库中读取到旧值
请求A将新值写入数据库
请求B将旧值写入缓存
这样就会导致缓存中存的还是旧值,在缓存过期之前都无法读到新值。这个问题在数据库读写分离的情况下会更明显,因为主从同步需要时间,请求B获取到的数据很可能还是旧值,那么写入缓存中的也会是旧值。这样会产生大量脏数据。
3、先更新数据库,然后再删除缓存
到我们最常用的方案了,但是也会导致一致性问题,不过产生脏数据比较少。
我们假设有两个请求,请求A是读请求,请求B是写请求,那么可能会出现下述情形:
请求B更新数据库
请求A查数据库,得到旧值
请求A将旧值写入缓存
请求B删除缓存
期间只有请求B更新数据库,还没来得及删除缓存这段时间内会有脏数据,导致数据不一致。但是后面更新操作完成后,立马将缓存删除了,在后面的读请求获取到的就是新的数据了。
我们依然假设有两个请求,请求A是读请求,请求B是写请求,那么可能会出现下述情形:
先前缓存刚好失效
请求A查数据库,得到旧值
请求B更新数据库
请求B删除缓存
请求A将旧值写入缓存
该情况出现的概率可能非常低,因为这个条件需要发生在读缓存时缓存失效,而且并发着有一个写操作。考虑到数据库上的写操作一般都会比读操作要慢得多,写操作还会上锁,而读操作必需在写操作前进入数据库操作,而又要晚于写操作更新缓存,所有的这些条件都具备的概率基本并不大。
后话:当然,这都是我们在不考虑热点数据缓存击穿的情况下来讲的。结合业务,也不太可能在热点数据并发量最高的时候去更新数据库删除缓存吧。在并发量不高的时候更新热点数据,缓存删除了后,自己再请求下,初始化一下缓存没有任何问题。
什么是缓数据一致性?
我们通常存储数据是数据库+缓存使用。我们需要保证缓存数据和数据库数据相同,这就是数据一致性。我们将数据分为最终一致和强一致两类,但是真正意义上来讲数据库的数据和缓存的数据是不可能一致的,因为这是两个系统,必然是两步操作。不过我们可以通过某些方法达到相同目的!
如何保证数据强一致?
1、通过2PC或Paxos等一致性协议来达到强一致目的,但是难度很大,很复杂!
2、通过分布式锁来达到目的,但是实现起来同样难度很大,很复杂!
思路其实就是一种串行化的思路,写请求一定要在读请求之前完成,才能保证最新的数据对所有读请求来说是可见的。
比如,"先更新数据库,再删除缓存",从字面上来看,这里有两步操作,因此在数据库更新之前,到缓存被删除这段时间之内,读请求读取到的都是脏数据。
如果要实现这两者的强一致性,只能是在更新完数据库之前,所有的读请求都必须要被阻塞直到缓存最终被删除为止。如果是读写分离的场景,则要在更新完主库之前就开始阻塞读请求,直到主从同步完毕,且缓存被删除之后才能释放。
如何实现这种阻塞?
对于写请求来说,在更新数据库之前,必须要先申请写锁,而其他线程或机器在读取数据之前,必须要先申请读锁。读锁是共享的,写锁是排他的,即如果读锁存在,可以继续申请读锁但无法申请写锁,如果写锁存在,则无论是读锁还是写锁都无法申请。只有实现了这种分布式读写锁,才能保证写请求在完成数据库和缓存的操作之前,读请求不会读取到脏数据。
串行化可以保证不会出现不一致的情况,但是它也会导致系统的吞吐量大幅度降低,用比正常情况下多几倍的机器去支撑线上的一个请求,甚至可能会超出你引入缓存所得到的性能提升。
总结:我们不需要追求数据强一致,保证数据最终一致即可。
如何保证数据最终一致?
即使只是保证数据最终一致性,我们也有可能碰到问题。不管是先删库再删缓存,还是先删缓存再删库,都可能出现数据不一致的情况,比如:先更新数据库,再删除缓存。如果删除缓存失败了,那么会导致数据库中是新数据,缓存中是旧数据,数据就出现了不一致。
| 双删加超时
除了设置缓存过期时间这种兜底方案之外,如果我们希望尽可能保证缓存可以被及时删除,那么我们必须要考虑对删除操作进行重试。
这样较差的情况是在超时时间内存在不一致,当然这种情况极其少见,可能的原因就是服务宕机。此种情况可以满足绝大多数需求。 当然这种策略要考虑redis和数据库主从同步的耗时,所以在第二次删除前最好休眠一定时间,比如500毫秒,这样毫无疑问又增加了写请求的耗时。
为了不影响主流程的正常运行,你可能会将这个事情交给一个异步线程或者线程池来执行,但是如果机器此时也宕机了,这个删除操作也就丢失了。由此我们引入异步消息队列,在删除缓存失败的情况下,将删除缓存作为一条消息写入消息队列,然后由消费端进行消费和重试。
| 异步淘汰缓存
通过读取binlog日志来保持一致性。
| 异步消息队列
在确保Redis高可用,并且数据库数据延迟不影响的情况下,可以先直接更新Redis缓存,再通过异步消息队列更新数据库。
| 版权声明:本文为博主原创文章,转载请注明出处。