如何在Tomcat集群中保障AuctionRunnerThread唯一且持续运行?
集群环境下单节点定时任务的实现方案
针对Java Tomcat集群中AuctionRunnerThread的单节点运行需求,以下是几种成熟的通用解决方案,适配EC2节点故障自动切换的场景:
一、轻量方案:基于数据库的分布式锁
利用现有业务数据库实现分布式锁,无需引入第三方服务,适合小集群场景:
- 锁表设计:创建一张
distributed_lock表,字段包括lock_key(唯一标识锁,比如auction_runner_lock)、owner_node(节点标识,比如EC2实例ID+Tomcat端口)、expire_time(锁过期时间)。 - 抢占与保活逻辑:
- 节点启动时,执行原子SQL抢占锁:
INSERT INTO distributed_lock (lock_key, owner_node, expire_time) VALUES ('auction_runner_lock', 'i-123456-8080', DATE_ADD(NOW(), INTERVAL 5 SECOND)) ON DUPLICATE KEY UPDATE owner_node = CASE WHEN expire_time < NOW() THEN 'i-123456-8080' ELSE owner_node END, expire_time = CASE WHEN expire_time < NOW() THEN DATE_ADD(NOW(), INTERVAL 5 SECOND) ELSE expire_time END; - 执行后查询
owner_node是否为本节点,是则启动AuctionRunnerThread。 - 线程运行期间,每3秒执行一次锁续期SQL,更新
expire_time。 - 其他节点每隔1秒检查锁状态,若锁过期或持有节点无心跳(可额外维护节点心跳表),则尝试抢占锁并启动线程。
- 节点启动时,执行原子SQL抢占锁:
- 关键注意:锁过期时间要大于线程休眠间隔(100ms),同时小于业务允许的最大延迟,避免锁提前释放导致多节点启动。
二、高性能方案:Redis+Redisson分布式锁
如果集群规模大或对性能要求高,用Redis实现分布式锁,推荐Redisson客户端(封装了自动续约、重试逻辑):
- 核心代码示例:
// 初始化Redisson客户端 Config config = new Config(); config.useSingleServer().setAddress("redis://your-redis:6379"); RedissonClient redisson = Redisson.create(config); // 获取锁实例 RLock lock = redisson.getLock("auction_runner_lock"); try { // 尝试抢占锁,最多等待1秒,锁默认过期5秒(Redisson看门狗自动续约) if (lock.tryLock(1, 5, TimeUnit.SECONDS)) { // 启动拍卖线程 AuctionRunnerThread runner = new AuctionRunnerThread(); runner.start(); // 线程运行期间保持锁,看门狗自动续期 runner.join(); } } catch (InterruptedException e) { Thread.currentThread().interrupt(); } finally { if (lock.isHeldByCurrentThread()) { lock.unlock(); } redisson.shutdown(); } - 优势:性能远高于数据库锁,自动续约避免节点正常运行时锁过期,支持Redis集群模式,故障切换延迟低。
三、强一致性方案:ZooKeeper/Curator主节点选举
适合对一致性要求极高的场景,ZooKeeper临时节点特性天然适配主节点选举:
- 核心代码示例(用Curator简化操作):
// 初始化Curator客户端 CuratorFramework client = CuratorFrameworkFactory.newClient("zk-host:2181", new ExponentialBackoffRetry(1000, 3)); client.start(); // 创建LeaderSelector LeaderSelector selector = new LeaderSelector(client, "/auction/leader", new LeaderSelectorListenerAdapter() { @Override public void takeLeadership(CuratorFramework client) throws Exception { // 成为主节点,启动拍卖线程 AuctionRunnerThread runner = new AuctionRunnerThread(); runner.start(); // 线程运行期间保持领导权,直到节点故障或主动退出 runner.join(); } }); selector.autoRequeue(); // 失去领导权后自动重新参与选举 selector.start(); - 优势:强一致性,故障感知及时(ZooKeeper会话超时后临时节点自动删除),无需手动处理锁续期和过期逻辑,适合复杂集群场景。
通用注意事项
- 线程启动前务必检查业务状态:比如查询数据库中是否有正在进行的拍卖回合计算,避免锁抢占成功后重复执行操作。
- 线程内部要添加全局异常捕获:当线程崩溃时,主动释放锁或确保ZooKeeper会话关闭,触发重新选举。
- EC2节点故障场景下,锁过期时间或ZooKeeper会话超时时间建议设置为3-5秒,平衡故障切换速度和误判概率。
内容的提问来源于stack exchange,提问作者bluedevil2k
相关产品推荐
相关产品推荐

