1、描述介绍
功能:java开发的双时钟系统(也可以改成多时钟系统)
注意:
1)这不是一个定时同步集群时钟的服务,而是一个对外提供时钟同步接口、设置多时钟时间接口、查询多时钟时间接口的服务。即它需要接口触发时钟同步,而且提供时钟查询功能。
2)该服务仅能做到秒级别的时钟同步。
3)虽然是双时钟系统,但底层实际时间只有一个,另外一个时钟体系是基于实际时间加上偏移时间系数因子实现的。
4)所有要同步时间的节点都得部署该服务。
2、pom依赖
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.2.5</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>org.example</groupId>
<artifactId>wyt01clockserver</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>wyt01clockserver</name>
<description>wyt01clockserver</description>
<properties>
<java.version>17</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- fastjson -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.62</version>
</dependency>
<!-- swagger-ui依赖 https://springdoc.org/ -->
<dependency>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
<version>2.5.0</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<excludes>
<exclude>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</exclude>
</excludes>
<finalName>clockserver</finalName>
</configuration>
</plugin>
</plugins>
</build>
</project>
3、核心思路介绍 - 同步逻辑介绍
首先是更新当前节点的系统时间
其次是根据同步请求的参数情况,如果同步其它节点的标识设置为true,则从配置文件中获取要同步的主机(也可以自己改成从缓存或者入参中获取)。
最后轮询向每个节点发送同步请求,此时发送的请求参数中的同步标识为false,以此避免无限循环同步。
@Override
public BaseResult setSyncTime(SyncTimeReq request) {
//当前节点设置时间
executor.submit(() -> updateSystemDate(request.getDate()));
//如果syncFlag标识为true,则遍历所有非当前节点发送同步请求
if (request.getSyncFlag()) {
String local = getLocalHost();
String[] hostArr = hostStr.split(",");
for (int i = 0; i < hostArr.length; i++) {
String host = hostArr[i];
if (host.equalsIgnoreCase(local)) continue;
executor.submit(() -> updateOtherSystemDate(host, request.getDate()));
}
}
return BaseResult.success();
}
4、完整代码介绍
4.1 application.yml
spring:
application:
name: wyt01clockserver
server:
port: 8183
servlet:
context-path: /clockserver
clockserver:
host: 127.0.0.1
4.2 controller
package com.clockserver.controller;
import com.alibaba.fastjson.JSONObject;
import com.clockserver.bean.BaseResult;
import com.clockserver.bean.clock.SyncFactorReq;
import com.clockserver.bean.clock.SyncTimeReq;
import com.clockserver.service.IClockServerService;
import com.clockserver.util.ExceptionUtils;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
@Tag(name = "时钟控制类",description = "包含所有时钟控制相关的接口")
@RestController
@RequestMapping("/clock")
public class ClockServerController {
private static Logger logger = LoggerFactory.getLogger(ClockServerController.class);
@Autowired
private IClockServerService clockServerService;
@Operation(summary = "获取天文时间接口")
@GetMapping("/getAstronomicalTime")
public BaseResult getAstronomicalTime(){
return clockServerService.getAstronomicalTime();
}
@Operation(summary = "获取时间接口")
@GetMapping("/getAnotherTime")
public BaseResult getAnotherTime(){
return clockServerService.getAstronomicalTime();
}
@Operation(summary = "设置时间系数接口")
@PostMapping("/setAnotherTimeFactor")
public BaseResult setAnotherTimeFactor(@RequestBody SyncFactorReq request){
logger.info("接收到设置时间系数请求{}", JSONObject.toJSONString(request));
try {
BaseResult result = clockServerService.setAnotherTimeFactor(request);
return result;
}catch (Exception e){
logger.error("设置时间系数异常,异常信息为:",e);
return BaseResult.error(ExceptionUtils.getSimpleException(e));
}
}
@Operation(summary = "设置同步时间接口")
@PostMapping("/setSyncTime")
public BaseResult setSyncTime(@RequestBody SyncTimeReq request){
logger.info("接收到设置时间请求{}", JSONObject.toJSONString(request));
try {
BaseResult result = clockServerService.setSyncTime(request);
return result;
}catch (Exception e){
logger.error("设置同步时间异常,异常信息为:",e);
return BaseResult.error(ExceptionUtils.getSimpleException(e));
}
}
}
4.3 service
package com.clockserver.service;
import com.clockserver.bean.BaseResult;
import com.clockserver.bean.clock.SyncFactorReq;
import com.clockserver.bean.clock.SyncTimeReq;
public interface IClockServerService {
public BaseResult getAstronomicalTime();
public BaseResult getAnotherTime();
public BaseResult setAnotherTimeFactor(SyncFactorReq request);
public BaseResult setSyncTime(SyncTimeReq request);
}
4.4 serviceImpl
package com.clockserver.service.impl;
import com.alibaba.fastjson.JSONObject;
import com.clockserver.bean.BaseResult;
import com.clockserver.bean.clock.SyncFactorReq;
import com.clockserver.bean.clock.SyncTimeReq;
import com.clockserver.bean.clock.TimeBean;
import com.clockserver.service.IClockServerService;
import com.clockserver.util.ExceptionUtils;
import jakarta.servlet.http.HttpServletRequest;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import org.springframework.web.client.RestTemplate;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import java.net.InetAddress;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
@Component
public class ClockServerServiceImpl implements IClockServerService {
private static Logger logger = LoggerFactory.getLogger(ClockServerServiceImpl.class);
@Autowired
private RestTemplate restTemplate;
@Value("${server.port}")
private Integer port;
@Value("${clockserver.host}")
private String hostStr;
//@TODO 如果需要多个作战事件系数,将该系数因子改成map即可,然后每种作战系数各个不同的唯一标识
private volatile long AnotherTimeFactor = 0l;
private static ThreadPoolExecutor executor = new ThreadPoolExecutor(5, 5, 0l, TimeUnit.SECONDS, new ArrayBlockingQueue<>(10));
private static String localIp = null;
@Override
public BaseResult getAstronomicalTime() {
long millis = System.currentTimeMillis();
TimeBean timeBean = TimeBean.getTimeBean(millis, millis + AnotherTimeFactor);
return BaseResult.success(timeBean);
}
@Override
public BaseResult getAnotherTime() {
long millis = System.currentTimeMillis();
TimeBean timeBean = TimeBean.getTimeBean(millis, millis + AnotherTimeFactor);
return BaseResult.success(timeBean);
}
@Override
public BaseResult setAnotherTimeFactor(SyncFactorReq request) {
//当前节点设置系数
this.AnotherTimeFactor = request.getFactor();
//如果syncFlag标识为true,则遍历所有非当前节点发送同步请求
if (request.getSyncFlag()){
String local = getLocalHost();
String[] hostArr = hostStr.split(",");
for (int i = 0; i < hostArr.length; i++) {
String host = hostArr[i];
if (host.equalsIgnoreCase(local)) continue;
executor.submit(() -> updateOtherSystemFactor(host, request.getFactor()));
}
}
return BaseResult.success();
}
@Override
public BaseResult setSyncTime(SyncTimeReq request) {
//当前节点设置时间
executor.submit(() -> updateSystemDate(request.getDate()));
//如果syncFlag标识为true,则遍历所有非当前节点发送同步请求
if (request.getSyncFlag()) {
String local = getLocalHost();
String[] hostArr = hostStr.split(",");
for (int i = 0; i < hostArr.length; i++) {
String host = hostArr[i];
if (host.equalsIgnoreCase(local)) continue;
executor.submit(() -> updateOtherSystemDate(host, request.getDate()));
}
}
return BaseResult.success();
}
private String getRequestHost() {
ServletRequestAttributes attr = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
HttpServletRequest requests = attr.getRequest();
String remoteHost = requests.getRemoteHost();
return remoteHost;
}
private void updateSystemDate(String timeStr) {
try {
logger.info("更新系统时间为{}", timeStr);
String timeCommand = "sudo date -s '" + timeStr + "'";
// 以root身份执行命令
String[] command = {"/bin/sh", "-c", timeCommand};
Process process = Runtime.getRuntime().exec(command);
process.waitFor();
int exitValue = process.waitFor();
if (exitValue == 0) {
logger.info("系统时间已设置为:{}", timeStr);
} else {
logger.error("设置系统时间失败:{}", timeStr);
}
} catch (Exception e) {
logger.error("设置系统时间异常,异常信息为:", e);
}
}
private void updateOtherSystemDate(String host, String dateStr) {
SyncTimeReq req = new SyncTimeReq();
req.setDate(dateStr);
String url = "http://" + host + ":" + port + "/clockserver/clock/setSyncTime";
logger.info("执行url[{}]更新系统时间为{}", url, dateStr);
BaseResult result = BaseResult.error();
try {
result = restTemplate.postForObject(url, req, BaseResult.class);
} catch (Exception e) {
logger.error("更新{}主机时间异常,异常信息为:", host, e);
result.setMsg(ExceptionUtils.getSimpleException(e));
}
logger.info("更新主机{}系统时间为{}的结果是{}", host, dateStr, JSONObject.toJSONString(result));
}
private void updateOtherSystemFactor(String host, Long factor) {
SyncFactorReq req = new SyncFactorReq();
req.setFactor(factor);
String url = "http://" + host + ":" + port + "/clockserver/clock/setAnotherTimeFactor";
logger.info("执行url[{}]更新时间系数为{}", url, factor);
BaseResult result = BaseResult.error();
try {
result = restTemplate.postForObject(url, req, BaseResult.class);
} catch (Exception e) {
logger.error("更新{}时间系数异常,异常信息为:", host, e);
result.setMsg(ExceptionUtils.getSimpleException(e));
}
logger.info("更新主机{}时间系数为{}的结果是{}", host, factor, JSONObject.toJSONString(result));
}
private String getLocalHost() {
if (localIp != null) return localIp;
String ip = "";
try {
InetAddress inetAddress = InetAddress.getLocalHost();
ip = inetAddress.getHostAddress();
localIp = ip;
} catch (Exception e) {
logger.error("获取本机ip异常:", e);
}
return ip;
}
}
4.5 其它辅助类
@Data
public class BaseResult {
private Integer code;
private String msg;
private Object data;
private Boolean success;
private BaseResult(Integer code, String msg, Object data, Boolean success) {
this.code = code;
this.msg = msg;
this.data = data;
this.success = success;
}
public static BaseResult success(){
BaseResult result = new BaseResult(200,"success",null,true);
return result;
}
public static BaseResult success(Object data){
BaseResult result = new BaseResult(200,"success",data,true);
return result;
}
public static BaseResult error(){
BaseResult result = new BaseResult(-1,"error",null,false);
return result;
}
public static BaseResult error(String msg){
BaseResult result = new BaseResult(-1,msg,null,false);
return result;
}
}
@Data
public class SyncFactorReq {
private long factor; //时钟偏移量
private Boolean syncFlag = false; // true:同步给其它所有节点 false:不同步给其它节点
}
@Data
public class SyncTimeReq {
private String date; //要同步的时间 "yyyy-MM-dd HH:mm:ss"
private Boolean syncFlag = false; // true:同步给其它所有节点 false:不同步给其它节点
}
@Data
public class TimeBean {
private Long astronomicalTime; //天文事件(毫秒级)
private Long AnotherTime; //时间(毫秒级)
public static TimeBean getTimeBean(Long astronomicalTime,Long AnotherTime){
TimeBean timeBean = new TimeBean();
timeBean.setAstronomicalTime(astronomicalTime);
timeBean.setAnotherTime(AnotherTime);
return timeBean;
}
}
public class ExceptionUtils {
public static String getSimpleException(Exception e) {
return e.toString() + ":" + e.getMessage();
}
}
4、总结
上述代码仅仅是一个demo,主要是展示一直多时钟服务的编辑思路。如果想要更稳定的系统,还需要自行完善。比如下面两点改进建议:
1)如果某个节点掉线了,上线后怎么知道自己的时间系数?
将各个时钟系统的时间系数存放在redis缓存中。每次更新时间系数时,先刷新缓存,然后在同步通知其它节点。
2)当前服务是双时钟,如果多时钟怎么改?
当前只存储了一个时钟偏移系数,如果想多时钟系统,可以用map将不同标识的时钟偏移系数存储起来使用。