使用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
更多推荐


所有评论(0)