需求:
假设当前有一个短信服务是多节点集群部署,我们希望每个服务节点在启动时能将服务信息"注册"到redis缓存中,所有服务节点每隔3分钟上报一次,表示当前服务可用。每个服务还会作为哨兵节点每隔10分钟查询一次redis,将没有即时上报的服务节点从redis中剔除。
步骤:
(1)创建一个Springboot工程,由于要使用到Spring Task定时任务,所以在启动类上标注@EnableScheduling注解:
@SpringBootApplication
@EnableScheduling//s开启定时功能
public class SmsApplication {
public static void main(String[] args) {
SpringApplication.run(SmsApplication.class,args);
}
}
(2)定义一个ServerRegister类,实现服务注册、服务上报、服务检查等逻辑:
@Component
public class ServerRegister implements CommandLineRunner {
//服务的唯一标识
public static String SERVER_ID = null;
@Resource
private RedisTemplate redisTemplate;
/**
* 项目启动时自动执行该方法
* @param args
* @throws Exception
*/
@Override
public void run(String... args) throws Exception {
//服务注册方法
registrationService();
}
}
代码解读:
1、ServerRegister需要标注@Component注解配置成bean,装载到Spring容器中。
2、ServerRegister需要实现CommandLineRunner接口,重写run方法,在项目启动时会自动执行run方法中的逻辑,我们可以在run方法中实现服务注册功能。
(3)服务注册逻辑:
private void registrationService(){
//通过UUID随机生成服务节点的唯一标识
SERVER_ID = UUID.randomUUID().toString();
System.out.println("当前服务实例唯一标识:" + SERVER_ID);
//将服务节点信息以Hash结构存储
redisTemplate.opsForHash().put(RedisConstants.REGISTRY_CENTER,SERVER_ID,System.currentTimeMillis());
}
SERVER_ID:服务节点唯一标识,通过UUID随机生成一段字符串作为节点的唯一标识。
Hash结构图解:
(4)服务上报逻辑:
/**
* 定时服务上报
* 每隔三分钟定时上报
* 上报的逻辑:修改value为当前时间戳
*/
@Scheduled(initialDelay = 10000,fixedRate = 180000)
public void keepAlive(){
System.out.println("定时上报,上报服务唯一标识:" + SERVER_ID);
redisTemplate.opsForHash().put(RedisConstants.REGISTRY_CENTER,SERVER_ID,System.currentTimeMillis());
}
上报逻辑其实就是修改了一下服务节点对应的value时间戳,如果服务挂了那么服务肯定无法执行该逻辑,以此来判断服务是否仍然可用。
initialDelay属性:表示从项目启动后延迟多久开始执行定时任务。
fixedRate属性:表示每次任务执行的时间间隔。
两个属性都是以ms为单位。
(5)服务检查逻辑:
/**
* 定时服务检查
* 每隔十分钟定时检查
* 检查逻辑:每隔服务的最后一次上报时间与当前时间的差值 > 5分钟则将服务实例从redis中剔除
*/
@Scheduled(initialDelay = 10000,fixedRate = 600000)
public void checkServer(){
System.out.print("进行服务实例的检查,执行当前任务的服务为:" + SERVER_ID);
String center_key = RedisConstants.REGISTRY_CENTER,SERVER_ID;
//获得Redis中注册的所有服务实例id
Map map = redisTemplate.opsForHash().entries(center_key );
//获取当前系统时间戳
long current = System.currentTimeMillis();
//保存没有及时上报的服务节点唯一标识
List removeKeys = new ArrayList();
map.forEach((key,value) -> {//key为服务实例id,value为上报的系统时间戳
long parseLong = Long.parseLong(value.toString());
if(current - parseLong > (1000 * 60 * 5)){
//当前服务实例超过5分钟没有上报
removeKeys.add(key);
}
});
//清理服务实例
removeKeys.forEach(key ->{
System.out.print("清理服务实例:"+key);
redisTemplate.opsForHash().delete(RedisConstants.REGISTRY_CENTER,key);
});
}
检查逻辑其实就是将一些没有及时修改value(时间戳)的服务节点从hash结构中删除,因为服务如果是正常状态,那么它肯定能及时更新value,没有及时更新说明服务已经不可用了,需要从redis中剔除。
为什么是5五分钟检查以此而不是3分钟检查一次?
有时候可能会因为网络问题导致服务节点上报不及时,而不是因为服务节点真的挂了,此时我们不应该将服务节点从redis中剔除,多预留的2分钟就是为了避免这种情况发生。
(6)RedisConstants常量类:
public class RedisConstants {
public static String REGISTRY_CENTER = "Service_REGISTRY_Collection";
}
测试:
(1)启动redis-server:
(2)运行启动类的run方法启动项目:
(3)测试结果:
Redis gui工具:
总结:
1、每个服务在启动时都会通过UUID生成随机字符串作为自己的唯一标识,随后基于Hash结构将每个服务的唯一标识和时间戳存储在redis中。
2、服务上报(保活)实际上就是不断修改value时间戳,以此来表示服务仍可用。
3、服务检查会对所有保存在Hash结构中的服务节点进行检查,判断上报时间是否在规定范围内,没有及时上报的服务节点会从Hash结构中剔除。
4、方案缺陷是每个服务间的系统时钟不能偏差太多,否则会存在误判,将一些正常服务从redis中剔除。