Cache Aside策略
比如电商系统中有一个用户表,表中有ID和年龄两个字段,缓存中以ID为key存储年龄信息。当我们把ID为1的用户的年龄从19更新到20,怎么做?可以先更新数据库,再更新缓存。但是这样会导致缓存和数据库中数据不一致,如下图所示:
A 请求将数据库中 ID 为 1 的用户年龄从 19 变更为 20,与此同时,请求 B 也开始更新 ID 为 1 的用户数据,它把数据库中记录的年龄变更为 21,然后变更缓存中的用户年龄为 21。紧接着,A 请求开始更新缓存数据,它会把缓存中的年龄变更为 20。此时,数据库中用户年龄是 21,而缓存中的用户年龄却是 20。
**如何解决这个问题?**我们可以再更新数据时不更新缓存,而是删除缓存中的数据,在读取数据时,发现缓存中没了数据之后,再从数据库中读取数据,更新到缓存中。
这就是Cache Aside 策略。它的问题在于当写入比较频繁时,缓存中的数据会被频繁地清理,这样会对缓存的命中率有一些影响。如果业务中对缓存命中有严格要求,可以考虑以下两种解决方案:
- 更新数据时也更新缓存,只是在更新缓存前先加一个分布式锁,因为这样在同一时间只允许一个服务更新缓存,就不会产生并发问题了。当然这么做对于写入的性能会有一些影响。可以满足强一致性。
- 在更新数据时也更新缓存,只是给缓存加一个较短的过期时间,这样即使出现缓存不一致的情况,缓存的数据也会很快地过期,对业务的影响也是可以接受的。
Cache Aside 策略也并非一定能保证数据一致性,比如请求A读数据时,先读取缓存,发生Cache Miss进而读DB(数据版本V1),同时请求B修改数据,更新DB数据(数据版本V2),然后立马把以前的key删掉(发现key为空,请求A还没回填完),而后请求A才将V1版本数据回填到了缓存,产生了数据不一致的情况。
目前公司的解决办法是用一个消息队列异步补偿缓存,即用消费者消费Mysql的bin-log日志,再回填一次缓存,满足数据最终一致性。这也是有风险的,毕竟这个消费者服务和刚刚说到的请求A,无法满足“Happens Before”。因为也可能发生消费者服务在消费完成V2版本数据的回填之后,请求A才V1版本的数据覆盖写入到了缓存中,又造成不一致。这里得用一点小技巧,异步Job操作的时候用SetEX(优先级高,异步补偿直接写缓存),而请求A这种类型的操作用SetNX(优先级低,缓存中有数据,则不写入),这样就能保证数据最终一致性啦。