为了提高服务的并发度,我们通常会使用缓存。缓存的使用有两个方面:一个是数据查询时,一个是数据修改时。
- **查询时:**先查询缓存,缓存不存在查询数据库,数据库查询完毕添加到缓存。
- **修改时:**修改数据和删除缓存,两者顺序不定
当我们在并发环境下,多个线程同时查询和修改数据时,会导致脏数据的产生。这是由于Redis中保存的数据同MySql中的数据不一致导致的。
数据不一致原因
先缓存后数据库
当我们修改数据时,先将缓存中的数据删除,然后向数据库中修改数据。这个时候如果有另外一个线程查询数据,就有可能查询到修改前的脏数据。
两个线程在同一时间,A线程查询数据,B线程修改数据
- B线程先将缓存中的数据删除
- A线程查询缓存,未查询到。去数据库中查询,并将查询到的数据添加到缓存中
- B线程修改数据库
- 缓存中的数据就是修改前的脏数据
这种情况下,我们有一个保底的策略就是指定Redis中数据的缓存时间,缓存失效后数据最终一致性得以保持
先数据库后缓存
当我们修改数据时,先修改数据库,然后删除缓存。这个时候如果有一个线程在修改数据库前查询数据库,另一个线程修改数据库并且删除缓存,这个线程这时候添加缓存,也会导致缓存跟数据库数据不一致问题。
两个线程在同一时间,A线程查询数据,B线程修改数据
- A查询缓存,未查询到数据
- A查询数据库
- B修改数据库
- B删除缓存
- A将查询到的数据添加到缓存中
这种情况发生几率是很小的,因为数据库查询耗费的时间跟数据库修改耗费的时间相差好几个级别。
数据不一致问题解决
数据库和缓存的数据是无法保持强一致性的,一致性解释(强一致性、弱一致性、最终一致性)。不管怎样,他都是有可能出现脏数据的。所以我们解决一致性问题,是为了保持他们的最终一致性。
最终一致性强调的是系统中所有的数据副本,在经过一段时间的同步后,最终能够达到一个一致的状态。因此,最终一致性的本质是需要系统保证最终数据能够达到一致,而不需要实时保证系统数据的强一致性。
缓存延时双删
当我们采用先删除缓存再修改数据库策略时,并发情况下有可能产生脏数据。为了解决这个问题,我们可以休眠一秒,然后再次删除缓存。
- 先删除缓存
- 修改数据库
- 休眠一秒,再次删除缓存
这个休眠的时间要根据自己的项目的读数据业务逻辑的耗时来评估。为了不影响业务执行,重试这个过程可以异步执行,使用定时线程池或者mq处理。
缓存删除失败重试
缓存删除失败时,需要重试,同理适用mq或线程池异步处理
使用中间件
上述处理,都会导致代码耦合,我们可以读取数据库binlog变更,然后异步修改缓存。可以采用阿里开源的canal中间件