定时任务开发避雷指南:当@Scheduled遇到Redis分布式锁该怎么玩?
·
分布式定时任务实战:Redis锁解决@Scheduled并发难题
想象一下这样的场景:凌晨三点,你的电商平台准时启动日结算任务,却在同一时刻被部署在三个不同Pod上的相同服务重复执行了三次。结果?库存数据错乱,财务对账崩溃,运维团队半夜被紧急呼叫。这就是分布式环境下定时任务失控的典型噩梦。
1. 为什么单机锁在分布式场景中失效
单机环境下,我们用synchronized或ReentrantLock就能轻松解决定时任务并发问题。但当你把服务部署到多台机器上,这些本地锁瞬间变成"纸老虎"。每个JVM都有自己的锁管理器,它们根本不知道其他JVM的存在。
典型问题场景:
- 微服务多实例部署时,每个实例的
@Scheduled都会独立触发 - Kubernetes滚动更新期间,新旧版本可能同时运行定时任务
- 自动扩缩容导致临时出现多个服务实例
// 这种锁在分布式环境下完全无效
private final Object lock = new Object();
@Scheduled(cron = "0 0 3 * * ?")
public void dailySettlement() {
synchronized(lock) {
// 仍然会被多个实例同时执行
}
}
2. Redis分布式锁核心实现方案
2.1 基于SETNX的原子操作
Redis的SETNX(SET if Not eXists)命令是实现分布式锁的基石。但单纯使用SETNX会存在锁无法释放的风险,我们需要给锁设置过期时间:
# 正确的加锁命令
SET lock:order_task <unique_value> NX PX 30000
参数解析:
NX:仅当key不存在时才设置PX 30000:30秒后自动过期<unique_value>:每个客户端唯一的标识值(推荐UUID)
2.2 Redisson客户端最佳实践
手动实现Redis锁需要考虑太多边界情况,不如直接使用成熟的Redisson客户端:
@Configuration
public class RedissonConfig {
@Bean
public RedissonClient redissonClient() {
Config config = new Config();
config.useSingleServer()
.setAddress("redis://127.0.0.1:6379");
return Redisson.create(config);
}
}
@Service
public class OrderTaskService {
@Autowired
private RedissonClient redissonClient;
@Scheduled(cron = "0 0/5 * * * ?")
public void processOrders() {
RLock lock = redissonClient.getLock("order:process:lock");
try {
if (lock.tryLock(10, 30, TimeUnit.SECONDS)) {
// 真正的业务逻辑
}
} finally {
lock.unlock();
}
}
}
3. 分布式锁的五大陷阱与规避策略
3.1 死锁:当锁忘记释放时
常见原因:
- 任务执行时间超过锁过期时间
- 程序崩溃未执行finally块
- 网络分区导致解锁命令未到达Redis
解决方案:
// 推荐使用带超时的尝试获取锁
boolean acquired = lock.tryLock(5, 30, TimeUnit.SECONDS);
3.2 脑裂:集群故障时的锁失效
Redis集群发生网络分区时可能出现多个客户端同时持有锁的情况。应对方案:
- 使用Redlock算法(需要至少5个Redis主节点)
- 增加校验机制,如操作前检查数据版本号
- 关键操作实现幂等性
3.3 锁粒度:太粗降低并发度
错误示范:
// 整个方法加锁,并发性能差
RLock lock = redissonClient.getLock("global:lock");
优化方案:
// 按订单ID分段加锁
String lockKey = "order:" + orderId + ":lock";
RLock lock = redissonClient.getLock(lockKey);
3.4 锁续期:长任务的特殊处理
对于执行时间不确定的长任务,需要实现锁续期机制:
// Redisson已经内置看门狗机制
lock.lock(30, TimeUnit.SECONDS); // 看门狗会自动续期
// 手动续期示例
if (lock.isHeldByCurrentThread()) {
lock.expire(30, TimeUnit.SECONDS);
}
3.5 锁等待:避免系统雪崩
大量线程同时争抢锁会导致Redis压力激增。应对策略:
- 采用随机退避算法
- 实现二级缓存(本地锁+分布式锁)
- 限制最大等待时间
// 带超时的锁等待
if (!lock.tryLock(100, 10000, TimeUnit.MILLISECONDS)) {
// 记录监控指标并快速失败
metrics.counter("lock.timeout").increment();
return;
}
4. 定时任务锁的高级应用场景
4.1 集群任务调度:确保唯一执行
使用Redis锁实现集群环境下定时任务的全局唯一执行:
@Scheduled(cron = "0 0 2 * * ?")
public void generateDailyReport() {
RLock lock = redissonClient.getLock("report:generate:lock");
if (!lock.tryLock()) {
log.info("其他节点正在生成报表");
return;
}
try {
// 实际报表生成逻辑
} finally {
lock.unlock();
}
}
4.2 任务执行顺序控制
通过锁的获取顺序实现任务执行顺序控制:
public void executeInOrder() {
// 先获取前置任务锁
RLock firstLock = redissonClient.getLock("task:first");
RLock secondLock = redissonClient.getLock("task:second");
boolean first = firstLock.tryLock();
if (!first) return;
try {
// 执行第一个任务
doFirstTask();
// 完成后尝试获取第二个锁
if (secondLock.tryLock()) {
try {
doSecondTask();
} finally {
secondLock.unlock();
}
}
} finally {
firstLock.unlock();
}
}
4.3 动态定时任务管理
结合Redis实现动态调整的定时任务:
@Scheduled(fixedDelayString = "${task.interval}")
public void dynamicTask() {
RLock lock = redissonClient.getLock("dynamic:task:lock");
try {
if (lock.tryLock()) {
// 从Redis读取最新配置
String config = redissonClient.getBucket("task:config").get();
processWithConfig(config);
}
} finally {
lock.unlock();
}
}
5. 监控与性能优化实战
5.1 锁竞争监控指标
关键监控指标示例:
| 指标名称 | 类型 | 说明 |
|---|---|---|
| lock.acquire.time | 计时器 | 获取锁耗时 |
| lock.wait.count | 计数器 | 等待锁的线程数 |
| lock.hold.time | 计时器 | 锁持有时间 |
| lock.timeout.rate | 比率 | 锁获取超时比例 |
5.2 Redisson配置调优
推荐的生产环境配置:
# application-redis.yml
singleServerConfig:
idleConnectionTimeout: 10000
connectTimeout: 5000
timeout: 3000
retryAttempts: 3
retryInterval: 1000
subscriptionsPerConnection: 5
connectionMinimumIdleSize: 10
connectionPoolSize: 64
dnsMonitoringInterval: 5000
threads: 16
nettyThreads: 32
5.3 替代方案对比
当Redis不适用时的备选方案:
-
Zookeeper:
- 优点:强一致性,完善的watch机制
- 缺点:写入性能较低
-
数据库分布式锁:
-- 基于唯一索引的实现 INSERT INTO distributed_lock(lock_name, owner, expire_at) VALUES ('report_lock', 'node1', NOW() + INTERVAL 30 SECOND); -
ETCD:
- 优点:高可用,强一致性
- 缺点:部署复杂度较高
在实际项目中,我们团队曾遇到过一个典型案例:对账系统每天凌晨需要处理上百万条交易记录。最初使用数据库行锁导致任务经常超时失败,后来改用Redis分段锁(按商户ID分片)后,任务执行时间从4小时缩短到40分钟。关键点在于:
- 锁粒度细化到每个商户
- 设置合理的锁超时时间(5分钟)
- 实现任务中断后的断点续做功能
更多推荐
所有评论(0)