博主介绍:✌全网粉丝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 中实现缓存一致性的策略和实践:
写穿透(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);
}
在更新数据库和缓存时,确保两者的操作是原子的。
如果无法保证原子性,可以通过分布式锁(如 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");
}
}
在更新数据库后,先删除缓存,延迟一段时间后再删除一次缓存。
目的是解决缓存与数据库不一致的短暂窗口问题。
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);
}
在缓存中存储数据的版本号或时间戳,每次更新时检查版本号或时间戳,确保缓存数据是最新的。
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);
}
为缓存设置合理的 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;
}
缓存穿透:查询不存在的数据,导致请求直接打到数据库。
解决方案:缓存空值,或使用布隆过滤器拦截无效请求。
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;
}
缓存雪崩:大量缓存同时失效,导致数据库压力骤增。
解决方案:分散缓存过期时间,或使用高可用缓存架构。
如果使用 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);
}
使用监控工具(如 Prometheus、Grafana)监控缓存的命中率、响应时间、一致性等指标。
设置告警机制,当缓存与数据库不一致时,及时通知开发人员处理。
在 Java 中使用 Redis 缓存时,保证一致性的关键在于:
选择合适的缓存更新策略(如写穿透、失效策略)。
使用分布式锁或版本号确保原子性。
处理缓存穿透、击穿和雪崩等问题。
结合 Spring Cache 注解简化缓存操作。
通过监控和告警确保系统的稳定性。
3