数据库(MySQL)与缓存(Redis)彼此独立,更新数据就像要把同一封信分别送到两座房子。

我们只能追求这一核心结论:“最终一致”(允许短时间存在差异,但会迅速修复),绝对强一致无法用低成本实现。
假定两个线程在同一时间更新数据:
受网络延迟影响,若 B 的请求先抵达缓存、A 后抵达,缓存最后会成为旧值 18。
但“删除缓存”不会带来这一困扰:无论执行有先有后,删除后缓存都不存在;下次读取会先查库再回填,从而写入最新的 DB 值。
在极少见的情形下:
解决办法:延迟双删。即在更新 MySQL 并删除 Redis 后,等待 1~2 秒(等待可能的并发读回填完成),再删除一次 Redis,把旧值清理掉。
完成 DB 更新并提交事务后,再删除缓存,同时设置 TTL 作为兜底。
代码示例:
@Service @Slf4j public class UserService { @Autowired private UserDao userDao; @Autowired private RedisTemplate<String, Object> redisTemplate; private static final String CACHE_KEY = "user:"; // 查询:旁路缓存 public User getById(Long id) { String key = CACHE_KEY + id; // 1. 查缓存 User user = (User) redisTemplate.opsForValue().get(key); if (user != null) { return user; } // 2. 查数据库 user = userDao.selectById(id); if (user != null) { // 3. 回填缓存,必须设置过期时间(兜底) redisTemplate.opsForValue().set(key, user, 30, TimeUnit.MINUTES); } return user; } // 更新:先更新DB,后删缓存 @Transactional public void updateUser(User user) { // 1. 更新数据库 userDao.updateById(user); // 2. 删除缓存(注意:必须确保事务提交后再删,避免DB回滚导致缓存被误删) String key = CACHE_KEY + user.getId(); redisTemplate.delete(key); log.info("更新用户 id={}, 缓存已删除", user.getId()); } }缓存删除由事务同步器安排在 DB 真正提交之后,同时还要执行延迟二次删除。
代码示例:
@Service @Slf4j public class UserServiceStrict { @Autowired private UserDao userDao; @Autowired private RedisTemplate<String, Object> redisTemplate; // 用于延迟任务的线程池 private final ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(4); @Transactional public void updateUser(User user) { // 1. 更新数据库 userDao.updateById(user); String key = CACHE_KEY + user.getId(); // 2. 注册事务同步器,确保事务提交后再操作缓存 TransactionSynchronizationManager.registerSynchronization( new TransactionSynchronization() { @Override public void afterCommit() { // 2.1 立即删除缓存 redisTemplate.delete(key); log.info("事务提交后立即删除缓存 key={}", key); // 2.2 延迟 1.5 秒后再次删除(防止并发读回填旧数据) scheduler.schedule(() -> { try { redisTemplate.delete(key); log.info("延迟删除缓存 key={}", key); } catch (Exception e) { log.error("延迟删除缓存失败", e); } }, 1500, TimeUnit.MILLISECONDS); } } ); } }业务若对最终一致性要求极高,可让 Canal 监听 binlog 并异步删除缓存。应用层仅更新 DB,变更事件由 Canal 解析,随后自动删除 Redis。
由框架回调触发的 Canal 消费端伪代码示例:
@Component @Slf4j public class CanalCacheListener { @Autowired private RedisTemplate<String, Object> redisTemplate; public void onUserChange(Long userId, String eventType) { if ("UPDATE".equals(eventType) || "DELETE".equals(eventType)) { String key = "user:" + userId; redisTemplate.delete(key); log.info("Binlog 异步清理缓存 userId={}", userId); } } }| 你的业务场景 | 建议采用方案 |
|---|---|
| 普通管理后台,低并发 | 使用基础版(5.1)+ TTL 即可 |
| 用户端读取并发高,要求数据较新 | 采用严谨版(5.2)延迟双删 |
| 金融/库存需要强校验,绝不允许出错 | 不采用缓存,而是直接读取数据库 |
| 分布式微服务,优先考虑解耦 | 采用 Binlog 异步删除(5.3) |
以上详细介绍了Redis缓存与数据库一致性的原理及最佳实践,想了解更多Redis缓存与数据库一致性资料,可继续关注本站其他相关文章!
您或许感兴趣的文章: