十、缓存与数据库一致性
1、普通方案
(1)、先更新数据库,再更新缓存
①、执行过程
a、A请求和B请求同时进行操作。
b、A请求先更新了数据库的一条数据。
c、B请求又更新了该条数据。
d、B请求比A请求先更新了缓存,因为网络延迟等原因。
e、A请求后更新缓存
②、存在问题
缓存中的数据并不最新的B更新过的数据,就导致了数据不一致的情况。如果业务场景是一个写数据库较多而读数据库较少的业务,如果采用这种方案就会导致数据还没读缓存就会被频繁更新,白白浪费性能。
(2)、先删除缓存,再更新数据库
①、执行过程(问题)
a、同时有A请求进行更新操作,B请求进行查询操作。
b、A请求进行写操作前会删除缓存。
c、B请求发现缓存刚好为空,就会查询数据库。
d、A请求在B请求查询完成之后,将新的值写入数据库。
e、B请求将查询到的旧值写入缓存。
②、存在问题
a、此时就会导致数据不一致的问题,如果不采用给缓存设置过期时间的策略,该数据永远都是脏数据。
b、可以采用延时双删的策略,就是在更新数据库之前先删除缓存,然后对数据库进行写入操作,数据库更新完成之后再次进行删除缓存的操作,目的是删除读请求可能造成的缓存脏数据,第二次删除缓存之前可以休眠几秒。这么做的目的就是确保读请求结束写请求可以删除读请求造成的脏数据。
(3)、设置过期时间
理论上说给缓存设置过期时间,保证最终一致性,过期时间内允许脏数据。所有的写操作以数据库为准,只要到达缓存过期时间,则后面的读请求就会从数据库中读取新值然后回填缓存。
(4)、先更新数据库,再删除缓存
①、执行过程(问题)
a、此时缓存刚好失效
b、A请求就会去查询数据库得到一个旧值
c、B请求将新的值写入数据库
d、B请求写入成功后删除缓存
e、A请求将查到旧值写入缓存,产生脏数据b
②、如果要产生这种结果,就是B请求的操作时间非常短,B请求写入数据库的操作要比A请求从数据库中读取数据的速度要快,只有这种情况下d操作才可能比e操作先发生。但是数据库的读操作要远比写操作快的多,理论上这种情况发生的概率是非常低,但是为了保险起见可以给缓存设置过期时间或者采用延时双删策略。
2、延时双删策略
(1)、操作步骤
①、先删除缓存
②、再更新数据库
③、休眠500毫秒
④、再次删除缓存
/**
* 伪代码
**/
public void write(String key,Object data){
// 删除缓存
redis.delKey(key);
// 更新数据库
db.updateData(data);
// 休眠500毫秒
Thread.sleep(500);
// 再删除缓存
redis.delKey(key);
}
(2)、说明
①、休眠时间
具体该休眠多久,需要评估自己的项目的读数据业务逻辑的耗时,还要考虑redis和数据库主从同步的耗时。目的就是确保读请求结束,写请求可以删除读请求造成的缓存脏数据。写数据的休眠时间,在读数据业务逻辑的耗时基础上,加几百ms即可。
②、最后一次删除失败
a、将删除失败的缓存放入消息队列中。
b、业务代码从消息队列中获取需要删除的key。
c、继续尝试删除操作,直到成功。
3、异步更新缓存(基于订阅binlog的同步机制)
(1)、操作步骤
技术思路:MySQL binlog增量订阅消费+消息队列+增量数据更新到Redis
a、读Redis:热数据基本都在Redis。
b、写MySQL:增删改都是操作MySQL。
c、更新Redis数据:MySQL的数据更新通过binlog来更新到Redis,这种机制,类似MySQL的主从备份机制,因为这也是通过binlog来实现的数据一致性。
(2)、Redis更新操作
①、数据操作分类
a、一个是全量(将全部数据一次写入到redis)
b、一个是增量(实时更新)
这主要说增量,就是MySQL的update、insert、delate变更数据。
②、更新步骤
a、MySQL执行update、insert、delate等操作,会产生binlog日志,它记录了所有的DDL和DML语句(除了数据查询语句select)。
b、把binlog相关信息通过消息中间件推送到Redis。
c、Redis再根据binlog中的记录,对Redis进行更新。
③、阿里的一款开源框架canal,就可以对MySQL的binlog进行订阅,而canal正是模仿了mysql的slave数据库的备份请求,使得Redis的数据更新达到了相同的效果。