如何保证Redis和Mysql数据缓存一致性?

1.读数据流程:

bash 复制代码
1. 先读缓存
2. 缓存命中 → 直接返回
3. 缓存未命中 → 读数据库 → 写入缓存 → 返回数据

2.写数据流程:

bash 复制代码
1. 更新数据库
2. 删除缓存(而不是更新缓存)

3.完整代码示例

3.1.用户服务实现

java 复制代码
@Service
@Slf4j
public class UserService {
    
    @Autowired
    private UserMapper userMapper;
    
    @Autowired
    private RedisCache redisCache;
    
    // 缓存key前缀
    private static final String USER_CACHE_KEY = "user:";
    
    /**
     * 根据ID获取用户(读操作 - 旁路缓存)
     */
    public User getUserById(Long userId) {
        String cacheKey = USER_CACHE_KEY + userId;
        
        // 1. 先从缓存读取
        User user = redisCache.get(cacheKey);
        if (user != null) {
            log.info("缓存命中,用户ID: {}", userId);
            return user;
        }
        
        // 2. 缓存未命中,查询数据库
        log.info("缓存未命中,查询数据库,用户ID: {}", userId);
        user = userMapper.selectById(userId);
        
        if (user == null) {
            return null;
        }
        
        // 3. 将数据写入缓存,设置30分钟过期
        redisCache.set(cacheKey, user, 30 * 60);
        log.info("数据写入缓存,用户ID: {}", userId);
        
        return user;
    }
    
    /**
     * 更新用户(写操作 - 先更新数据库,再删除缓存)
     */
    @Transactional
    public boolean updateUser(User user) {
        try {
            // 1. 更新数据库
            int result = userMapper.updateById(user);
            if (result <= 0) {
                return false;
            }
            
            // 2. 删除缓存
            String cacheKey = USER_CACHE_KEY + user.getUserId();
            redisCache.delete(cacheKey);
            log.info("更新用户成功,删除缓存,用户ID: {}", user.getUserId());
            
            return true;
        } catch (Exception e) {
            log.error("更新用户失败,用户ID: {}", user.getUserId(), e);
            throw new RuntimeException("更新用户失败", e);
        }
    }
    
    /**
     * 新增用户
     */
    @Transactional
    public boolean addUser(User user) {
        try {
            // 1. 插入数据库
            int result = userMapper.insert(user);
            if (result <= 0) {
                return false;
            }
            
            // 2. 新增用户不需要立即缓存,等第一次读取时自然缓存
            // 但如果需要立即使用,可以在这里设置缓存
            // String cacheKey = USER_CACHE_KEY + user.getUserId();
            // redisCache.set(cacheKey, user, 30 * 60);
            
            log.info("新增用户成功,用户ID: {}", user.getUserId());
            return true;
        } catch (Exception e) {
            log.error("新增用户失败", e);
            throw new RuntimeException("新增用户失败", e);
        }
    }
    
    /**
     * 删除用户
     */
    @Transactional
    public boolean deleteUser(Long userId) {
        try {
            // 1. 删除数据库(逻辑删除)
            User user = new User();
            user.setUserId(userId);
            user.setDelFlag("1"); // 逻辑删除标志
            int result = userMapper.updateById(user);
            
            if (result <= 0) {
                return false;
            }
            
            // 2. 删除缓存
            String cacheKey = USER_CACHE_KEY + userId;
            redisCache.delete(cacheKey);
            log.info("删除用户成功,删除缓存,用户ID: {}", userId);
            
            return true;
        } catch (Exception e) {
            log.error("删除用户失败,用户ID: {}", userId, e);
            throw new RuntimeException("删除用户失败", e);
        }
    }
}

3.2 控制器层

java 复制代码
@RestController
@RequestMapping("/user")
@Slf4j
public class UserController {
    
    @Autowired
    private UserService userService;
    
    /**
     * 获取用户信息
     */
    @GetMapping("/{userId}")
    public AjaxResult getUser(@PathVariable Long userId) {
        try {
            User user = userService.getUserById(userId);
            if (user == null) {
                return AjaxResult.error("用户不存在");
            }
            return AjaxResult.success(user);
        } catch (Exception e) {
            log.error("获取用户信息失败,用户ID: {}", userId, e);
            return AjaxResult.error("系统异常");
        }
    }
    
    /**
     * 更新用户信息
     */
    @PutMapping
    public AjaxResult updateUser(@RequestBody User user) {
        try {
            boolean result = userService.updateUser(user);
            return result ? AjaxResult.success() : AjaxResult.error("更新失败");
        } catch (Exception e) {
            log.error("更新用户信息失败,用户ID: {}", user.getUserId(), e);
            return AjaxResult.error("更新失败");
        }
    }
    
