分布式环境下利用缓存解决重复性问题

1,174 阅读3分钟

今天讨论一个关于分布式集群中如何防止重复操作的问题,重复操作在单机应用就显得很重要了,何况是在分布式系统中。小编举个栗子,我们首先模拟这么一个场景,假如我们有一个只有2台机器的小集群,每台机器上面部署了同一个应用服务系统,每个系统中定义了1个相同的定时任务(我们假设它是-----在每天23点执行对同一个数据库某个操作),因为是在集群环境中,我们怎么保证这两个完全相同的定时任务只执行其中一个呢?

我们想象最坏的一种情况,那就是两个定时任务都执行了,结果必然影响相应的业务,下面我们假设是两个定时任务:

task

@Component
@EnableScheduling
public class Task implements SchedulingConfigurer {
    private static String cron = "0 0/1 * * * ?";
    
    @Override
    public void configureTasks(ScheduledTaskRegistrar taskRegistrar) {
        taskRegistrar.addTriggerTask(() -> {
        // 此处不加防重复操作,两台机器必然执行两次该核心业务
        payRecordSvc.handleProcessRecord();
        } catch (Exception e) {
            LOGGER.error("orderRecordExpiredJob error!", e);
        }
        }, (triggerContext) -> {
            // 任务触发,可修改任务的执行周期  
            if (StringUtils.isNotBlank(lifecSysConfig.getPayRecordJobQueryCron())) {
                cron = Config.getPayRecordJobQueryCron();
            }
            CronTrigger trigger = new CronTrigger(cron);
            Date nextExec = trigger.nextExecutionTime(triggerContext);
            return nextExec;
        });
    }
}

既然是分布式系统,我们可以使用某种共享资源来实现并发控制,于是乎codis出现了,codis是redis的分布式版本,当然下面讲的使用它来防止重复操作,采用redis也是可行的。当第一台机器执行时,我们在redis中插入某个键值,然后执行定时任务,当第二台机器准备执行定时任务时,我们可以判断redis中是否已经存在该键,如果存在,则不执行定时任务。

看下面改进代码:

@Component
@EnableScheduling
public class PayRecordTradeQueryTask implements SchedulingConfigurer {

    private static String cron = "0 0/1 * * * ?";

    @Override
    public void configureTasks(ScheduledTaskRegistrar taskRegistrar) {
        taskRegistrar.addTriggerTask(() -> {
            try {
                // 防止多台服务器重复处理
                boolean isRepeated = RedisAvoidRepeatUtils.repeatOpreat(LifecCommonCst.ORDER_PAY_QUERY_JOB_REPEATED_KEY, "1", jedisPool, 10);
                if (!isRepeated) {
                    payRecordSvc.handleProcessRecord();
                }
            } catch (Exception e) {
                LOGGER.error("orderRecordExpiredJob error!", e);
            }
        }, (triggerContext) -> {
            // 任务触发,可修改任务的执行周期  
            if (StringUtils.isNotBlank(lifecSysConfig.getPayRecordJobQueryCron())) {
                cron = lifecSysConfig.getPayRecordJobQueryCron();
            }
            CronTrigger trigger = new CronTrigger(cron);
            Date nextExec = trigger.nextExecutionTime(triggerContext);
            return nextExec;
        });
    }
}

我们进入repeatOpreat()方法看看怎么实现:

/**
 * 是否重复操作
 * true  :重复操作
 * false : 没有重复操作
 */
public static boolean repeatOpreat(String key, String value, JedisResourcePool jedisPool, int time) {
    Jedis jedis = null;
    try {
        jedis = getJedis(jedisPool);
        // 这句话的意思是当key存在时,则返回null,否则插入该键值,并返回OK
        String status = jedis.set(key, value, "NX", "EX", time);
        LOG.info("key = " + key + "; value = " + value +"; jedis status ->" +  status);
        if ("OK".equalsIgnoreCase(status)) {
            return false;
        }
        return true;
    } catch (Exception e) {
        LOG.error("repeatOpreat faild", e);
    } finally {
        if (jedis != null) {
            jedis.close();
        }
    }
    return false;
}

注意jedis提供了很多个版本的set方法,我们应该使用注入上面的方法,其中参数值NX指示redis,当key存在时,则返回null,否则插入该键值,并返回OK,否则当你重复插入相同键值时,都会返回OK。这里的原理是使用到了redis的分布式锁,同理我们也可以使用setNX命令。

最后,其实利用缓存实现防止重复操作的用途非常广泛,不一定只适用于分布式定时任务的防重,具体问题具体分析!