1、需求
缓存由于其高并发和高性能的特性,已经在项目中被广泛使用。在读取缓存方面,普遍的做法都是没有啥疑问的,基本都是按照如下的流程进行。但是在更新缓存的时候,却有很多问题,接下来我们慢慢分析。
2、表明自己的态度
首先我抛出自己的看法,从理论上来说,给缓存设置过期时间,是保证最终一致性的解决方案。这种方案下,我们可以对存入缓存的数据设置过期时间。
3、讨论四种更新策略
1、先更新数据库,再更新缓存
2、先删除缓存,再更新数据库
3、先更新数据库,再删除缓存
4、先更新缓存,再更新数据库(基本上没有人使用这种策略,我们不讨论)
第一种方案的讨论(先更新数据库,再更新缓存):
观点:大家是普遍反对的,不建议在生产环境使用。原因如下:
原因一(线程安全角度)
同时有请求A和请求B进行更新操作,那么会出现:
(1)线程A更新了数据库
(2)线程B更新了数据库
(3)线程B更新了缓存
(4)线程A更新了缓存
这就出现请求A更新缓存应该比请求B更新缓存早才对,但是因为网络等原因,B却比A更早地更新了缓存。这就导致了脏数据。
原因二(业务场景角度)有如下两点:
(1)如果是一个写多读少的场景业务需求,采用这种方案就会导致,数据压根还没读到,缓存就被频繁的更新,浪费性能。
(2)如果你写入数据库的值,并不是直接写入缓存的,而是要经过一系列复杂的计算再写入缓存。那么,每次写入数据库后,都需要再次计算写入缓存的值,无疑是浪费性能的。
通过上面的分析,我们不建议在生产环境中使用第一种方案。
第二种方案的讨论(先删除缓存,再更新数据库):
该方案同样会导致数据不一致。原因是:同时有一个请求A进行更新操作,另一个请求B进行查询操作。那么会出现如下情形:
(1)请求A进行写操作,先删除缓存(2)请求B查询发现缓存不存在
(3)请求B去数据库查询得到旧值
(4)请求B将旧值写入缓存
(5)请求A将新值写入数据库
如果你用了mysql的读写分离架构怎么办?在这种情况下,造成数据不一致的原因如下:还是有两个请求,一个请求A进行更新操作,另一个请求B进行查询操作。
(1)请求A进行写操作,先删除缓存
(2)请求A将数据写入数据库了
(3)请求B查询缓存发现,缓存没有值
(4)请求B去slave库查询,此时还没有完成主从同步,因此查询到的是slave库中的旧值
(5)请求B将旧值写入缓存
(6)数据库完成主从同步,slave库变为新值
上述情形,就是数据不一致的原因。
通过上面的分析,我们依然不建议在生产环境中使用第二种方案。
第三种方案的讨论(先更新数据库,再删除缓存):
在介绍这种方案之前,先阅读两篇文章:cache-aside 以及 Scaling Memcache at Facebook 。
这种情况不存在并发问题么?不是的。假设依然有两个请求,一个请求A做查询操作,一个请求B做更新操作,那么会有如下情形产生:
(1)缓存刚刚好被删除掉
(2)请求A查询数据库,得到一个旧值
(3)请求B将新值写入数据库
(4)请求B删除缓存
(5)请求A将查到的旧值写入缓存
如果发生上述情况,确实是会发生脏数据。然而,发生这种情况的概率又有多少呢?
发生上述情况有一个先天性条件,就是步骤(3)的写数据库操作比步骤(2)的读数据库操作耗时更短,才有可能使得步骤(4)先于步骤(5)。可是,大家想想,数据库的读操作的速度远快于写操作的(不然做读写分离干嘛,做读写分离的意义就是因为读操作比较快,耗资源少),因此步骤(3)耗时比步骤(2)更短,这一情形很难出现。假设,有人非要抬杠,有强迫症,一定要解决怎么办?
如何解决上述并发问题?
首先,给缓存设有效时间是一种方案。还有其他造成不一致的原因么?有的,这也是第二种方案和第三种方案都存在的一个问题,如果删缓存失败了怎么办?那不是会有不一致的情况出现么。比如一个写数据请求,然后写入数据库了,删缓存失败了,这就会出现不一致的情况了。
如何解决?这里给出两套方案。
第一套方案:设置全局的缓存失效时间 + 第三种方案。即使删除失败,等待失效时间,缓存自动被删除。
第二套方案:
流程如下图所示:
(1)更新数据库数据
(2)数据库会将操作信息写入binlog日志当中
(3)订阅程序提取出所需要的数据以及key
(4)另起一段非业务代码,获得该信息
(5)尝试删除缓存操作,发现删除失败
(6)将这些信息发送至消息队列
(7)重新从消息队列中获得该数据,
(8)重试操作,进行重新删除,如果删除成功,程序结束,否则进入第(7)步
备注说明:上述的订阅binlog程序在mysql中有现成的中间件叫canal,可以完成订阅binlog日志的功能。
参考文献:
2、缓存更新的套路