    /**
     * 新增用户
     */
    @PostMapping
    public AjaxResult addUser(@RequestBody User user) {
        try {
            boolean result = userService.addUser(user);
            return result ? AjaxResult.success() : AjaxResult.error("新增失败");
        } catch (Exception e) {
            log.error("新增用户失败", e);
            return AjaxResult.error("新增失败");
        }
    }
    
    /**
     * 删除用户
     */
    @DeleteMapping("/{userId}")
    public AjaxResult deleteUser(@PathVariable Long userId) {
        try {
            boolean result = userService.deleteUser(userId);
            return result ? AjaxResult.success() : AjaxResult.error("删除失败");
        } catch (Exception e) {
            log.error("删除用户失败,用户ID: {}", userId, e);
            return AjaxResult.error("删除失败");
        }
    }
}

4.使用建议

1. 缓存key设计

java 复制代码
// 使用统一的命名规范
private static final String USER_CACHE_KEY = "user:";
private static final String PRODUCT_CACHE_KEY = "product:";
private static final String ORDER_CACHE_KEY = "order:";

2. 过期时间策略

java 复制代码
// 根据业务特点设置不同的过期时间
redisCache.set(key, value, 30 * 60);        // 用户数据:30分钟
redisCache.set(key, value, 5 * 60);         // 商品数据:5分钟  
redisCache.set(key, value, 24 * 60 * 60);   // 配置数据:24小时

3. 监控和日志

java 复制代码
// 添加详细的日志,便于排查问题
log.info("缓存命中,key: {}", key);
log.info("缓存未命中,查询数据库,key: {}", key);
log.info("删除缓存,key: {}", key);

5.CAP理论的基本概念

CAP理论主要用于描述分布式系统中的三大特性:

  1. 一致性(Consistency):所有节点在同一时刻看到的数据是一致的。即每次读操作都能返回最新的写入结果,确保数据的正确性。

  2. 可用性(Availability):系统能够保证每个请求都会收到响应,无论是成功的响应还是失败的响应。即使某些节点出现故障,系统仍然能够继续提供服务。

  3. 分区容忍性(Partition Tolerance):系统能够容忍网络分区,即节点之间的通信中断,并且在出现分区时仍然能够继续运作。

6.为什么选择"更新DB + 删除缓存"而不是"更新DB + 更新缓存"?

方案对比:

方案 优点 缺点
更新DB + 删除缓存 简单、避免并发写问题、减少不必要的缓存更新 有极小的不一致窗口期
更新DB + 更新缓存 数据一致性更好 并发写时可能脏数据、浪费资源更新不常读的数据

并发问题分析:

场景:两个并发写操作

  • 线程A更新用户信息 → 更新DB → 更新缓存

  • 线程B更新用户信息 → 更新DB → 更新缓存

可能出现:

java 复制代码
DB中:用户B的数据
缓存中:用户A的数据(因为A的更新晚于B)

而使用删除缓存策略,可以避免这种并发写导致的脏数据问题。

针对删除缓存异常的情况,可以使用 2 个方案避免:

1.消息队列方案:将第二个操作(删除缓存)要操作的数据加入到消息队列,由消费者来操作数据。
2.订阅 MySQL binlog,再操作缓存:

先更新数据库,再删缓存」的策略的第一步是更新数据库,那么更新数据库成功,就会产生一条变更日志,记录在 binlog 里。于是我们就可以通过订阅 binlog 日志,拿到具体要操作的数据,然后再执行缓存删除。

相关推荐
大吱佬2 小时前
八股速记(自用)
java
征尘bjajmd2 小时前
Java使用okhttp发送get、post请求
java·服务器·数据库
程序编程- Java2 小时前
三角洲行动-java游戏程序
java·游戏程序·安全架构·玩游戏
陈佳梁2 小时前
构造器(详解)
java·开发语言
麦烤楽鸡翅2 小时前
【模板】二维前缀和 (牛客)
java·c++·算法·秋招·春招·二维前缀和·面试算法题
清风6666662 小时前
基于单片机的智能高温消毒与烘干系统设计
数据库·单片机·嵌入式硬件·毕业设计·课程设计·期末大作业
Databend2 小时前
Databend 十月月报:存储过程正式可用,数据流程全面自动化
数据库
Mos_x2 小时前
集成RabbitMQ+MQ常用操作
java·后端
wangjialelele2 小时前
MySQL操作库
数据库·mysql·oracle