文章目录
1. 关于数据一致性理解
为了系统性能一般都会引入缓存机制,比如 Redis。
这种情况下当用户读数据时一般会按照如下流程:
但是对于数据库的更新,怎么操作才算合理的操作呢?
- 先更新数据库,再更新缓存;
- 先删除缓存,再更新数据库;
- 先更新数据库,再更新缓存。
2. 一致性解决办法
2.1、缓存TTL
简单直接又暴力的方法,如果有些数据不重要,我们读完一次数据到缓存后设置个TTL即可,等待超时后缓存自动从数据库读取下数据。
2.2、先更新数据库,再更新缓存
假设现在有两个请求(线程)A、B,都要改变数据库的数据。A请求将age = 14,B请求将age = 12。下图是正常执行跟非正常执行情况:
由上图可知,出现网络问题可以导致数据库与缓存不一致。因此这个方法是不可取的。出现如下场景也不可取:
- 写场景多,但是读场景少的时候,由于和数据库关联比较多,和缓存关联比较少,但是缓存却经常被更新。
- 如果缓存的数据是被经过大量复杂计算后的数据,那么每次更新缓存都要进行大量的计算,影响系统性能。
2.3、先删除缓存,再更新数据库
假设现在有两个请求A、B。A请求去写数据,B请求去读数据。如果先删除缓存,再更新数据库,就会出现如下情况:
由上图可知,就会出现缓存是脏数据。那么有办法解决吗?,答案是:有的
我们可以采用延迟双删策略,代码如下:
public void write(String key,Object value){
redis.delKey(key);
db.updateValue(value);
Thread.sleep(1000); // 再次删除
redis.delKey(key);
}
- 先淘汰缓存;
- 再写数据库(这两步和原来一样);
- 休眠1秒;
- 再次淘汰缓存。
sleep的时间要根据业务数据逻辑耗时而定,反正目的是确保读请求结束,写请求可以删除读请求造成的缓存脏数据。这么做,可以将1秒内所造成的缓存脏数据,再次删除!
可是其实第二次删除还是有不妥的地方:
- 二次删除前面涉及到休眠,可能导致系统性能降低,可以采用异步的方式,再起一个线程来进行异步删除。
- 如果二次删除失败了,还是会导致缓存脏数据存在的啊!
2.4、先更新数据库,再删除缓存
假如此时A、B两个线程同时请求,正常来讲不管你是读写分离还是单机版,读一般比写快。那删除缓存一般是有效的。
该方案相比先删除缓存再更新数据库还是稳妥些的,但是也不是万无一失的。不管是先删缓存再更新数据库还是先更新数据库再删缓存,如果删除缓存失败了都会导致缓存跟数据不一致问题!
2.5、消息队列确保消息删除
通过消息队列的确认消费机制来删除缓存。
缺点也很明显:
- 对业务线代码造成大量的侵入,引入了中间件。
- 消息的延迟删除也会造成短暂的不一致。
2.6、专门程序+消息队列确保消息删除
该方案启动一个订阅程序去订阅数据库的binlog,获得需要操作的数据。在应用程序中,另起一段程序,获得这个订阅程序传来的信息,进行删除缓存操作。
订阅binlog程序在mysql中有现成的中间件叫canal,可以完成订阅binlog日志的功能。
3. 总结
分析后你会发现数据更新时缓存是删除不是更新,而删除缓存一般有三种方法:
- 如果缓存数据不敏感,直接给缓存设置TTL即可。
- 先删缓存再更新数据库,此时需配合延时双删技术,但可能导致二次删除失败。
- 先更新数据库再删缓存,此时需配合binlog消费 + 消息队列来实现。