基于分布式锁解决分布式定时任务重复执行的方法
遇到的问题及使用场景
对将要过期(倒计时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