在一台服务器上,某个服务的单例启动是通过在该服务器的某个路径下记录一个.pid文件实现的,当启动服务时,如果有该文件存在则不再启动,反之则启动.
实际开发中,团队成员协作开发某个工程,比如java定时器,可能会存在测试服务器开启着,其它开发人员本地也开启着的情况,多台机器同时运行一个服务,某些程序的重复执行导致数据库层面的并发可能造成数据错乱,本文目的就是通过zk和mysql对同一服务多处启动做一个控制.
思路:
1.通过redis进行控制
步骤1:启动时检测redis中luchkey的值,如果laucheky=0,则启动,并设值lauchkey=1,服务停止时,设值lauchkey=0
问题1:但是如果kill -9 杀掉服务进程,或由于某些原因中断服务进程,redis中lauchkey可能无法更改为0,其它服务无法启动,造成死锁
步骤2:对lauchkey设值过期时间如10s,在服务中单独启个进程,定时更新lauchkey的过期时间,为lauchkey续命
问题2:redis地址是配置在项目的配置文件中的,开发人员在启动时如果改为自己本地的redis地址,也可以启动,2个定时器依然会对mysql中的数据重复执行
步骤3:redis+mysql , 将redis地址存到mysql中,服务启动时读取redis地址,并去这个redis中读取laucheky的值进行判断
问题3:可行,但是如果将redis换为zookeeper,redis的过期时间的定时维护便会更换为与服务进程绑定的临时节点,节省开发成本且服务绑定关系更稳定
2.通过mysql进行控制
存在问题1,造成启动死锁
3.通过zk临时节点进行控制
服务进程挂掉后,临时节点自动删除,但是依然存在问题2
4.通过zk+mysql进行控制
将zk地址写在mysql中,服务启完成后立即(实现接口InitializingBean)进行检测,判断zk是否存在临时节点"/javatimer/singletonlaunch",不存在则正常启动,存在则终止服务,并报出错误信息
@Slf4j
public class JobManager implements InitializingBean {
@Autowired
private TaskJobService taskJobService;
@Autowired
private QuartzService quartzService;
@Autowired
private ZKClientUtil zkClientUtil;
private String singletonLauchPath = "/javatimer";
private String singletonLauchNode = "/javatimer/singletonlaunch";
public void afterPropertiesSet() throws Exception {
//检查定时器是否重复启动
String active = SpringContextUtils.getByName("environment", Environment.class).getProperty("spring.profiles.active");
if(!"prod".equals(active)){
singletonLaunchCheck();
}
//开启定时任务
List<TaskJob> list = taskJobService.getTaskList();
quartzService.enableCronSchedule(list);
}
private void singletonLaunchCheck() {
//初始化zk连接
zkClientUtil.init();
if (!zkClientUtil.exist(singletonLauchPath)) {
zkClientUtil.createNode(singletonLauchPath, "javatimer", CreateMode.PERSISTENT);
}
//判断是否存在节点
if (zkClientUtil.exist(singletonLauchNode)) {
throw new RuntimeException("启动失败,ip=" + zkClientUtil.getData(singletonLauchNode) + "正在运行此服务");
} else {
try {
InetAddress ia = InetAddress.getLocalHost();
zkClientUtil.createNode(singletonLauchNode, ia.getHostAddress(), CreateMode.EPHEMERAL);
log.info("启动成功,ip={}",ia.getHostAddress());
} catch (UnknownHostException e) {
e.printStackTrace();
}
}
}
}
@Component("zKClientUtil")
public class ZKClientUtil {
private String connectString;
private int sessionTimeout = 3000;
private ZooKeeper zkClient;
@Autowired
private SysContentService sysContentService;
/**
* 判断节点是否存在
*
* @param path
* @return
*/
public Boolean exist(String path) {
Stat stat = null;
try {
stat = zkClient.exists(path, false);
} catch (Exception e) {
e.printStackTrace();
}
return stat == null ? false : true;
}
/**
* 获取客户端连接
*/
public ZooKeeper init() {
try {
if (zkClient == null) {
synchronized (this) {
if (zkClient == null) {
//从数据库中读取zk集群地址
SysContent jobZKLock = sysContentService.findBySkey("JobZKLock");
if (jobZKLock != null && StringUtils.isNotEmpty(jobZKLock.getSvalue())) {
this.connectString = jobZKLock.getSvalue();
}
this.zkClient = new ZooKeeper(connectString, sessionTimeout, new Watcher() {
public void process(WatchedEvent event) {
}
});
int i = 0;
while (!zkClient.getState().isConnected() && i < 10){
Thread.sleep(1000);
i++;
}
}
}
}
} catch (Exception e) {
e.printStackTrace();
}
return zkClient;
}
/**
* 获取节点数据
*
* @param nodePath
* @return
*/
public String getData(String nodePath) {
byte[] data = new byte[0];
try {
data = zkClient.getData(nodePath, false, null);
} catch (Exception e) {
e.printStackTrace();
}
return new String(data);
}
/**
* 创建节点
*
* @param nodePath
* @param value
*/
public void createNode(String nodePath, String value, CreateMode createMode) {
try {
zkClient.create(nodePath, value.getBytes(), ZooDefs.Ids.OPEN_ACL_UNSAFE, createMode);
} catch (Exception e) {
e.printStackTrace();
}
}
}