说到注册中心,相信很多人用过eureka和nacos,用法比较简单,只需引用jar包和配置服务器地址即可启用.但是今天我要说的不是两者的使用或者原理,而是更纯粹一点,就是手搓一个注册中心.
概念
其实注册中心的概念就是提供给外部用来注册信息和获取信息的储存服务,这个存储服务的载体是数据库,也可以是系统应用.当然,我们一般不会采用直连数据库这种方案,所以比较合适的方案是搭建系统应用作为注册中心.
注册中心一般会外提供API的方式,但外部直接调用API的方式并不方便,所以配套一个客户端给外部使用.了解基本概念后,那就开始手搓.
服务端
首先是服务端,对外提供三个接口,分别是注册,心跳,获取实例注册接口作用是接收客户端提交过来的节点信息,如服务名,ip,端口等,如下
private final Map<String, List<Instance>> serviceMap = new ConcurrentHashMap<>();
@PostMapping(value = "/register")
public String register(@RequestBody Instance instance) {
synchronized (this) {
List<Instance> instanceList = serviceMap.get(instance.getName());
if (instanceList == null) {
instanceList = new ArrayList<>();
instance.setBeatTime(new Date());
instanceList.add(instance);
serviceMap.put(instance.getName(), instanceList);
}
}
System.out.println("注册 " + instance.getName() + instance.getIp());
return "success";
}
因为架构是客户端->服务端单向的,为了确保节点在线,需要心跳接口接收客户端的心跳,如下
@PostMapping(value = "/beat")
public String beat(@RequestBody Instance instance) {
List<Instance> instanceList = serviceMap.get(instance.getName());
if (CollectionUtils.isEmpty(instanceList)) {
register(instance);
instanceList = serviceMap.get(instance.getName());
}
Instance instance1 = instanceList.stream().filter(x -> x.getIp().equals(instance.getIp()) && x.getPort().equals(instance.getPort())).findFirst().orElse(null);
if (instance1 == null) {
return "failure";
}
instance1.setBeatTime(new Date());
System.out.println("心跳 " + instance1.getName() + instance1.getIp());
return "success";
}
最后,获取实例则向客户端提供节点列表,返回列表时做一个简单逻辑处理,过滤掉心跳时间在30秒以前的节点,因为那些节点可以视为下线了,如下
@GetMapping(value = "/list")
public List<Instance> listInstance(String serviceName) {
List<Instance> instanceList = null;
if (StringUtils.isBlank(serviceName)) {
instanceList = serviceMap.entrySet().stream().flatMap(x -> x.getValue().stream()).collect(Collectors.toList());
} else {
instanceList = serviceMap.get(serviceName);
}
if (instanceList == null) {
instanceList = new ArrayList<>();
}
//心跳时间在30秒之前的,移除掉
instanceList.removeIf(x -> x.getBeatTime().before(DateUtils.addSeconds(new Date(), -30)));
System.out.println("查询 " + instanceList.size());
return instanceList;
}
客户端
虽然可以直接调用服务端接口进行注册中心的操作,但是并不方便,所以一般都会有配套的客户端sdk.那么客户端需要做什么呢,首先是对服务端的三个接口进行封装,如下
public class InstanceServiceImpl implements InstanceService {
List<Instance> instanceList;
ObjectMapper mapper = new ObjectMapper();
public String serverAddress;
public InstanceServiceImpl(String serverAddress) {
this.serverAddress = serverAddress;
}
@PostConstruct
void init() {
ScheduledTaskUtils.schedule("获取实例列表", 30, 0, () -> {
try {
String result = HttpUtils.doGet(serverAddress + "/instance/list");
instanceList = mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false).readValue(result, new TypeReference<List<Instance>>() {
});
} catch (Exception ex) {
ex.printStackTrace();
}
});
}
@Override
public void register(String serviceName, String ip, Integer port) throws JsonProcessingException {
Instance instance = new Instance();
instance.setName(serviceName);
instance.setIp(ip);
instance.setPort(port);
HttpUtils.doPost(serverAddress + "/instance/register", mapper.writeValueAsString(instance));
}
@Override
public void beat(String serviceName, String ip, Integer port) throws JsonProcessingException {
Instance instance = new Instance();
instance.setName(serviceName);
instance.setIp(ip);
instance.setPort(port);
HttpUtils.doPost(serverAddress + "/instance/beat", mapper.writeValueAsString(instance));
}
@Override
public List<Instance> listInstance(String serviceName) {
return instanceList.stream().filter(x -> x.getName().equals(serviceName)).collect(Collectors.toList());
}
}
Ribbon
虽然上面服务端和客户端都完成了,但是我们一般不会直接使用client的接口,更多的是使用@FeignClient来实现微服务间的互相调用,而@FeignClient的背后是默认引入了Ribbon作为负载均衡器.而Ribbon提供了ServerList接口作为负载均衡的数据来源.所以我们需要在ServerList实现类中获取注册中心服务端的数据.如下
public class RegisterServerList extends AbstractServerList<Server> {
private String serviceId;
@Autowired
InstanceService instanceService;
@Override
public List<Server> getInitialListOfServers() {
return getServers();
}
@Override
public List<Server> getUpdatedListOfServers() {
return getServers();
}
private List<Server> getServers() {
try {
List<Instance> instanceList = instanceService.listInstance(serviceId);
return instanceList.stream().map(x -> new Server(x.getIp(), x.getPort())).collect(Collectors.toList());
} catch (Exception e) {
}
return new ArrayList<>();
}
@Override
public void initWithNiwsConfig(IClientConfig iClientConfig) {
this.serviceId = iClientConfig.getClientName();
}
}
另外,还需要写一个配置类,提供ServerList的实现类bean,如下
@AutoConfigureAfter(RibbonAutoConfiguration.class)
@Configuration(proxyBeanMethods = false)
public class RegisterClientConfiguration {
@Bean
public ServerList<?> ribbonServerList(IClientConfig config) {
RegisterServerList list = new RegisterServerList();
list.initWithNiwsConfig(config);
return list;
}
}
而在引用了client包的应用,在springboot的启动类中还需要指定ribbon的默认配置类,如下
@RibbonClients(defaultConfiguration = RegisterClientConfiguration.class)
Springboot
事实上我们在使用nacos之类的注册中心时,并不需要使用注解RibbonClients指定配置类.那是因为nacos使用了springboot的自动化配置机制,那么我们也需要做类似的实现,达到开箱即用的效果在resources/META-INF路径下添加spring.factories文件,里面加入,实现自动加载配置类
org.springframework.boot.autoconfigure.EnableAutoConfiguration=com.example.register.client.spring.ClientAutoConfiguration
ClientAutoConfiguration会检查serverAddr是否配置了,如果配置则创建ServiceRegistration实例,开启自动注册,如下
@Configuration(proxyBeanMethods = false)
@RibbonClients(defaultConfiguration = RegisterClientConfiguration.class)
@EnableConfigurationProperties
@ConditionalOnProperty(value = "spring.cloud.discovery.serverAddr")
public class ClientAutoConfiguration {
// @Bean
// public RegisterProperties registerProperties() {
// return new RegisterProperties();
// }
@Bean
public InstanceService instanceService(@Value("${spring.cloud.discovery.serverAddr}") String serverAddress) {
InstanceServiceImpl instanceService = new InstanceServiceImpl(serverAddress);
return instanceService;
}
@Bean
public ServiceRegistration serviceRegistration() {
ServiceRegistration serviceRegistration = new ServiceRegistration();
return serviceRegistration;
}
}
ServiceRegistration主要干的就是,注册本机ip端口到服务端,开启心跳定时任务,如下
public class ServiceRegistration {
@Autowired
InstanceService instanceService;
@Value("${spring.application.name:}")
private String service;
@Value("${server.port:}")
private Integer port;
String ip;
@PostConstruct
public void init() {
InetUtilsProperties target = new InetUtilsProperties();
try (InetUtils utils = new InetUtils(target)) {
ip = utils.findFirstNonLoopbackHostInfo().getIpAddress();
register(service, ip, port);
}
}
void register(String service, String ip, Integer port) {
try {
instanceService.register(service, ip, port);
addBeat(service, ip, port);
} catch (Exception ex) {
ex.printStackTrace();
}
}
void addBeat(String service, String ip, Integer port) {
ScheduledTaskUtils.schedule("心跳", 10, 10, () -> {
try {
instanceService.beat(service, ip, port);
} catch (Exception ex) {
ex.printStackTrace();
}
});
}
}
小结
简单来说,注册中心就是一个提供HTTP接口的服务端,还提供封装了这些API的客户端jar包,为了适用于SpringCloud和SpringBoot框架,客户端jar包还针对性地实现了Ribbon和SpringBoot的接口.当然,行业上的注册中心远没有这么简单,例如Nacos之类的,添加分组,权重,集群等等功能.