目录
一、什么是缓存双写一致性呢?
1.1 双检加锁机制
二、数据库和缓存一致性的更新策略
2.1、先更新数据库,后更新缓存
2.2 、先更新缓存,后更新数据库
2.3、先删除缓存,在更新数据库
延时双删的策略:
2.4.先更新数据库,在删除缓存(常用)
2.5、实际中是不可能做到强一致性的,那么怎么做到最终一致性呢?
三、canal中间件
3.1 canal工作原理
3.2 MySQL的主从复制
一、什么是缓存双写一致性呢?
-
如果redis中有数据
- 需要和数据库中的值相同
-
如果redis中无数据
- 数据库中的值是最新值,且准备回写redis
缓存按照操作分
- 只读缓存 (就没有同步这一说法了)
- 读写缓存
- 同步直写策略 (比如比较紧急的事情,冲了vip得立即生效)
- 写数据库后也同步写 redis 缓存,缓存中的数据和数据库中的一致
- 对于读写缓存来说,要想保证缓存和数据库中的数据一致
- 同步直写策略 (比如比较紧急的事情,冲了vip得立即生效)
- 异步缓写策略 (一般都是用这种)
- 正常业务运行中,mysql数据变动了,但是可以在业务上容许出现一定时间后才作用于redis,比如仓库、物流系统
- 异常情况出现了,不得不将失败的动作重新修补,有可能需要借助kafka或者RabbitMQ等消息中间件,实现重写重试
1.1 双检加锁机制
加锁前从redis中查一次,加锁后再查一次。
多个线程同时去查询数据库的这条数据,那么我们可以在第一个查询数据的请求上使用一个 互斥锁来锁住它。
其他的线程走到这一步拿不到锁就等着,等第一个线程查询到了数据,然后做缓存。
后面的线程进来发现已经有缓存了,就直接走缓存。
二、数据库和缓存一致性的更新策略
一般都是以MySQL为准。
给缓存设置过期时间,定期清理缓存并回写,是保证最终一致性的解决方案。
我们可以对存入缓存的数据设置过期时间,所有的写操作以数据库为准,对缓存操作只是尽最大努力即可。也就是说如果数据库写成功,缓存更新失败,那么只要到达过期时间,则后面的读请求自然会从数据库中读取新值然后回填缓存,达到一致性,切记,要以mysql的数据库写入库为准
2.1、先更新数据库,后更新缓存
异常一: 回写失败会出现脏数据
异常二: 高并发下会出现数据覆盖
2.2 、先更新缓存,后更新数据库
我们一般是不用这种的,因为我们一般都把MySQL作为根基
异常: 高并发下会出现数据覆盖
2.3、先删除缓存,在更新数据库
当有两个线程:一个线程负责删Redis,修改MySQL, 另一个来查找redis
如果数据库更新失败或者不及时就会发生异常:
(1)请求A进行写操作,删除redis缓存后,工作正在进行中,更新mysql......A还没有彻底更新完mysql,还没commit
(2)请求B开工查询,查询redis发现缓存不存在(被A从redis中删除了)
(3)请求B继续,去数据库查询得到了mysql中的旧值(A还没有更新完)
(4)请求B将旧值回写redis缓存
(5)请求A将新值写入mysql数据库
上述情况就会导致不一致的情形出现。
时间
|
线程A
|
线程B
|
出现的问题
|
t1
|
请求A进行写操作,删除缓存成功后,工作正在mysql进行中......
| ||
t2
| 1 缓存中读取不到,立刻读mysql,由于A还没有对mysql更新完,读到的是旧值
2 还把从mysql读取的旧值,写回了redis
| 1 A还没有更新完mysql,导致B读到了旧值
2 线程B遵守回写机制,把旧值写回redis,导致其它请求读取的还是旧值,A白干了。
| |
t3
|
A更新完mysql数据库的值,over
| redis是被B写回的旧值,
mysql是被A更新的新值。
出现了,数据不一致问题。
|
总结一下:
先删除缓存,再更新数据库
|
如果数据库更新失败或超时或返回不及时,导致B线程请求访问缓存时发现redis里面没数据,缓存缺失,B再去读取mysql时,
从数据库中读取到旧值,还写回redis,导致A白干了,o(╥﹏╥)o
|
改怎么解决呢?
延时双删的策略:
这个删除该休眠多久呢?
因为这种同步淘汰机制加上了sleep,导致MySQL吞吐量降低怎么办?
2.4.先更新数据库,在删除缓存(常用)
这一种方法的弊端相对比较少
时间
|
线程A
|
线程B
|
出现的问题
|
t1
|
更新数据库中的值......
| ||
t2
|
缓存中立刻命中,此时B读取的是缓存旧值。
| A还没有来得及删除缓存的值,导致B缓存命中读到旧值。 | |
t3
|
更新缓存的数据,over
|
先更新数据库,再删除缓存
|
假如缓存删除失败或者来不及,导致请求再次访问redis时缓存命中,
读取到的是缓存旧值。
|
2.5、实际中是不可能做到强一致性的,那么怎么做到最终一致性呢?
需要用到消息队列:kafka或者RabbitMQ
但是还是都需要是先更新数据库,再删除缓存,这样最多也就是数据暂时不一致,不会导致雪崩、击穿啥的出现。
1 可以把要删除的缓存值或者是要更新的数据库值暂存到消息队列中(例如使用Kafka/RabbitMQ等)。
2 当程序没有能够成功地删除缓存值或者是更新数据库值时,可以从消息队列中重新读取这些值,然后再次进行删除或更新。
3 如果能够成功地删除或更新,我们就要把这些值从消息队列中去除,以免重复操作,此时,我们也可以保证数据库和缓存的数据一致了,否则还需要再次进行重试
4 如果重试超过的一定次数后还是没有成功,我们就需要向业务层发送报错信息了,通知运维人员。
|
三、canal中间件
能够立刻感知到MySQL改变的有一个MySQL的binlog文件
我们需要一种技术来充当两者之前的吹哨人
这里有阿里研发的一种中间件canal
3.1 canal工作原理
1. canal 模仿MySQL的dump协议,假装自己是MySQL的slave,向MySQL发送dump协议
2. MySQLmaster收到dump请求之后,便会给canal推送自身bin log 变化给canal
3. cannal收到bin log 消息并解析。
3.2 MySQL的主从复制
MySQL的主从复制将经过如下步骤:
1、当 master 主服务器上的数据发生改变时,则将其改变写入二进制事件日志文件中;
2、salve 从服务器会在一定时间间隔内对 master 主服务器上的二进制日志进行探测,探测其是否发生过改变,
如果探测到 master 主服务器的二进制事件日志发生了改变,则开始一个 I/O Thread 请求 master 二进制事件日志;
3、同时 master 主服务器为每个 I/O Thread 启动一个dump Thread,用于向其发送二进制事件日志;
4、slave 从服务器将接收到的二进制事件日志保存至自己本地的中继日志文件中;
5、salve 从服务器将启动 SQL Thread 从中继日志中读取二进制日志,在本地重放,使得其数据和主服务器保持一致;
6、最后 I/O Thread 和 SQL Thread 将进入睡眠状态,等待下一次被唤醒;