方案1:采用延时双删策略
在高并发的业务场景下(如秒杀或者双十一),数据库最容易挂掉环节。所以,就需要使用Redis做一个缓冲操作,让请求先访问到Redis,如果Redis命中就不在访问数据库,从而减轻数据库的压力。
如果涉及到数据更新(用户下单或者修改用户信息等造成的数据变动):数据库和缓存更新,就容易出现缓存(Redis)和数据库(MySQL)间的数据一致性问题。
网上有主张先写缓存后写数据库,或者先写数据库后写缓存等观点。
不管是先写数据库,再删除Redis缓存;还是先删除缓存,再写库,都有可能出现数据不一致的情况。举一个例子:
1.如果删除了Redis,还没有来得及写DB,另一个线程就来读取,发现缓存为空,则去DB中读取数据写入缓存,此时缓存中为脏数据。
2.如果先写了DB,在删除缓存前,写库的线程抛异常,没有删除掉旧的缓存,则也会出现数据不一致情况。
因为写和读是并发的,没法保证读操作和写操作的先后顺序,就会出现缓存和数据库的数据不一致的问题。
方案1:采用延时双删策略
在写DB前后都进行redis.del(key)操作,并且设定合理的超时时间。伪代码如下。
public void write(String key,Object data){
redisTemplate.delKey(key);
db.update(data);
Thread.sleep(80);
redisTemplate.delKey(key);
}
具体的步骤就是:
1)先删除旧的缓存
2)再写新的数据到DB
3)休眠80毫秒
4)再次删除缓存(删除的缓存可能存在也可能不存在)
那么,这个80毫秒怎么确定的,具体该休眠多久呢?
需要评估自己的项目的读数据业务逻辑的耗时。这么做的目的,就是确保读写入的DB的数据一定会在执行数据库操作后更新到Redis缓存中。保证最终一致性。
但是这种策略考虑Redis和DB同步的耗时。
从理论上来说,给缓存设置合理的过期时间,是保证最终一致性的解决方案。所有的写操作最终都要以DB的数据为准,只要到达缓存过期时间,则后面的读请求会因为缓存不能命中而必须从数据库中读取新值然后回写到缓存中。
该方案的弊端
结合双删策略+缓存超时设置,这样最差的情况就是在超时时间内数据存在不一致,并且延迟时间很难把控,如果设置的时间过久将会影响应用的并发性能。
方案2:异步更新缓存
MySQL binlog增量订阅消费+消息队列+消息队列监听回写Redis
1)读操作发生在Redis上
2)增删改等数据库操作发生在DB上,更新后无需处理缓存
3)更新Redis数据,监听消息队列发来的MySQL的数据更新的binlog消息,异步更新Redis。
数据操作主要分为两大块:
读取binlog后分析 ,利用消息队列 ,推送更新缓存数据。
这样一旦MySQL中产生了新的写入、更新、删除等操作,就可以把binlog相关的消息推送至消息队列,消费者线程异步刷新Redis。
可以结合使用canal(阿里巴巴的一款开源框架),通过该框架可以对MySQL的binlog进行订阅,得Redis的数据得以更新。
消息推送工具可以采用别的第三方:Kafka、RabbitMQ等来实现消息发送。