基于Redis分布式锁实现分布式定时任务调度
|字数总计:1.1k|阅读时长:4分钟|阅读量:|
前情提要
众所不周知:矿小圈使用Sa-Token进行鉴权服务,为了防止”黑客”绕过Gateway网关直接访问内部微服务,使用Same-Token进行网关请求鉴定,拿到相同的Same-Token才能请求非网关服务
为了保证Same-Token不被”非法分子”破解,Same-Token采用定时刷新策略,每隔10分钟就会更新一次,因此,想要获取Same-Token并不是一件容易的事情
BUG现形
单例模式下,使用SpringBoot的@Scheduled
是一件非常”爽”的事情,如下很容易定义了一个定时任务每隔10分钟刷新一次Same-Token
1 2 3 4
| @Scheduled(cron = "0 0/10 * * * ?") public void refreshToken() { SaSameUtil.refreshToken(); }
|
但是,在分布式、微服务场景下,同一个类型的服务可能会部署多个实例以达到高可用,于此情景下,再直接使用@Scheduled
会导致同一个任务被重复多次调用
解决方案
如果我们不希望同一任务重复调用,就要用到分布式任务调度技术,市面上常见的有XXL-JOB、Quartz、ElasticJob、scheduleX,不过这些都需要引入新的服务,目前矿小圈就仅有这一个分布式任务,所以我手动实现了一个简易的类似于ShedLock的Redis分布式锁,实现分布式定时任务调度
RedisLockUtil是我自定义的Redis分布式锁工具类,核心代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45
| public class RedisLockUtil { private final RedisTemplate<String, String> redisTemplate; private long LOCK_EXPIRE_TIME; private TimeUnit LOCK_EXPIRE_TIME_UNIT;
private RedisLockUtil(RedisTemplate<String, String> redisTemplate, long expireTime, TimeUnit expireTimeUnit) { this.redisTemplate = redisTemplate; this.LOCK_EXPIRE_TIME = expireTime; this.LOCK_EXPIRE_TIME_UNIT = expireTimeUnit; }
public static RedisLockUtil create(RedisTemplate<String, String> redisTemplate, long expireTime, TimeUnit expireTimeUnit) { return new RedisLockUtil(redisTemplate, expireTime, expireTimeUnit); }
public static RedisLockUtil create(RedisTemplate<String, String> redisTemplate) { return new RedisLockUtil(redisTemplate, 30, TimeUnit.SECONDS); }
public void setLockExpireTime(long expireTime, TimeUnit expireTimeUnit) { this.LOCK_EXPIRE_TIME = expireTime; this.LOCK_EXPIRE_TIME_UNIT = expireTimeUnit; }
public boolean acquireLock(String lockKey, String requestId, long expireTime, TimeUnit expireTimeUnit) { Boolean success = redisTemplate.opsForValue().setIfAbsent(lockKey, requestId, expireTime, expireTimeUnit); return Boolean.TRUE.equals(success); }
public boolean tryLock(String lockKey) { return acquireLock(lockKey, "1", LOCK_EXPIRE_TIME, LOCK_EXPIRE_TIME_UNIT); }
|
此工具类需要用户自己传入RedisTemplate,acquireLock方法通过Redis原子操作setIfAbsent实现锁的获取,如果锁不存在并设置成功就拿到了锁,然后可以执行任务,如果锁存在设置不会成功,获取锁失败,就不能执行任务,requestId是请求标识,可以实现锁的续期与释放,防止其他实例(线程)偷锁,目前Same-Token的刷新用不到锁的续期与释放,故使用tryLock将requestId,设置为固定的”1”,相当于Boolean的True,也就是有实例(线程)拿到锁了
以下是改进后的SameToken刷新任务:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| @Component @Slf4j public class SaSameTokenRefreshTask { private final RedisLockUtil redisLockUtil; private static final String LOCK_KEY = "Authorization:var:same-token-refresh-lock"; @Autowired SaSameTokenRefreshTask(@Qualifier("saRedisTemplate") RedisTemplate<String, String> saRedisTemplate) { this.redisLockUtil = RedisLockUtil.create(saRedisTemplate, 5, TimeUnit.MINUTES); }
@Scheduled(cron = "0 0/10 * * * ?") public void refreshToken() { if (redisLockUtil.tryLock(LOCK_KEY)) { log.info("🔒 拿到刷新锁,开始刷新 Same-Token"); SaSameUtil.refreshToken(); } else { log.info("🔒 Same-Token 刷新锁已被占用,跳过刷新"); } } }
|
每隔10分钟,各个服务会争夺Redis分布式锁,拿到锁的才可以执行刷新操作,如此就只会有一个服务执行刷新任务,不会重复刷新Same-Token,拿到锁5分钟后,锁会被自动释放,不会导致死锁发生
实测如下:
[scheduling-1] c.a.auth.task.SaSameTokenRefreshTask : 🔒 Same-Token 刷新锁已被占用,跳过刷新
[scheduling-1] c.a.auth.task.SaSameTokenRefreshTask : 🔒 拿到刷新锁,开始刷新 Same-Token
[scheduling-1] c.a.auth.task.SaSameTokenRefreshTask : 🔒 Same-Token 刷新锁已被占用,跳过刷新
源码获取
此文章用到的所有代码及案例均可从Github开源项目FlyingForum(矿小圈后端)获取,如果对你有帮助,劳烦您为此项目点一个Star支持作者,如果您发现此项目有任何Bug或者改进之处,欢迎提Issue和PR
RedisLockUtil工具类:项目common模块下的utils包
Same-Token定时任务:项目auth模块下的task包