缓存可以提高数据查询的效率,降低数据库的压力,但使用缓存的过程中,需要注意缓存一致性问题。
先更新缓存,再更新数据库
假设2个并发线程A和B,均为写线程,其执行顺序如下:
- 线程A更新缓存值为v1;
- 线程B更新缓存值为v2;
- 线程B更新数据库值为v2;
- 线程A更新数据库值为v1。
此时,数据库中存储的为值v1,而缓存中的值为v2,数据不一致。
先删除缓存,再更新数据库
假设2个并发线程,1个读线程A,1个写线程B,其执行顺序如下:
- 线程B删除缓存;
- 线程A读取缓存,发现无缓存;
- 线程A继续读数据库,取出数据v1到缓存;
- 线程B写入数据v2到数据库。
后续的请求每次从缓存中读取到的均是老数据v1,而数据库中的为新数据v2,数据不一致。
先更新数据库,再更新缓存
假设2个并发线程A和B,均为写线程,其执行顺序如下:
- 线程A更新数据库值为v1;
- 线程B更新数据库值为v2。
- 线程B更新缓存值为v2;
- 线程A更新缓存值为v1;
此时,数据库中存储的为值v2,而缓存中的值为v1,数据不一致。
先更新数据库,再删除缓存(Cache Aside)
假设2个并发线程,1个读线程A,1个写线程B,其执行顺序如下:
- 线程B发起写操作,还未操作数据库;
- 缓存刚好失效;
- 线程A读取缓存,发现无缓存;
- 线程A继续读数据库,取出数据v1;
- 线程B写入数据v2到数据库;
- 线程B删除缓存;
- 线程A将数据v1更新到缓存。
此时,数据库中存储的为值v2,而缓存中的值为v1,数据不一致。
但是,上述情况发生的前提是线程A写数据到数据库的时间比线程A写数据到内存的时间要快,但缓存的写速度远远高于数据库,发生概率极低。
当然,为了避免上述小概率事件的发生,可以再给缓存添加1个过期时间,双重保证。
在左耳朵耗子的博文《缓存更新的套路》中,还提到了另外2种缓存更新模式:
Read/Write Through
Read/Write Through的思路是把更新数据库的操作由缓存自己代理,更新只与缓存交互。
Read-Through直接从缓存中读数据,若缓存中存在则直接返回,否则缓存自己去数据库查询数据更新到缓存。
Write-Through相当于是先更新缓存,再更新数据库,区别在于:
- 2步操作均在同一个事务中完成;
- 第2步的更新数据库由缓存负责,与应用方无关。
优点是保障了一致性,且应用方编码简单,缺点就是引入了事务,增加了写入延迟,一般应用在银行系统等。
Write-Behind
该种模式也是先更新缓存,再更新数据库,且更新数据库的操作也是由缓存自己负责。
是不是和Write-Through很像?它俩的区别主要是:
Write-Behind的更新数据库步骤是异步的,即第2步更新数据库会在第1步之后的一段时间之后(或者被其他方式触发)再执行。
同时,其异步写入数据库的操作为了提升性能,往往采取批量提交的方式。
基本原理和Linux系统的PageCache类似。
Write-Behind优点是性能强劲(内存操作、异步批量落盘),缺点就是缓存和数据库的一致性在所有方案中最差。
综上,在一般业务场景下,缓存一致性的最佳实践为:
- 为缓存设置过期时间;
- 先更新数据库,再删除缓存。
当然,没有最佳的方案,只有适合的方案,用户可以根据业务需求灵活选择缓存一致性的实现方法。