使用redis缓存如何保证一致性

 

博主介绍:✌全网粉丝5W+,全栈开发工程师,从事多年软件开发,在大厂呆过。持有软件中级、六级等证书。可提供微服务项目搭建与毕业项目实战,博主也曾写过优秀论文,查重率极低,在这方面有丰富的经验✌

博主作品:《Java项目案例》主要基于SpringBoot+MyBatis/MyBatis-plus+MySQL+Vue等前后端分离项目,可以在左边的分类专栏找到更多项目。《Uniapp项目案例》有几个有uniapp教程,企业实战开发。《微服务实战》专栏是本人的实战经验总结,《Spring家族及微服务系列》专注Spring、SpringMVC、SpringBoot、SpringCloud系列、Nacos等源码解读、热门面试题、架构设计等。除此之外还有不少文章等你来细细品味,更多惊喜等着你哦

uniapp微信小程序面试题软考题免费使用,还可以使用微信支付,扫码加群。由于维护成本问题得不到解决,可能将停止线上维护。

文末获取联系精彩专栏推荐订阅 不然下次找不到哟

Java项目案例《100套》
https://blog.csdn.net/qq_57756904/category_12173599.html
uniapp小程序《100套》

https://blog.csdn.net/qq_57756904/category_12173599.html

有需求代码永远写不完,而方法才是破解之道,抖音有实战视频课程,某马某千等培训都是2万左右,甚至广东有本科院校单单一年就得3万4年就12万学费,而且还没有包括吃饭的钱。所以很划算了。另外博客左侧有源码阅读专栏,对于求职有很大帮助,当然对于工作也是有指导意义等。在大城市求职,你面试来回一趟多多少少都在12块左右,而且一般不会一次性就通过,还得面试几家。而如果你对源码以及微服务等有深度认识,这无疑给你的面试添砖加瓦更上一层楼。

最后再送一句:最好是学会了,而不是学废了!!

2

在 Java 中使用 Redis 缓存时,保证缓存与数据源(如数据库)的一致性是一个常见挑战。以下是一些在 Java 中实现缓存一致性的策略和实践:


1. 缓存更新策略

  • 写穿透(Write-Through)

    • 在更新数据库的同时,同步更新缓存。

    • 优点:缓存与数据库始终保持一致。

    • 缺点:写操作性能较低,因为每次写操作都需要更新缓存。

    public void updateUser(int userId, User user) {
        // 更新数据库
        userDao.update(user);
        // 同步更新缓存
        String key = "user:" + userId;
        redisTemplate.opsForValue().set(key, user);
    }

  • 写回(Write-Back)

    • 先更新缓存,延迟写入数据库。

    • 优点:写操作性能高。

    • 缺点:存在数据丢失风险(如缓存宕机),且缓存与数据库可能短暂不一致。

    public void updateUser(int userId, User user) {
        // 先更新缓存
        String key = "user:" + userId;
        redisTemplate.opsForValue().set(key, user);
        // 延迟写入数据库(异步)
        executorService.submit(() -> userDao.update(user));
    }

  • 失效策略(Cache Invalidation)

    • 更新数据库后,使缓存失效,下次读取时重新加载数据。

    • 优点:简单易实现。

    • 缺点:可能存在缓存击穿问题。

    public void updateUser(int userId, User user) {
        // 更新数据库
        userDao.update(user);
        // 使缓存失效
        String key = "user:" + userId;
        redisTemplate.delete(key);
    }


2. 双写一致性

  • 在更新数据库和缓存时,确保两者的操作是原子的。

  • 如果无法保证原子性,可以通过分布式锁(如 Redis 的 SETNX)来确保一致性。

public void updateUserWithLock(int userId, User user) {
    String lockKey = "lock:user:" + userId;
    String cacheKey = "user:" + userId;
    // 获取分布式锁
    Boolean locked = redisTemplate.opsForValue().setIfAbsent(lockKey, "locked", 10, TimeUnit.SECONDS);
    if (locked != null && locked) {
        try {
            // 更新数据库
            userDao.update(user);
            // 更新缓存
            redisTemplate.opsForValue().set(cacheKey, user);
        } finally {
            // 释放锁
            redisTemplate.delete(lockKey);
        }
    } else {
        throw new RuntimeException("Failed to acquire lock");
    }
}

3. 延迟双删策略

  • 在更新数据库后,先删除缓存,延迟一段时间后再删除一次缓存。

  • 目的是解决缓存与数据库不一致的短暂窗口问题。

