缓存与数据库的一致性

什么是缓存?

缓存就是数据交换的缓冲区,针对服务对象的不同(本质就是不同的硬件)都可以构建缓存。
目的是,把读写速度慢的介质的数据保存在读写速度快的介质中,从而提高读写速度,减少时间消耗。 例如:

  • CPU 高速缓存 :高速缓存的读写速度远高于内存。

    • CPU 读数据时,如果在高速缓存中找到所需数据,就不需要读内存
    • CPU 写数据时,先写到高速缓存,再回写到内存。
  • 磁盘缓存:磁盘缓存其实就把常用的磁盘数据保存在内存中,内存读写速度也是远高于磁盘的。

读数据时,从内存读取。
写数据时,可先写到内存,定时或定量回写到磁盘,或者是同步回写。

为什么要用缓存?

使用缓存的目的,就是提升读写性能。而实际业务场景下,更多的是为了提升读性能,带来更好的性能,更高的并发量。
日常业务中,我们使用比较多的数据库是 MySQL,缓存是 Redis 。Redis 比 MySQL 的读写性能好很多。那么,我们将 MySQL 的热点数据,缓存到 Redis 中,提升读取性能,也减小 MySQL 的读取压力。例如说:

  • 论坛帖子的访问频率比较高,且要实时更新阅读量,使用 Redis 记录帖子的阅读量,可以提升性能和并发。
  • 商品信息,数据更新的频率不高,但是读取的频率很高,特别是热门商品。

一致性问题是分布式常见的问题,分为最终一致性和强一致性。如果对数据有强一致性要求,不能放缓存,我们只能保证最终一致性。

缓存与数据库的一致性问题?

问题的产生

并发场景下,读取旧的 DB 数据并更新到缓存中。
缓存和 DB 的操作不在一个事务中,可能只有一个操作成功,另一个操作失败,导致不一致。

解决方案
1.先淘汰缓存,再更新数据库

先淘汰缓存,即使写数据库发生异常,也就是下次缓存读取时,多读取一次数据库,这样理论上来说保证了数据的
一致性。但是实际在并发环境下仍然会出现数据不一致的情况。
首先来说写流程:先淘汰缓存,再写 DB;
然后是读流程:先读缓存,如果未命中再读 DB,然后将 DB 中读出来的数据更新到缓存中。并发环境下,在数据库层面并发的读写并不能保证完全顺序,也就是说后发出的读请求可能先完成。举例来说

  • 线程 T1 发出了写请求,淘汰了缓存;然后写数据库,发出修改请求。
  • 线程 T2 发出了读请求,先读取缓存,未命中,再去读取数据库,发出读取请求,这时候,线程 T1 的写数据还未完成,导致 T2 读取了一个脏数据放入缓存,这样就导致了数据不一致。

这种情况下,可以引入分布式锁实现“串行化”来解决。

  • 在写请求时,先获取分布式锁,再淘汰缓存,更新完数据库后再释放锁。
  • 在读请求时,如果缓存未命中,则先获取分布式锁,加锁失败说明写请求还未完成,继续等待;加锁成功则说明写请求已完成,再去缓存查询一次,如果未命中,则再去 DB 查询并即使更新到缓存。
2.先写数据库,再更新缓存

由于操作缓存和操作数据库不是原子的,则第一步写数据库操作成功,第二步淘汰缓存失败,就会出现 DB 中是新数据,缓存中是旧数据,数据不一致的情况。在这种逻辑下,只有保证写数据库和更新缓存在同一个事务中,才能保证最终一致性。

a.基于定时任务来实现
  • 先写入数据库,然后在写入数据库所在的事务中,插入一条记录到任务表,该记录会存储需要更新的缓存 key 和 value。
  • 定时任务每秒扫描任务表,更新到缓存中,之后删除该记录。
b.基于消息队列实现
  • 首先写入数据库,然后发送带有缓存 key 和 value 的事务消息,此时需要有支持事务消息特性的消息队列。
  • 消费者消费该消息,更新到缓存中。
基于数据库的 binlog 日志

在这里插入图片描述

  • 应用直接写数据到数据库中。
  • 数据库更新 binlog 日志。
  • 利用 Canal 中间件读取 binlog 日志。
  • Canal 借助于限流组件按频率将数据发到 MQ 中。
  • 应用监控 MQ 通道,将 MQ 的数据更新到 Redis 缓存中。

可以看到这种方案对研发人员来说比较轻量,不用关心缓存层面,而且这个方案虽然比较重,但是却容易形成统一的解决方案。
备注说明: 上述的订阅 binlog 程序在 mysql 中有现成的中间件叫 canal,可以完成订阅 binlog 日志的功能。

链接:https://juejin.im/post/5ca1f4a251882543bf704316

保证缓存数据库的一致性是分布式系统中一个常见的难题,尤其是在高并发场景下。常见的做法包括: --- ### 一、缓存数据库一致性策略 #### 1. **先更新数据库,再更新缓存** - **流程**: - 更新数据库; - 更新缓存。 - **缺点**: - 如果更新缓存失败,会导致缓存数据库不一致; - 高并发下,可能有请求读取到旧缓存。 #### 2. **先更新数据库,再删除缓存(推荐)** - **流程**: - 更新数据库; - 删除缓存。 - **优点**: - 下次读请求会触发缓存重建,保证数据一致性; - **注意**: - 删除失败可能导致数据不一致,可通过消息队列或定时补偿机制处理。 #### 3. **先删除缓存,再更新数据库(延迟删)** - **流程**: - 删除缓存; - 更新数据库; - 延迟一段时间后再次删除缓存(防止更新过程中有旧缓存入)。 - **适用场景**: - 对一致性要求较高的场景。 --- ### 二、使用消息队列进行异步补偿 - 将数据库变更事件发布到消息队列(如 Kafka、RocketMQ); - 消费者监听变更事件,更新或删除缓存; - 可以实现最终一致性。 --- ### 三、使用分布式事务(强一致性) - 通过分布式事务框架(如 Seata)保证数据库缓存操作在同一个事务中; - 实现复杂、性能开销大,一般用于对一致性要求极高的场景。 --- ### 四、TTL(过期时间)机制 - 设置缓存的过期时间,即使缓存数据库不一致,也会在一定时间内自动失效; - 适合容忍短暂不一致的场景。 --- ### 五、Cache-Aside Pattern(旁路缓存) - 查询时先查缓存缓存无则查数据库缓存; - 更新时先更新数据库,再删除缓存; - 是目前最常用、实现简单的方式。 --- ### 示例代码(Spring Boot + Redis + MySQL) ```java @Service public class UserService { @Autowired private UserRepository userRepository; @Autowired private RedisTemplate<String, User> redisTemplate; public void updateUser(User user) { // 1. 更新数据库 userRepository.save(user); // 2. 删除缓存 String cacheKey = "user:" + user.getId(); redisTemplate.delete(cacheKey); } public User getUserById(Long id) { String cacheKey = "user:" + id; User user = redisTemplate.opsForValue().get(cacheKey); if (user == null) { // 缓存未命中,查询数据库 user = userRepository.findById(id).orElse(null); if (user != null) { // 缓存 redisTemplate.opsForValue().set(cacheKey, user, 5, TimeUnit.MINUTES); } } return user; } } ``` --- ### 六、注意事项 - 高并发下建议使用**延迟删 + 消息队列补偿**; - 不建议使用“先更新缓存再更新数据库”,容易出现脏数据; - 可以结合本地缓存和分布式缓存,使用统一的缓存管理策略; - 对一致性要求极高时,可考虑引入分布式事务,但需权衡性能。 ---
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值