环境 四台tomcat作为集群,redis作为共享内存。
需求 定时任务获取文件的地址,监控这些文件的状态(未到,已到达,重发),将文件状态保存;代码每台tomcat须一致。
实现 用redis写个消息队列,监控文件的程序作为消费者从消息队列中分别取得相应任务并且执行。
问题 如果四台tomcat的获取文件地址的任务都执行的话,会导致消息队列中每个任务*4,需要的只是每个任务查出来一次就可以了。
解决 使用redis乐观锁实现tomcat集群中一台抢占定时任务。
乐观锁
乐观锁其实就是利用版本的形式来确定修改的数据为最新版本,比如你和其他人获取信息时是1.0版本,当其他人修改时版本会增加一个版本,变成了2.0版本,当你再修改的时候检验你的版本是1.0而最新的是2.0,就会报错或者撤销操作。(自己理解的,或许有偏差,欢迎评论告知)
PS:第一次接触乐观锁的时候还是在Hibernate
redis乐观锁
redis的乐观锁机制使用WATCH来实现的,但是redis的事务和关系型数据库的事务不一样,关系型数据库的事务是出错后全部撤销,而redis的事务是更像是打包执行,错误后边的不运行了,前边的不撤销。
Jedis乐观锁实现
jedis.watch(key)
try {
Transaction transaction = jedis.multi();
transaction.hset(key, value);
transaction.hset(key, value);
List result = transaction.exec();
if (result == null || result.isEmpty()) {
// 已经修改
} else {
// 修改成功
}
} catch (Exception e) {
// 处理异常
} finally {
jedis.unwatch();
jedis.close();
}
代码实现
先用redis创建一个Hash,里面有两个字段,一个状态status(false或true),一个时间time。status是用来抢占任务的,time则是为了避免当一台tomcat的时间快了很多,当这个时间段结束后status变为false,其他tomcat又获取,发现status又是false,继续执行定时任务,增加上time后就可以判断上一次运行的任务是上一时段的还是当前时段的。
/**
* 抢占任务(多台tomcat利用redis抢占任务)
* 利用redis事务实现乐观锁
* @return
*/
public boolean seize() {
boolean retValue = false;
Jedis jedis = redisConn4Monitor.getConn();
try {
jedis.watch(Global.PUBLIC_AUTOPARAM_QUERYMQKEY);
Map<String, String> queryMap = jedis.hgetAll(Global.PUBLIC_AUTOPARAM_QUERYMQKEY);
log.info("监控状态:queryMap.get(\"status\")) = " + queryMap.get("status"));
if ("true".equals(queryMap.get("status"))) {
log.info("ip:" + InetAddress.getLocalHost() + ",抢占监控查询任务失败,任务状态为true" + DateFormatUtil.msecToFormat(this.getRedisTime()));
log.info("ip:" + InetAddress.getLocalHost() + ",抢占监控查询任务失败,时间:" + DateFormatUtil.msecToFormat(this.getRedisTime()));
} else if ("false".equals(queryMap.get("status"))) {
long currentTime = this.getRedisTime();
String intervalTimeStr = jedis.hget(Global.PUBLIC_AUTOPARAM_CRONKEY, "intervaltime");
long intervalTime = Long.parseLong(intervalTimeStr) * 60000;
long runTime = Long.parseLong(queryMap.get("time") == null ? "0" : queryMap.get("time"));
if ((currentTime - intervalTime) > (runTime - (intervalTime / 60))) { // 当前时间超出时间间隔 允许有数秒误差
Transaction transaction = jedis.multi();
transaction.hset(Global.PUBLIC_AUTOPARAM_QUERYMQKEY, "status", "true");
transaction.hset(Global.PUBLIC_AUTOPARAM_QUERYMQKEY, "time", String.valueOf(this.getRedisTime()));
List result = transaction.exec();
if (result == null || result.isEmpty()) {
log.info("ip:" + InetAddress.getLocalHost() + ",抢占监控查询任务失败,时间:" + DateFormatUtil.msecToFormat(this.getRedisTime()));
} else {
log.info("ip:" + InetAddress.getLocalHost() + ",抢占监控查询任务成功,时间:" + DateFormatUtil.msecToFormat(this.getRedisTime()));
retValue = true;
}
} else {
log.info("ip:" + InetAddress.getLocalHost() + ",抢占监控查询任务失败,任务已经结束,时间:" + DateFormatUtil.msecToFormat(this.getRedisTime()));
}
}
} catch (Exception e) {
throw new RuntimeException("抢占任务出错:" + e.getMessage());
} finally {
jedis.unwatch();
redisConn4Monitor.resourceClose(jedis);
}
return retValue;
}
/**
* 获取redis服务器时间
* @return Unix时间戳
*/
private long getRedisTime() {
long retTime = 0L;
Jedis jedis = redisConn4Monitor.getConn();
try {
List<String> retTimeList = jedis.time();
String retTimeStr = retTimeList.get(0) + (retTimeList.size() > 1 ? retTimeList.get(1).substring(0, 3) : "000");
retTime = Long.parseLong(retTimeStr);
} catch (Exception e) {
throw new RuntimeException("获取redis服务器时间出错:" + e.getMessage());
} finally {
redisConn4Monitor.resourceClose(jedis);
}
return retTime;
}
代码解析
在获取完文件地址后,会将status变为false,时间更改为redis服务器当前时间。