项目中,为了提高数据的访问速度,减少数据库(比如mysql)的压力,我们通常把常用数据放一份到缓存中,比如redis。
访问数据时,通常我的操作如下:
查询缓存,如果发现数据,就返回,如果发现没有数据查询mysql数据库,然后把查询结果写入缓存,并返回。
那么,就会有一个大家经常提到的问题,redis缓存和mysql数据库,如何保证数据的一致性?
通常使用最初级的的方法是:
- 先删除缓存中的数据。
- 写mysql数据库。
- 提交,并返回。
- 当更新的数据被查询时,查询缓存,发现没有数据查询mysql数据库,然后把查询结果写入缓存,并返回。
这样在访问量大的时候会有个问题,就是当步骤1执行之后,步骤2完成之前,如果有查询过来,就会去查询数据库中数据,而此时,数据库中的数据更新还没有的完成提交,这时候查到的数据就还是更新前的数据,该数据会被写到缓存中,等数据库中的数据更新提交之后,用户再次读取数据的时候,从缓存中得到的数据仍然是更新前的数据。一旦这种情况出现,就可能造成很严重的错误。
如何避免以上问题?我们可以通过加锁的方法避免:
- 数据更新的时候,先删除缓存,并对相应的key加锁,防止读数据库的时候,mysql数据库还没有更新完成。(要求严格的话,推荐使用etcd,锁可续租。)
- 如果在所释放之前有线程来读取数据,直接读数据库。
- 更新mysql数据库
- 释放锁,并返回。
- 读的时候,先读缓存;如果没有的话,就读数据库,同时将数据放入缓存,并返回响应。
这样避免的修改时读数据出现错误的情况,但是,如果数据量过大的时候,在更新时,会给数据库在带来很大的压力。于是,我们进一步改进如下:
- 数据更新的时候,先复制一份缓存,到一个副本key,(副本key的名字可以定义为${key}_bak),然后对相应的key加锁,并删除原来的key,这样是为了防止更新mysql数据库时,还没有更新完成,就有大量的读数据进来直接读mysql数据库。(要求严格的话,推荐使用etcd,锁可续租。)
- 如果在所释放之前有线程来读取数据,会发现有锁,直接读数据库副本key。副本设置有效时长10s,如果不放心,可以在设置长一点。
- 更新mysql数据库。对于并发很大的数据,写缓存。
- 释放锁,删除缓存副本。
- 读的时候,直接读缓存。如果并发量不大的数据,可以在更新的时候不写缓存,更新后第一次读缓存发现没有,在读数据库,同时将数据放入缓存,并返回响应。
我听说过有人在写数据库的时候切换缓存到备份库的,这种方案如果是在更新比较少的情况下应该没问题,如果更新也相对频繁,频繁的切库,也是不小的问题。