当前位置: 首页 > news >正文

Java【缓存设计】定时任务+分布式锁实战:Redis vs Redisson实现状态自动扭转以及全量刷新预热机制


目录

1. 前言

插播一条消息~

2. 正文

2.1 缓存存在的价值与策略

2.2 定时任务实现状态扭转

2.2.1 Redis 原生命令 + Lua脚本实现分布式锁

2.2.2 Redisson框架实现高级分布式锁

2.3 RabbitMQ异步解耦:全局缓存更新策略

2.4 全量刷新缓存预热机制

3. 小结


1. 前言

现代租房系统动辄百万房源、千万用户,每天面临着海量的查询请求(如条件筛选、价格区间统计)和频繁的状态变更(如出租到期、用户信息更新)。如果每次请求都直接打到数据库,不仅响应慢,还极易引发DB瓶颈甚至宕机。

为此,我们必须引入缓存层——Redis作为工业级分布式缓存中间件,凭借其高性能、丰富数据结构、原子操作与持久化能力,成为我们的首选。但“用Redis并不等于正确使用Redis”。如何规避典型陷阱?如何保证缓存与数据库的一致性?如何在多节点部署下安全执行定时任务?这些问题才是真正的挑战。

本文将基于真实的租房系统场景,带你彻底解决这类问题。我们会重点剖析:

  • 如何用定时任务批量处理状态扭转
  • 在分布式环境下如何用分布式锁避免重复执行
  • Redis原生方案 vs Redisson框架的优劣对比

Feast不止于理论,我会手把手带你写出生产可用的代码,并分享实际开发中的坑点应对方案。


插播一条消息~

🔍十年经验淬炼 · 系统化AI学习平台推荐

系统化学习AI平台https://www.captainbed.cn/scy/

  • 📚 完整知识体系:从数学基础 → 工业级项目(人脸识别/自动驾驶/GANs),内容由浅入深
  • 💻 实战为王:每小节配套可运行代码案例(提供完整源码)
  • 🎯 零基础友好:用生活案例讲解算法,无需担心数学/编程基础

🚀 特别适合

  • 想系统补强AI知识的开发者
  • 转型人工智能领域的从业者
  • 需要项目经验的学生

2. 正文

2.1 缓存存在的价值与策略

在深入定时任务之前,我们先快速回顾下缓存的核心知识,这对理解整体架构很重要。

缓存策略三大核心

  1. 读取缓存:先查缓存,命中则返回,未命中查DB并回写缓存
  2. 写入缓存:更新DB后,删除或更新对应缓存(怕大家凌乱,具体可以看下文)
  3. 失效策略:给缓存设置合理的过期时间,避免永久存储脏数据

缓存数据类型

  • 热点数据:用户信息、配置信息等高频访问数据
  • 复杂查询结果:如价格区间统计、多表关联查询结果
  • 会话信息:用户登录Token、个性化设置等

缓存架构选择

对于分布式系统,我们选择Redis分布式缓存,主要原因:

  • 跨服务共享数据
  • 高性能读写能力
  • 丰富的数据结构支持
  • 持久化与高可用保障

数据一致性保障

Redis作为缓存,MySQL作为持久化存储,我们需要保障两者数据的一致性,常用策略:

策略

优点

缺点

适用场景

先更新DB,后删除缓存

简单、高效

可能短暂不一致

读多写少场景

先删除缓存,后更新DB

减少不一致时间

删除后更新前可能有脏数据

写多读少场景

异步同步

性能最好

实现复杂,可能丢失数据

最终一致性要求场景

缓存加载策略

  • 全量加载:启动时加载所有数据,适合数据量小场景(就是这个例子😁)
  • 增量加载:按需加载,节省内存但首次访问慢
  • 定时加载:定期刷新,适合数据变更不频繁场景

缓存高并发三大经典问题

这是面试官最爱问的问题,也是线上事故的高发地带。我们逐个击破:

问题

定义

缓解措施

缓存穿透

查询不存在的数据(既无缓存也无DB),导致大量请求直达DB

1. 对空结果也缓存(带短TTL)
2. 使用布隆过滤器(Bloom Filter)提前拦截非法key

缓存击穿

热点Key到期瞬间大量并发读,均未命中缓存

互斥锁(mutex lock):仅一个线程查DB并回填,其余阻塞或降级

缓存雪崩

大片缓存Key同时过期(或Redis宕机),瞬间涌入巨大流量冲垮DB

1. 设置随机TTL避免集体失效
2. 多级缓存(本地Caffeine + 远程Redis)缓冲冲击

现在我们对缓存有了基本认知,接下来进入正题:如何用定时任务+分布式锁解决房源状态同步问题。


