Fork me on GitHub

基于分布式锁解决分布式定时任务重复执行的方法

遇到的问题及使用场景

对将要过期(倒计时10天)的优惠券使用用户进行发短信提醒,每天上午10点发一次,发短信的服务有3个实例,如果不进行处理,用户每天会受到3条相同的短信,既影响用户体验,又浪费短信资源,那该怎么处理呢,我现在使用的Spring自带的定时任务组件Scheduled,可能Elastic-Job,XXL-Job开源框架已经很好的解决了这些问题,笔者只讨论是用Spring Task的情况。

Redis分布式锁的构建

@Slf4j
public class RedisLock implements AutoCloseable {

    private RedisTemplate redisTemplate;
    private String key;
    private String value;
    //单位:秒
    private int expireTime;

    /**
     * @param redisTemplate
     * @param key
     * @param expireTime    过期时间
     */
    public RedisLock(RedisTemplate redisTemplate, String key, int expireTime) {
        this.redisTemplate = redisTemplate;
        this.key = key;
        this.expireTime = expireTime;
        this.value = UUID.randomUUID().toString();
    }

    /**
     * 获取分布式锁
     *
     * @return
     */
    public boolean getLock() {
        RedisCallback<Boolean> redisCallback = connection -> {
            //设置NX
            RedisStringCommands.SetOption setOption = RedisStringCommands.SetOption.ifAbsent();
            //设置过期时间
            Expiration expiration = Expiration.seconds(expireTime);
            //序列化key
            byte[] redisKey = redisTemplate.getKeySerializer().serialize(key);
            //序列化value
            byte[] redisValue = redisTemplate.getValueSerializer().serialize(value);
            //执行setnx操作(获取锁的操作)
            Boolean result = connection.set(redisKey, redisValue, expiration, setOption);

            // 也可以通过这种方式:
            //Boolean aBoolean = redisTemplate.opsForValue().setIfAbsent(redisKey, redisValue);
            return result;
        };

        //获取分布式锁
        Boolean lock = (Boolean) redisTemplate.execute(redisCallback);
        return lock;
    }

    /**
     * 释放分布式锁
     *
     * @return
     */
    public boolean unLock() {
        String script = "if redis.call(\"get\",KEYS[1]) == ARGV[1] then\n" +
                "    return redis.call(\"del\",KEYS[1])\n" +
                "else\n" +
                "    return 0\n" +
                "end";
        RedisScript<Boolean> redisScript = RedisScript.of(script, Boolean.class);
        List<String> keys = Arrays.asList(key);

        Boolean result = (Boolean) redisTemplate.execute(redisScript, keys, value);
        if(result){
            log.info("线程{}释放锁成功",Thread.currentThread().getId(),result);

        }else{
            log.info("线程{}不能释放其他线程已抢到的锁,等待下一次定时任务执行",Thread.currentThread().getId());
        }
        return result;
    }
    
    @Override
    public void close() throws Exception {
        unLock();
    }

}

定时任务构建

只举一个小的例子:

@Service
@Slf4j
public class SchedulerService {

    @Autowired
    private RedisTemplate redisTemplate;

    @Scheduled(cron = "0/5 * * * * ?")
    public void sendSms() {
        try (RedisLock redisLock = new RedisLock(redisTemplate, "autoSms", 30)) {
            if (redisLock.getLock()) {
                log.info("线程{}抢到了锁",Thread.currentThread().getId());
                log.info("向138xxxxxxxx发送短信!");
                Thread.sleep(3000);
            }else {
                log.info("线程{}未抢到锁",Thread.currentThread().getId());
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

}

任务执行分析

源代码

https://gitee.com/hardon123a/project-research/tree/master/redis/redis-distribute-lock

posted @ 2021-07-13 17:41  薄荷加冰2060  阅读(545)  评论(0编辑  收藏  举报