redis读写一致问题
title: redis读写一致问题
date: 2025-05-18 11:11:31
tags: redis
categories: redis的问题方案
Redis读写一致问题
条件:
数据库此时的数据为10,redis此时的数据也为10
业务流程:
操作数据库使得数据库的数据为20,删除redis里面的数据保证读写一致
先删缓存,再操作数据库
出现读写不一致情况:
线程1(业务) | 线程2(并发线程) |
---|---|
删除缓存 | |
查询缓存,没有命中,查询数据库(数据库查到为10,下一步将10写入redis) | |
将10写入缓存 | |
更新数据库,将数据库中的数据改为20 |
最终情况
redis里面的数据 | 数据库里面的数据 |
---|---|
10 | 20 |
出现数据不一致情况
先操作数据库,再删除缓存
线程1(并发线程) | 线程2(业务线程) |
---|---|
查询缓存未命中,查询数据库(下一步:将缓存更新为10) | |
更新数据库 v=20 | |
删除缓存 | |
写入缓存数据10 |
最终情况:
redis数据 | 数据库数据 |
---|---|
10 | 20 |
两个方法选择原则
适用策略 | 典型场景 | 是否推荐使用延迟双删 |
---|---|---|
先删缓存 → 后更新数据库 | 高一致性业务(余额、库存) | ✅ 一定要延迟双删! |
先更新数据库 → 后删缓存 | 低一致性业务(资料、文章内容) | ❌ 可以不用延迟双删 |
解决方案:双写一致性
读操作没啥问题按照老流程
延时双删
问题 | 答案 |
---|---|
先删缓存还是先改数据库? | 先删缓存! 避免并发写入旧值 |
为什么删两次? | 防止“改库之后,又有人写了旧值到缓存” |
为什么要延迟删? | 给并发线程一个“写入脏缓存”的机会,然后再清理掉它 |
缺点:
问题点 | 延迟双删解决得了么? | 推荐改进方式 |
---|---|---|
并发窗口写入脏缓存 | ❌ 只能删最后一个 | 分布式锁 + 双删 / MQ |
延迟时间难控制 | ❌ 不可预测 | MQ 或 Canal 机制更精准 |
异步删除失败风险 | ❌ 会丢失删除 | 使用可靠任务队列 / Redis 持久化 |
操作复杂、代码维护困难 | ❌ 容易遗漏 key | 封装中间件、使用 AOP统一处理 |
给他加锁
读写都加锁
如图,程序运行串行化,性能低
引入共享锁和排他锁机制
共享锁:读锁readLock,加锁之后,其他线程可以共享读操作
排他锁:独占锁writeLock也叫,加锁之后,阻塞其他线程读写操作
代码Demo
import org.redisson.api.RReadWriteLock;
import org.redisson.api.RedissonClient;
import org.redisson.api.RLock;
import java.util.concurrent.TimeUnit;public class UserService {private final RedissonClient redissonClient;private final RedisService redisService; // 你封装的 Redis 工具类private final UserRepository userRepository; // 你操作数据库的类public UserService(RedissonClient redissonClient, RedisService redisService, UserRepository userRepository) {this.redissonClient = redissonClient;this.redisService = redisService;this.userRepository = userRepository;}// 读操作:加“读锁”public User getUserById(Long userId) {String key = "user:" + userId;String lockKey = "lock:user:" + userId;RReadWriteLock rwLock = redissonClient.getReadWriteLock(lockKey);RLock readLock = rwLock.readLock();try {readLock.lock(5, TimeUnit.SECONDS); // 加读锁,防止同时写入User user = redisService.get(key); // 先查缓存if (user != null) {return user;}// 缓存未命中 → 查数据库并回写缓存user = userRepository.findById(userId);if (user != null) {redisService.set(key, user, 10, TimeUnit.MINUTES);}return user;} finally {readLock.unlock(); // 释放读锁}}// 写操作:加“写锁”public void updateUser(User user) {Long userId = user.getId();String key = "user:" + userId;String lockKey = "lock:user:" + userId;RReadWriteLock rwLock = redissonClient.getReadWriteLock(lockKey);RLock writeLock = rwLock.writeLock();try {writeLock.lock(10, TimeUnit.SECONDS); // 加写锁,防止并发读/写redisService.del(key); // 删除缓存(第一次)userRepository.save(user); // 更新数据库// 第二次删除可延迟做(避免并发写入旧值)Thread.sleep(500); // 模拟延迟redisService.del(key); // 延迟删除(第二次)} catch (InterruptedException e) {e.printStackTrace();} finally {writeLock.unlock(); // 释放写锁}}
}
import org.redisson.api.RReadWriteLock;
import org.redisson.api.RedissonClient;
import org.redisson.api.RLock;
import java.util.concurrent.TimeUnit;public class UserService {private final RedissonClient redissonClient;private final RedisService redisService; // 你封装的 Redis 工具类private final UserRepository userRepository; // 你操作数据库的类public UserService(RedissonClient redissonClient, RedisService redisService, UserRepository userRepository) {this.redissonClient = redissonClient;this.redisService = redisService;this.userRepository = userRepository;}// 读操作:加“读锁”public User getUserById(Long userId) {String key = "user:" + userId;String lockKey = "lock:user:" + userId;RReadWriteLock rwLock = redissonClient.getReadWriteLock(lockKey);RLock readLock = rwLock.readLock();try {readLock.lock(5, TimeUnit.SECONDS); // 加读锁,防止同时写入User user = redisService.get(key); // 先查缓存if (user != null) {return user;}// 缓存未命中 → 查数据库并回写缓存user = userRepository.findById(userId);if (user != null) {redisService.set(key, user, 10, TimeUnit.MINUTES);}return user;} finally {readLock.unlock(); // 释放读锁}}// 写操作:加“写锁”public void updateUser(User user) {Long userId = user.getId();String key = "user:" + userId;String lockKey = "lock:user:" + userId;RReadWriteLock rwLock = redissonClient.getReadWriteLock(lockKey);RLock writeLock = rwLock.writeLock();try {writeLock.lock(10, TimeUnit.SECONDS); // 加写锁,防止并发读/写redisService.del(key); // 删除缓存(第一次)userRepository.save(user); // 更新数据库// 第二次删除可延迟做(避免并发写入旧值)Thread.sleep(500); // 模拟延迟redisService.del(key); // 延迟删除(第二次)} catch (InterruptedException e) {e.printStackTrace();} finally {writeLock.unlock(); // 释放写锁}}
}
中间件解决方案
异步通知保证数据的最终一致性
canal是基于mysql的主从同步来实现的
二进制日志(BINLOG)记录了所有的 DDL(数据定义语言)语句和 DML(数据操纵语言)语句,但不包括数据查询(SELECT、SHOW)语句。