2.2 定时任务实现状态扭转

背景说明

租房系统中,房源有三种核心状态:

  • 可出租(UP)
  • 已出租(RENTING)
  • 已下线(DOWN)

房东签署租赁合约后,房源状态变为【出租中】(RENTING)。合约到期时间明确(例:2025-06-01 00:00:00),系统需在该时刻后将其自动改为【待上线】(UP),释放给新租客。

但不能让前端或管理员手动改!原因如下:

  1. 租赁合同可能几千条并发结束;
  2. 时间精确性要求高(零点整切换);
  3. 若忘记操作会导致房源长期挂失无法上架;

所以——引入每日凌晨批量校验 + 更正状态是刚需动作。

业务代码实现

先看基础版的业务逻辑代码,后面我们再给这个代码加"分布式锁"的保护壳:

// 查询全部已出租房源
List<HouseStatus> rentHouseStatusList = houseStatusMapper.selectList(new LambdaQueryWrapper<HouseStatus>().eq(HouseStatus::getStatus, HouseStatusEnum.RENTING.name())
);// 过滤需要扭转状态的房源列表(出租到期时间)
List<HouseStatus> needChangeHouseStatusList = rentHouseStatusList.stream().filter(houseStatus -> null != houseStatus.getRentEndTime()&& 0 > TimestampUtil.calculateDifferenceMillis(TimestampUtil.getCurrentSeconds(), houseStatus.getRentEndTime())).collect(Collectors.toList());// 批量更新房源状态
for (HouseStatus housestatus:needChangeHouseStatusList) {HouseStatusEditReqDTO houseStatusEditReqDTO = new HouseStatusEditReqDTO();housestatus.setHouseId(housestatus.getHouseId());houseStatusEditReqDTO.setStatus(HouseStatusEnum.UP.name());editStatus(houseStatusEditReqDTO);
}

这段代码在单机环境下运行没问题,但在分布式部署时就会出现重复执行的问题。

假设我们部署了3个实例,那么零点时分:

  1. 实例1执行任务,更新了100条房源状态
  2. 实例2执行同样任务,又更新了相同的100条房源
  3. 实例3也执行了相同任务...

这不仅浪费资源,更可能导致业务逻辑错误!

解决方案:分布式锁

我们需要一种机制确保同一时间只有一个实例能执行任务,这就是分布式锁的核心价值。

下面我们分别用两种方式实现分布式锁。


2.2.1 Redis 原生命令 + Lua脚本实现分布式锁

这是最经典的实现方式。优势在于无外部依赖升级成本低,劣势为需自行处理超时续期和死锁检测逻辑。

完整实现代码

// 定时任务实现扭转房源状态
// 每日零点开始执行定时任务
@Scheduled(cron = "0 0 0 * * ?")
public void scheduledHouseStatus(){log.info("开始执行房源状态扭转任务");// 加redis分布式锁,锁的value来判断所是否为当前线程所有String value = UUID.randomUUID().toString();try {Boolean lock = redisService.setCacheObjectIfAbsent(LOCK_KEY, value, 180L, TimeUnit.SECONDS);if(Boolean.TRUE.equals( lock)){// 业务代码(上面2.2节的代码)doHouseStatusChangeBusiness();}else{// 获取锁失败,跳过执行log.info("定时任务被其他实例执行");}} finally {// 解锁,只能解锁自己redisService.cad(LOCK_KEY, value);}
}

关键方法解析

加锁方法:使用Redis的SETNX命令(SET if Not exists)

/*** 缓存String数据,如果该键不存在则存储,并设置数据有效时间,若已存在则不存储。** @param key      缓存的键值* @param value    缓存的值* @param timeout  时间* @param timeUnit 时间单位* @param <T> 对象类型* @return 是否缓存了对象   如果key已经存在,则返回false,否则返回true*/
public <T> Boolean setCacheObjectIfAbsent(final String key, final T value, final long timeout, final TimeUnit timeUnit) {return redisTemplate.opsForValue().setIfAbsent(key, value, timeout, timeUnit);
}

解锁方法:使用Lua脚本保证原子性

/*** 删除指定值对应的 Redis 中的键值(compare and delete)** @param key   缓存key* @param value value* @return 是否完成了比较并删除*/
public boolean cad(String key, String value) {if (key.contains(StringUtils.SPACE) || value.contains(StringUtils.SPACE)) {return false;}String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";// 通过lua脚本原子验证令牌和删除令牌Long result = (Long) redisTemplate.execute(new DefaultRedisScript<>(script, Long.class),Collections.singletonList(key),value);return !Objects.equals(result, 0L);
}

为什么需要Lua脚本?

