在高并发系统中,几乎都会采用 MySQL + Redis 的架构来提升系统性能。Redis负责扛流量、提升查询速度;MySQL负责数据持久化存储。但引入缓存的同时也带来了一个相对棘手的问题:
- 数据库更新了,缓存没更新怎么办?
- 缓存更新了,数据库没更新怎么办?
- 如何保证数据库与缓存中的数据一致性?
什么是数据库与缓存一致性?
Redis缓存与数据库双写一致性,本质是异构存储系统的数据对齐问题,缓存(Redis)与数据库(如MySQL)是两个独立的、无原生分布式原子性保证的存储系统,在并发读写场景下,因操作时序错位、执行失败、网络波动等因素,导致缓存和数据库存储的数据出现差异,最终引发业务脏读、数据错误。假设商品库存如下:
此时数据库与缓存的数据相同。此时用户修改了库存,执行以下语句:
updateproductsetstock=99whereid=1001;如果因为某种因素没有将 Redis 中的数据更新,导致数据库的数据是99,而Redis中的数据是100,那么用户读取缓存时得到的就是脏数据。这就是典型的缓存数据不一致。要解决数据不一致问题通常有以下4种方式:
先更新数据库,再更新缓存
在并发场景下,假设有两个线程,线程1和线程2同时并发更新同一条数据时,由于网络延迟或线程调度,可能出现以下执行时序:
- 线程1先将数据库中的数据更新为100
- 线程2紧接着将数据库中的数据更新为99
- 线程2动作极快,顺手把缓存也更新成了99
- 最后,线程1把缓存更新成了100
这就造成数据库的数据是99,缓存的数据是100,很明显数据库和缓存数据不一致问题。如果业务上由于缓存命中率要求,必须使用更新缓存,需要引入以下方案解决数据不一致问题:
分布式锁:在更新缓存前加分布式锁(如 Redisson),确保同一时间只有一个请求能更新数据库+缓存,强制串行化。
短过期时间:给缓存设置较短的 TTL,让不一致的脏数据能尽快失效。
先更新缓存,再更新数据库
并发场景下,同样的分析两个并发更新请求线程1和线程2:
- 请求线程1先将缓存中的数据更新为100。
- 在请求线程1还没来得及更新数据库时,线程2再将缓存中的数据更新为99。
- 请求线程2顺手把数据库也更新成了99。
- 最后,请求线程1才执行数据库更新,把数据库的数据更新成了100。
先更新数据库,再删除缓存
在并发场景下读写时,理论上存在一种极端的漏洞时序:
- 此时缓存中刚好没有该数据。请求线程1前来读取,从数据库中查到旧值为100。
- 在请求线程1还没来得及将旧值写入缓存之前,请求线程2前来更新。
- 请求线程2将数据库更新为99,并执行删除缓存(此时缓存本来就空,删了个寂寞)。
- 最后,线程1才写入缓存的值为100,把刚才线程2更新的99写回了缓存。
这就是著名的Cache Aside 旁路缓存写策略。要解决这个问题使用以下方式:
消息队列(MQ)重试机制:如果删除缓存失败,将要删除的 Key 写入 MQ,由消费者尝试异步重试删除,直到成功。
订阅 MySQL binlog:利用 Canal 等中间件伪装成 MySQL 从节点,监听 binlog 变更,一有更新就由专职服务异步解析并删除对应的 Redis 缓存。
先删除缓存,再更新数据库
并发场景下在读写时,这种方案的漏洞触发概率非常高:
- 请求线程1准备更新数据为100时,先删除了缓存。
- 此时请求线程2前来读取数据,发现缓存未命中,便去数据库中读取到了99。
- 线程2将读到的99写入了缓存。
- 最后,线程1才更新数据库数据为100,将其改为了新值。
要解决这个问题也简单,延迟双删:
redis.delKey(X);// 1. 先删缓存db.update(X);// 2. 更新库Thread.sleep(N);// 3. 强制休眠一段时间(如 200ms)redis.delKey(X);// 4. 再次删除缓存阿里云参考文档
https://developer.aliyun.com/article/1732763