一,问题描述
由于楼主公司paas应用是双边部署,异地双活机制,导致每台服务器的定时任务都启动一次。
二,解决方案
想过用数据库乐观锁解决问题,执行定时任务之前去数据库获取锁,其他线程将无法获取锁执行程序。考虑到性能问题,采用另一种方案:redis锁。当然还有zookeeper方式实现,这里先不讨论。
三,实现细节
1,首先引入jedis依赖(楼主使用Java)
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
<version>2.9.0</version>
</dependency>
2,使用工具类
public class RedisTool {
/**
* 尝试获取分布式锁
* @param jedis Redis客户端
* @param lockKey 锁
* @param requestId 请求标识
* @param expireTime 超期时间
* @return 是否获取成功
*/
public static boolean tryGetDistributedLock(Jedis jedis, String lockKey, String requestId, int expireTime) {
String result = jedis.set(lockKey, requestId, “NX”, "EX", expireTime);
if ("OK".equals(result)) {
return true;
}
return false;
}
/**
* 释放分布式锁
* @param jedis Redis客户端
* @param lockKey 锁
* @param requestId 请求标识
* @return 是否释放成功
*/
public static boolean releaseDistributedLock(Jedis jedis, String lockKey, String requestId) {
String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
Object result = jedis.eval(script, Collections.singletonList(lockKey), Collections.singletonList(requestId));
if (1L.equals(result)) {
return true;
}
return false;
}
}
四,分析
1,可以看到,加锁就一行代码:jedis.set(String key, String value, String nxxx, String expx, int time)
,这个set()方法一共有五个形参:
- 第一个为key,我们使用key来当锁,因为key是唯一的。
- 第二个为value,通过给value赋值为requestId,实现绑定请求,方便后续解锁,楼主实际使用通过
UUID.randomUUID().toString()
方法生成。 - 第三个为nxxx,这个参数我们填的是NX,意思是SET IF NOT EXIST,即当key不存在时,我们进行set操作;若key已经存在,则不做任何操作;
- 第四个为expx,EX是秒,PX是毫秒。
- 第五个为time,代表key的过期时间。
总的来说,执行上面的set()方法就只会导致两种结果:1. 当前没有锁(key不存在),那么就进行加锁操作,并对锁设置个有效期,同时value表示加锁的客户端。2. 已有锁存在,不做任何操作。
注意本方法,只适用于Redis单机部署的场景,分布式部署参考redisession。
2,不适用setNX方法在于其和expire配合使用,这样操作就不是原子性了
3,同理,释放锁的时候,通过lua执行能保证原子性,如果通过判断requestId,使用delete方法删除,可能判断同一个id时其他线程获取到锁,导致删除其他线程的锁。