简单用get+del两步操作不是原子性的,期间可能发生:

  1. 实例A检查value匹配,准备执行del
  2. 锁过期,实例B获得了锁
  3. 实例A执行del,误删了实例B的锁

Lua脚本保证判断和删除在同一次原子操作中完成


2.2.2 Redisson框架实现高级分布式锁

为什么选择Redisson?

虽然Redis原生方案能 work,但需要考虑太多细节:

  • 锁过期时间设置多少合适?
  • 如何处理锁续期问题?
  • 怎样避免误删其他实例的锁?

相比手动封装Lua,使用成熟的第三方库无疑开发效率更高,并天然提供watch-dog自动续期、公平锁/红锁选举算法等能力。

Redisson帮我们封装了这些复杂性,提供了更健壮的分布式锁实现。

完整实现代码

@Scheduled(cron = "0 0 0 * * ?")
public void scheduledHouseStatus2(){log.info("开始执行房源状态扭转任务");// 加redisson分布式锁RLock lock = redissonLockService.acquire(LOCK_KEY, -1);if(null == lock){log.info("定时任务被其他实例执行");return;}try {// 业务代码doHouseStatusChangeBusiness();} finally {if(lock.isLocked() && lock.isHeldByCurrentThread()){redissonLockService.releaseLock(lock);}}
}

Redisson锁服务封装

@Slf4j
@RequiredArgsConstructor
public class RedissonLockService {/*** redis操作客户端*/private final RedissonClient redissonClient;/*** 获取锁** @param lockKey        锁的key,唯一标识,建议模块名+唯一键* @param expire         超时时间,单位毫秒,传入-1自动续期* @return 获取到的RLock实例,为null则获取失败*/public RLock acquire(String lockKey, long expire) {try {final RLock lockInstance = redissonClient.getLock(lockKey);// 注意:如果tryLock指定了leaseTime>0就不会续期。参考 RedissonLock类的tryAcquireAsync方法的实现lockInstance.lock(expire, TimeUnit.MILLISECONDS);return lockInstance;} catch (Exception e) {return null;}}/*** 释放锁。注意:必须和获取锁在一个线程中** @param lockInstance 锁的实例,acquire返回的* @return 释放成功返回true,否则返回false*/public boolean releaseLock(RLock lockInstance) {if (lockInstance.isHeldByCurrentThread()) {lockInstance.unlock();return true;}return false;}
}

两种方案对比

特性

Redis原生实现

Redisson实现

实现复杂度

高,需自己处理各种边界情况

低,API简单易用

锁续期

需手动实现

内置看门狗自动续期

可重入性

需手动实现

内置支持

公平锁

不支持

支持

性能

略高(无额外开销)

略低(有额外功能开销)

可靠性

依赖自己实现细节

久经生产环境考验

生产环境建议:除非有极致性能要求,否则推荐使用Redisson


2.3 RabbitMQ异步解耦:全局缓存更新策略

前面提到的是被动定期轮询修正状态的做法,然而现实中很多变化其实是由其他事件主动驱动发生的 —— 如房东上传新版身份证扫描件(认证级别提高) ⇒ 展示徽章样式改变;亦或者用户停用了某个优惠券模板 ⇒ 旗下已发布的房源应同步关闭特惠标……

这些变更并非周期规律发生,也不能等到第二天凌晨统一处理。于是我们借助事件驱动思维,通过发布订阅模式将跨模块间的耦合解开,实现高效灵活的消息通知机制。

这里使用RabbitMQ实现异步解耦:

@Component
@Slf4j
@RabbitListener(bindings = {@QueueBinding(value = @Queue,exchange = @Exchange(value = RabbitConfig.EXCHANGE_NAME, type = ExchangeTypes.FANOUT))
})
public class EditAppUserMessageReceiver {@Autowiredprivate IHouseService houseService;@RabbitHandlerpublic void process(AppUserDTO appUserDTO){if(null == appUserDTO || null == appUserDTO.getUserId()){log.error("用户信息为空");return;}log.info("MQ成功收到消息,message:{}", JsonUtil.obj2String(appUserDTO));try {List<Long> houseIds = houseService.listByUserId(appUserDTO.getUserId());for (Long houseId : houseIds){houseService.cacheHouse(houseId);}} catch (Exception e) {log.error("处理用户更新时,更新房源缓存异常,appUserDTO:{}",JsonUtil.obj2String(appUserDTO), e);}}
}

关键设计要点

  1. 使用发布/订阅模式(Fanout Exchange):一个用户消息需要被多个服务消费
  2. 异步处理:避免同步调用阻塞主流程
  3. 异常处理:保证消息消费的可靠性

2.4 全量刷新缓存预热机制

有时候我们会遇见这种情况:

