SpringBoot + 分布式锁 + 定时任务:避免集群环境下任务重复执行的 3 种方案对比
相信很多小伙伴都遇到过这样的场景:系统部署在多台服务器上,每个服务器都配置了相同的定时任务,到了执行时间,所有服务器上的任务一起触发,不仅浪费系统资源,还可能导致数据不一致等问题。这可怎么办呢?今天我们来聊聊一个在分布式系统中非常常见的问题:如何避免集群环境下定时任务的重复执行?
问题的根源:集群环境下的定时任务困境
在单体应用时代,定时任务的执行很简单,就是到了指定时间点就执行。但在微服务或集群环境下,情况就变得复杂了。
想象一下,你有一个清理过期数据的定时任务,设定每天凌晨2点执行。如果你的系统部署在5台服务器上,那么就会有5个清理任务同时运行,不仅浪费资源,还可能因为并发操作导致数据混乱。
这就是分布式锁要解决的核心问题:如何在多个节点中协调,确保同一时间只有一个节点执行特定的任务。
解决方案概览
目前业界主流的分布式锁实现方案主要有三种:
- 基于Redis的分布式锁
- 基于数据库的分布式锁
- 基于Zookeeper的分布式锁
每种方案都有其独特的优势和适用场景,下面我们来详细对比一下。
方案一:基于Redis的分布式锁
实现原理
Redis的分布式锁实现主要依靠其原子性操作。我们使用SET命令的NX和EX选项来实现:
// 尝试获取锁
String result = redisTemplate.opsForValue().setIfAbsent(
lockKey, // 锁的键
requestId, // 锁的值,用于标识锁的持有者
expireTime, // 过期时间
TimeUnit.MILLISECONDS
);
这里的NX选项表示只有当键不存在时才设置,EX选项设置键的过期时间,这样可以避免死锁问题。
代码实现
@Component
public class RedisDistributedLock {
@Autowired
private StringRedisTemplate redisTemplate;
// Lua脚本用于原子性解锁
private static final String UNLOCK_LUA_SCRIPT =
"if redis.call('get', KEYS[1]) == ARGV[1] then " +
"return redis.call('del', KEYS[1]) " +
"else " +
"return 0 " +
"end";
/**
* 尝试获取分布式锁
*/
public boolean tryLock(String lockKey, String requestId, int expireTime) {
try {
String result = redisTemplate.opsForValue().setIfAbsent(
lockKey, requestId, expireTime, TimeUnit.MILLISECONDS);
return Boolean.TRUE.equals(result);
} catch (Exception e) {
log.error("获取Redis分布式锁异常", e);
return false;
}
}
/**
* 释放分布式锁
*/
public boolean releaseLock(String lockKey, String requestId) {
try {
// 使用Lua脚本保证解锁操作的原子性
DefaultRedisScript<Long> script =
new DefaultRedisScript<>(UNLOCK_LUA_SCRIPT, Long.class);
Long result = redisTemplate.execute(script,
Collections.singletonList(lockKey), requestId);
return RELEASE_SUCCESS.equals(result);
} catch (Exception e) {
log.error("释放Redis分布式锁异常", e);
return false;
}
}
}
优缺点分析
优点:
- 性能高:Redis是内存数据库,读写速度快
- 实现简单:利用SET命令的NX和EX选项即可实现
- 支持锁自动过期:避免死锁问题
- 社区生态丰富:有Redission等成熟的客户端库
缺点:
- 需要额外依赖:需要部署和维护Redis集群
- 单点故障风险:虽然可以通过Redis Cluster缓解,但仍存在风险
- 可能出现锁误删:如果业务处理时间超过锁过期时间,可能导致锁被其他节点获取
方案二:基于数据库的分布式锁
实现原理
数据库分布式锁利用数据库的唯一约束或行锁机制。我们创建一张锁表,利用主键唯一性来实现互斥。
代码实现
@Component
public class DatabaseDistributedLock {
@Autowired
private JdbcTemplate jdbcTemplate;
/**
* 初始化锁表
*/
public void initLockTable() {
String sql = """
CREATE TABLE IF NOT EXISTS distributed_lock (
lock_key VARCHAR(255) PRIMARY KEY,
lock_value VARCHAR(255) NOT NULL,
expire_time TIMESTAMP NOT NULL
)
""";
jdbcTemplate.execute(sql);
}
/**
* 尝试获取分布式锁
*/
@Transactional
public boolean tryLock(String lockKey, String requestId, int expireTime) {
try {
// 删除已过期的锁
deleteExpiredLocks();
// 尝试插入新的锁记录
String insertSql = """
INSERT INTO distributed_lock (lock_key, lock_value, expire_time)
VALUES (?, ?, ?)
ON DUPLICATE KEY UPDATE
lock_value = IF(expire_time < NOW(), ?, lock_value),
expire_time = IF(expire_time < NOW(), ?, expire_time)
""";
Timestamp expireTimeTs = Timestamp.valueOf(
LocalDateTime.now().plusSeconds(expireTime));
int affectedRows = jdbcTemplate.update(insertSql,
lockKey, requestId, expireTimeTs, requestId, expireTimeTs);
// 检查是否真的获得了锁
return checkIfLockAcquired(lockKey, requestId);
} catch (Exception e) {
log.error("获取数据库分布式锁异常", e);
return false;
}
}
/**
* 释放分布式锁
*/
@Transactional
public boolean releaseLock(String lockKey, String requestId) {
try {
String sql = "DELETE FROM distributed_lock WHERE lock_key = ? AND lock_value = ?";
int affectedRows = jdbcTemplate.update(sql, lockKey, requestId);
return affectedRows > 0;
} catch (Exception e) {
log.error("释放数据库分布式锁异常", e);
return false;
}
}
}
优缺点分析
优点:
- 无需额外中间件:利用现有数据库
- 数据一致性有保障:数据库本身的事务特性
- 成本低:无需额外部署中间件
缺点:
- 性能相对较低:数据库读写性能低于内存数据库
- 数据库压力大:频繁的锁操作会影响数据库性能
- 锁释放依赖事务:在某些情况下可能导致锁无法及时释放
方案三:基于Zookeeper的分布式锁
实现原理
Zookeeper的分布式锁利用其临时顺序节点的特性。每个客户端在指定的父节点下创建临时顺序子节点,然后获取父节点下的所有子节点,判断自己创建的节点是否是最小的,如果是则获得锁,否则监听前一个节点的变化。
代码实现
@Component
public class ZookeeperDistributedLock {
@Value("${zookeeper.address:localhost:2181}")
private String zkAddress;
private CuratorFramework client;
@PostConstruct
public void init() {
client = CuratorFrameworkFactory.newClient(
zkAddress,
new ExponentialBackoffRetry(1000, 3)
);
client.start();
}
/**
* 尝试获取分布式锁
*/
public InterProcessMutex acquireLock(String lockPath, int timeout, TimeUnit timeUnit) {
try {
InterProcessMutex lock = new InterProcessMutex(client, lockPath);
boolean acquired = lock.acquire(timeout, timeUnit);
if (acquired) {
log.debug("成功获取Zookeeper分布式锁: {}", lockPath);
return lock;
} else {
log.debug("获取Zookeeper分布式锁失败: {}", lockPath);
return null;
}
} catch (Exception e) {
log.error("获取Zookeeper分布式锁异常", e);
return null;
}
}
/**
* 释放分布式锁
*/
public void releaseLock(InterProcessMutex lock) {
try {
if (lock != null && lock.isAcquiredInThisProcess()) {
lock.release();
log.debug("成功释放Zookeeper分布式锁");
}
} catch (Exception e) {
log.error("释放Zookeeper分布式锁异常", e);
}
}
}
优缺点分析
优点:
- 高可靠性:Zookeeper本身具有高可用性和一致性
- 强一致性:保证数据的一致性
- 天然支持监听机制:可以实现更复杂的锁策略
缺点:
- 需要额外依赖:需要部署和维护Zookeeper集群
- 实现相对复杂:需要处理连接管理、会话失效等问题
- 性能不如Redis:网络开销相对较大
实际应用示例
让我们看看如何在定时任务中使用这些分布式锁:
@Service
public class ScheduledTaskService {
@Autowired
private DistributedLockManager distributedLockManager;
/**
* 使用Redis分布式锁的定时任务
*/
@Scheduled(fixedRate = 30000) // 每30秒执行一次
public void redisLockedTask() {
String lockKey = "scheduled_task_redis_locked";
int expireTime = 25000; // 锁过期时间25秒
boolean executed = distributedLockManager.executeWithRedisLock(lockKey, expireTime, () -> {
log.info("Redis分布式锁保护的定时任务开始执行...");
// 模拟业务处理
try {
Thread.sleep(10000); // 模拟耗时操作
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
log.info("Redis分布式锁保护的定时任务执行完成");
});
if (!executed) {
log.debug("Redis分布式锁任务未执行(其他节点正在执行)");
}
}
}
三种方案对比总结
| 特性 | Redis | 数据库 | Zookeeper |
|---|---|---|---|
| 性能 | 高 | 中 | 中 |
| 实现复杂度 | 简单 | 中 | 复杂 |
| 可靠性 | 中 | 高 | 高 |
| 额外依赖 | 是 | 否 | 是 |
| 运维成本 | 中 | 低 | 高 |
如何选择合适的方案?
选择哪种分布式锁方案,主要取决于你的具体业务场景:
- 如果追求高性能:选择Redis方案,适合对性能要求较高的场景
- 如果已有数据库基础设施:选择数据库方案,无需额外部署中间件
- 如果对可靠性要求极高:选择Zookeeper方案,适合关键业务场景
在实际项目中,我个人更倾向于Redis方案,因为它在性能和实现复杂度之间取得了很好的平衡,而且社区生态丰富,有Redission等成熟的客户端库可供使用。
最佳实践建议
- 合理设置锁的过期时间:避免业务处理时间超过锁过期时间导致的问题
- 使用UUID作为锁的持有者标识:确保锁的归属关系明确
- 实现可重入机制:避免同一节点重复获取锁的问题
- 做好异常处理:确保在异常情况下也能正确释放锁
- 监控和告警:对锁的获取和释放情况进行监控
总结
分布式锁是解决集群环境下资源竞争的重要手段,选择合适的方案对系统稳定性至关重要。希望通过这篇文章,你能对三种分布式锁方案有更深入的理解,并能在实际项目中做出合适的选择。
如果你有任何问题或想法,欢迎在评论区交流讨论!
标题:SpringBoot + 分布式锁 + 定时任务:避免集群环境下任务重复执行的 3 种方案对比
作者:jiangyi
地址:http://jiangyi.space/articles/2026/01/24/1769238862076.html