public void updateUserWithDoubleDelete(int userId, User user) {
    // 更新数据库
    userDao.update(user);
    // 第一次删除缓存
    String cacheKey = "user:" + userId;
    redisTemplate.delete(cacheKey);
    // 延迟第二次删除缓存
    executorService.schedule(() -> redisTemplate.delete(cacheKey), 1, TimeUnit.SECONDS);
}

4. 使用版本号或时间戳

  • 在缓存中存储数据的版本号或时间戳,每次更新时检查版本号或时间戳,确保缓存数据是最新的。

public void updateUserWithVersion(int userId, User user) {
    // 获取当前版本号
    int currentVersion = userDao.getVersion(userId);
    // 更新数据库并增加版本号
    user.setVersion(currentVersion + 1);
    userDao.update(user);
    // 更新缓存,带上版本号
    String cacheKey = "user:" + userId;
    redisTemplate.opsForValue().set(cacheKey, user);
}

5. 设置合理的缓存过期时间

  • 为缓存设置合理的 TTL(Time to Live),即使缓存与数据库短暂不一致,也能通过过期机制自动恢复。

  • 适合对一致性要求不高的场景。

public User getUser(int userId) {
    String cacheKey = "user:" + userId;
    User user = redisTemplate.opsForValue().get(cacheKey);
    if (user == null) {
        user = userDao.get(userId);
        if (user != null) {
            redisTemplate.opsForValue().set(cacheKey, user, 1, TimeUnit.HOURS); // 设置 1 小时过期
        }
    }
    return user;
}

6. 处理缓存与数据库不一致的场景

  • 缓存穿透:查询不存在的数据,导致请求直接打到数据库。

    • 解决方案:缓存空值,或使用布隆过滤器拦截无效请求。

    public User getUser(int userId) {
        String cacheKey = "user:" + userId;
        User user = redisTemplate.opsForValue().get(cacheKey);
        if (user == null) {
            user = userDao.get(userId);
            if (user == null) {
                // 缓存空值,避免缓存穿透
                redisTemplate.opsForValue().set(cacheKey, new User(), 1, TimeUnit.MINUTES);
            } else {
                redisTemplate.opsForValue().set(cacheKey, user, 1, TimeUnit.HOURS);
            }
        }
        return user;
    }

  • 缓存击穿:热点数据过期后,大量请求同时访问数据库。

    • 解决方案:使用互斥锁或永不过期的热点数据。

    public User getUserWithLock(int userId) {
        String cacheKey = "user:" + userId;
        User user = redisTemplate.opsForValue().get(cacheKey);
        if (user == null) {
            String lockKey = "lock:user:" + userId;
            Boolean locked = redisTemplate.opsForValue().setIfAbsent(lockKey, "locked", 10, TimeUnit.SECONDS);
            if (locked != null && locked) {
                try {
                    user = userDao.get(userId);
                    if (user != null) {
                        redisTemplate.opsForValue().set(cacheKey, user, 1, TimeUnit.HOURS);
                    }
                } finally {
                    redisTemplate.delete(lockKey);
                }
            } else {
                // 等待重试或返回旧数据
                user = redisTemplate.opsForValue().get(cacheKey);
            }
        }
        return user;
    }

  • 缓存雪崩:大量缓存同时失效,导致数据库压力骤增。

    • 解决方案:分散缓存过期时间,或使用高可用缓存架构。


7. 使用 Spring Cache 注解

  • 如果使用 Spring Boot,可以通过 @Cacheable@CachePut 和 @CacheEvict 注解简化缓存操作。

@Cacheable(value = "users", key = "#userId")
public User getUser(int userId) {
    return userDao.get(userId);
}

@CachePut(value = "users", key = "#user.id")
public User updateUser(User user) {
    userDao.update(user);
    return user;
}

@CacheEvict(value = "users", key = "#userId")
public void deleteUser(int userId) {
    userDao.delete(userId);
}

8. 监控与告警

  • 使用监控工具(如 Prometheus、Grafana)监控缓存的命中率、响应时间、一致性等指标。

  • 设置告警机制,当缓存与数据库不一致时,及时通知开发人员处理。


总结

在 Java 中使用 Redis 缓存时,保证一致性的关键在于:

  • 选择合适的缓存更新策略(如写穿透、失效策略)。

  • 使用分布式锁或版本号确保原子性。

  • 处理缓存穿透、击穿和雪崩等问题。

  • 结合 Spring Cache 注解简化缓存操作。

  • 通过监控和告警确保系统的稳定性。

3

你可能感兴趣的:(SpringCloud,缓存,redis,数据库)