  • 新机房裁撤上线后首次查询极其缓慢?
  • 系统重启导致缓存全部清空前端访问出现短暂瘫痪?

上述就是典型的“缓存完全miss冷启动”效应,尤其对搜索筛选类接口打击巨大。为了避免首波冲击压垮DB集群,我们必须建立全量Warmup Preload Script在服务上线前提前准备好关键路径上的热点数据。

最后我们看下如何做全量缓存刷新,这是在系统启动或数据大规模变更时使用的策略:

public void refreshHouseIds() {// 查询全量城市列表(2级城市)List<SysRegion> sysRegions = regionMapper.selectList(new LambdaQueryWrapper<SysRegion>().eq(SysRegion::getLevel, 2));for(SysRegion sysRegion: sysRegions){// 删除当前城市下所有的房源列表redisLong cityId = sysRegion.getId();redisService.removeForAllList(CITY_HOUSE_PREFIX + cityId);// 查询当前城市下所有的房源列表mysqlList<CityHouse> cityHouses = cityHouseMapper.selectList(new LambdaQueryWrapper<CityHouse>().eq(CityHouse::getCityId, cityId));// 新增当前城市下所有的房源列表映射redisif(!CollectionUtils.isEmpty(cityHouses)){redisService.setCacheList(CITY_HOUSE_PREFIX + cityId,cityHouses.stream().map(CityHouse::getHouseId).distinct().collect(Collectors.toList()));}// 更新房源列表详细信息(redis)for(CityHouse cityHouse : cityHouses){cacheHouse(cityHouse.getHouseId());}}
}

优化建议

  1. 分页处理:如果数据量很大,需要分页处理避免内存溢出
  2. 异步执行:全量刷新耗时较长,应该异步执行并提供进度查询
  3. 灰度发布:先刷新部分数据,验证无误后再全量刷新

3. 小结

通过本文的实战讲解,我们完整掌握了:

✅ 核心解决方案

  1. 定时任务调度:使用Spring Schedule实现定时状态检查
  2. 分布式锁保障:Redis原生 vs Redisson两种实现方案
  3. 消息队列解耦:RabbitMQ处理用户信息变更事件
  4. 缓存全量刷新:系统启动或数据迁移时的缓存初始化

✅ 技术选型推荐

  • 中小项目:直接使用Redisson,避免重复造轮子
  • 极致性能场景:可考虑Redis原生方案,但要做好充分测试
  • 高可用要求:Redis集群模式+Sentinel保障可用性

希望本文能帮你彻底解决分布式环境下的定时任务同步问题。如果有任何疑问或建议,欢迎在评论区交流讨论!

http://www.dtcms.com/a/594010.html

相关文章:

  • 缓存更新策略
  • 网站海外推广方案品牌策划公司的市场
  • 大潮建设集团有限公司 网站h5的制作步骤
  • 河中跳房子(信息学奥赛一本通- P1247)
  • Julia 日期和时间
  • 虚幻引擎5 GAS开发俯视角RPG游戏 P07-11 实现自动运行
  • 培训人员网站建设龙岗网站开发公司
  • 【经验分享】Genio 520/Genio720未使用引脚处理方法
  • 重庆免费网站建站模板微网站趋势
  • DDR5引领变革:内存条行业的技术迭代与市场重构
  • 一小时速通Pytorch之Tensor张量(一)
  • 怎么做网站教程 用的工具提供网站推广公司电话
  • 网站服务内容怎么写wordpress 响应时间
  • 主成分分析(Principal Component Analysis,PCA)的个人理解
  • sqlite数据库迁移至mysql
  • PostgreSQL 高并发优化:从 “连接数满到崩” 到 “支撑 10 万 QPS” 的实战技巧
  • 怎么免费建自己的网站网络营销课程培训机构
  • 团队开发者git仓库工作手册
  • 欧美风网站建设seo品牌优化整站优化
  • 2.8 模型压缩与优化技术
  • 数字孪生工厂浪潮来袭,众趣科技以实景智能筑牢智造基石
  • [設計模式]二十三種設計模式
  • 有视频接口怎么做网站哪个行业最需要做网站
  • 解锁AI工作流的终极密码:Semantic Kernel Process框架深度技术解析
  • 从0到1:Makefile自动化编译实战全解析
  • 广州网站推广教程中国建设银行网站会员用户名
  • 怎么做分享软件的网站php网站开发框架搭建
  • 网站跟网页的区别jsp做的网页是网站吗
  • 根据docker服务保存日志脚本,时间可选版本
  • 九、神经网络的构建方式详解