Spirng + Zookeeper 实现配置中心
项目中使用springboot作为框架,配置文件为application.yml。但在部署多个实例的情况下,需要修改配置的时候需要在多个实例上进行修改并且需要重启才能生效,使用统一的配置中心来存放配置文件,可以实现统一配置以及实时生效。
原理
首先要在MATA-INFO中配置spring.factories开启自定义扩展,然后自定义扩展类,读取zookeeper中的配置信息到SpringBoot的Enviroment中,并注册watcher监听器,当zookeeper中的配置信息发生变化时,通过watcher的回调方法捕获数据变化事件,并拿到修改后的数据信息,然后再通过反射修改@Value的属性值
,来实现实时更新及时生效。
引入jar包
//curator是ZooKeeper 客户端框架
//curator‐recipes封装了一些ZooKeeper服务的高级特性,
//如:Cache 事件监听、选举、分布式锁、分布式 Barrier。
<dependency>
<groupId>org.apache.curator</groupId>
<artifactId>curator‐recipes</artifactId>
<version>5.0.0</version>
<exclusions>
<exclusion>
<groupId>org.apache.zookeeper</groupId>
<artifactId>zookeeper</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.apache.zookeeper</groupId>
<artifactId>zookeeper</artifactId>
<version>3.5.8
</version>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.13</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.12
</version>
</dependency>
创建配置类:
@Data
@ToString
@NoArgsConstructor
public class MyConfig {
private String key;
private String name;
}
zookeeper实现监听文件变化代码:
为了便于测试,直接在初始化方法中创建zookeeper实例
@Slf4j
public class ConfigCenter {
//客户端地址
private final static String CONNECT_STR="127.0.0.1:2181";
//超时时间
private final static Integer SESSION_TIMEOUT=30*1000;
//zookeeper实例
private static ZooKeeper zooKeeper=null;
//用来阻止当前业务线程结束
private static CountDownLatch countDownLatch=new CountDownLatch(1);
public static void main(String[] args) throws IOException, InterruptedException, KeeperException {
zooKeeper=new ZooKeeper(CONNECT_STR, SESSION_TIMEOUT, new Watcher() {
@Override
public void process(WatchedEvent event) {
if (event.getType()== Event.EventType.None
&& event.getState() == Event.KeeperState.SyncConnected){
log.info("连接已建立");
countDownLatch.countDown();
}
}
});
countDownLatch.await();
//配置文件实例
MyConfig myConfig = new MyConfig();
myConfig.setKey("anykey");
myConfig.setName("anyName");
ObjectMapper objectMapper=new ObjectMapper();
//转换成字节
byte[] bytes = objectMapper.writeValueAsBytes(myConfig);
// path 节点名称
// ZooDefs.Ids.OPEN_ACL_UNSAFE:创建权限
// CreateMode.PERSISTENT:持久化节点
String s = zooKeeper.create("/myconfig", bytes, ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.PERSISTENT);
//修改配置
Watcher watcher = new Watcher() {
@SneakyThrows
@Override
public void process(WatchedEvent event) {
//判断type = NodeDataChanged,并且路径不为空,并且路径 = 设置的节点
if (event.getType()== Event.EventType.NodeDataChanged
&& event.getPath()!=null && event.getPath().equals("/myconfig")){
log.info("PATH:{}发生了数据变化",event.getPath());
//如果修改了,继续添加监听,实现循环监听
byte[] data = zooKeeper.getData("/myconfig", this, null);
//字节转换为配置对象
MyConfig newConfig = objectMapper.readValue(new String(data), MyConfig.class);
log.info("变化后的数据: {}",newConfig);
}
}
};
byte[] data = zooKeeper.getData("/myconfig", watcher, null);
MyConfig originalMyConfig = objectMapper.readValue(new String(data), MyConfig.class);
log.info("变化前的数据: {}", originalMyConfig);
TimeUnit.SECONDS.sleep(Integer.MAX_VALUE);
}
}
new ZooKeeper()参数含义:
connectString: ZooKeeper服务器列表,由英文逗号分开的host:port字符串组成,每一个都代表一台ZooKeeper机器,例如:host1:port1,host2:port2,host3:port3。也可以设置客户端连接后使用的根目录,例如:host1:port1,host2:port2,host3:port3/zk-base,这样就指定了该客户端连接上ZooKeeper服务器之后,所有对ZooKeeper的操作,都会基于这个根目录。
sessionTimeout: 会话的超时时间,是一个以“毫秒”为单位的整型值。
watcher: ZooKeeper允许客户端在构造方法中传入一个接口 watcher的实现类对象来作为默认的 Watcher事件通知处理器。该参数可以设置为null 以表明不需要设置默认的 Watcher处理器。
canBeReadOnly: 是一个boolean类型的参数,用于标识当前会话是否支持“read-only(只
读)”模式。
sessionId和 sessionPasswd: 分别代表会话ID和会话秘钥。
刷新@Value配置代码:
@Override
public void postProcessEnvironment(ConfigurableEnvironment environment, SpringApplication application) {
environment.getPropertySources().addLast(new MapPropertySource(FILE_PATH, mapPropertySource()));
}
添加环境变量
public static void refreshEnvironment(Map<String, Object> map,ConfigurableEnvironment configurableEnvironment) {
MutablePropertySources mutablePropertySources = configurableEnvironment.getPropertySources();
// 添加远程配置信息
mutablePropertySources.addFirst(new MapPropertySource("zkPropertySource", map));
}
刷新Bean对象,利用field 属性,通过反射拿到两个值, 一个是Bean对象,一个是ConfigurablePropertyResolver 配置文件参数分解器。 获取bean 的field 的属性,看上面有没有@Value注解,根据注解的值和field的type 获取field 的值再重新赋值:
public static void refreshBean(Object bean, ConfigurablePropertyResolver propertyResolver) {
// 定义EL表达式解释器
SpelExpressionParser spelExpressionParser = new SpelExpressionParser();
TemplateParserContext templateParserContext= new TemplateParserContext();
String keyResolver, valueResolver = null;
Object parserValue;
// 获取真实对象属性
Field[] declaredFields = bean.getClass().getDeclaredFields();
boolean cglib = Arrays.stream(declaredFields).anyMatch(x -> x.getName().contains("CGLIB"));
// 如果是cglib 代理找其父类
if(cglib){
declaredFields = bean.getClass().getSuperclass().getDeclaredFields();
}
// 遍历Bean实例所有属性
for (Field field : declaredFields) {
// 判断field是否含有@Value注解
if (field.isAnnotationPresent(Value.class)) {
// 读取Value注解占位符
keyResolver = field.getAnnotation(Value.class).value();
try {
// 读取属性值
valueResolver = propertyResolver.resolveRequiredPlaceholders(keyResolver);
// EL表达式解析
// 兼容形如:@Value("#{'${codest.five.url}'.split(',')}")含有EL表达式的情况
Expression expression = spelExpressionParser.parseExpression(valueResolver, templateParserContext);
if(field.getType() == Boolean.class){
parserValue =Boolean.valueOf(expression.getValue().toString());
}
else if(field.getType() == Integer.class){
parserValue =Integer.valueOf(expression.getValue().toString());
}
else if(field.getType() == Long.class){
parserValue =Long.valueOf(expression.getValue().toString());
}else {
parserValue = expression.getValue(field.getType());
}
} catch (IllegalArgumentException e) {
logger.warn("{}", e.getMessage());
continue;
}
// 判断配置项是否存在
if (Objects.nonNull(valueResolver)) {
field.setAccessible(true);
try {
field.set(bean, parserValue);
continue;
} catch (IllegalAccessException e) {
logger.error("{}刷新属性值出错, bean: [{}], field: [{}], value: [{}]", bean.getClass().getName(), field.getName(), valueResolver);
}
}
}
}
}
SpringBean 工具类
@Component
public class SpringContextUtil implements ApplicationContextAware {
private static ApplicationContext applicationContext;
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
SpringContextUtil.applicationContext = applicationContext;
}
//获取applicationContext
public static ApplicationContext getApplicationContext() {
return applicationContext;
}
//通过name获取 Bean.
public static Object getBean(String name) {
return getApplicationContext().getBean(name);
}
//通过class获取Bean.
public static <T> T getBean(Class<T> clazz) {
return getApplicationContext().getBean(clazz);
}
//通过name,以及Clazz返回指定的Bean
public static <T> T getBean(String name, Class<T> clazz) {
return getApplicationContext().getBean(name, clazz);
}
}
测试代码
@Component
public class Person {
@Value("${person.isopen:false}")
private Boolean isopen;
// 通过反射,必须有get set
public Boolean getIsopen() {
return isopen;
}
public void setIsopen(Boolean isopen) {
this.isopen = isopen;
}
}
@Autowired
private ConfigurablePropertyResolver configurablePropertyResolver;
@Autowired
private ConfigurableEnvironment configurableEnvironment;
@Autowired
private Person person;
/**
* 动态刷新参数接口
* @param map
* @return Map<String, Object>
*/
@PostMapping("private/reSetProperties")
public Map<String, Object> reSetProperties(@RequestBody Map<String, Object> map) {
System.out.println(" 改变之前 "+person.getIsopen());
String name = String.valueOf(map.get("beanName"));
map.remove("beanName");
SpringUtils.refreshEnvironment(map,configurableEnvironment);
Object bean = SpringContextUtil.getBean(name);
SpringUtils.refreshBean(bean,configurablePropertyResolver);
System.out.println(" 改变之后 "+person.getIsopen());
return Response.customSuccessResponse(" 刷新成功");
}
Zookeeper实现分布式锁
非公平锁实现:
zk的非公平每次抢锁的数量只会在之前的连接数量上-1,因此这样持续的大量并发争夺资源的情况下肯定会影响性能,这种问题就叫做惊群效应,可以用公平锁来解决。
公平锁实现:
1、请求进来,直接在/lock容器节点下创建一个临时顺序节点,容器节点可以保证没有子节点时自动删除节点。
2、判断自己是不是lock节点下最小的节点,如果是最小的就抢锁,如果不是就监听前一个节点
3、处理完业务之后释放锁,然后后继节点会接收到通知,重复第二步判断操作。
如上借助于临时顺序节点,可以避免同时多个节点的并发竞争锁,缓解了服务端压力。这种实 现方式所有加锁请求都进行排队加锁,是公平锁的具体实现。
公屏锁和非公平锁这两种加锁方式有一个共同的特质,就是都是互斥锁,同一时间只能有一个请求占用,如果 是大量的并发上来,性能是会急剧下降,可以使用共享锁来提升性能。
共享锁实现:
1、请求加锁时给锁加一个标记用来区分读、写操作。
2、如果是read请求,并且前节点都是read节点,则可以直接获取锁,如果前节点有一个write节点,则不能获取锁,并对该write节点进行监听,如果有多个写节点,则对最后的write节点进行监听。
3、如果是write请求,不管是read节点还是write,只需要对前节点进行监听,因为写写、读写都要互斥。
zookeeper和reids分布式锁对比:
单机场景
使用redis分布式锁更好,虽然redis和zookeeper基于内存,但redis单线程多路复用,减少了多个线程创建时的开销,避免了不必要的上下文切换已经资源竞争导致的锁开销。redis用的hash结构,而zk的话是目录树结构,性能完全没得比。集群场景
集群模式下,如果redis的master挂了,但此时master上还持有锁,并且数据没有同步到从节点上,此时选举出了新的slave成为了新master的话就会导致锁失效,并且会造成短暂的阻塞。而zookeeper的集群模式是多数节点写入成功才认为写入成功,所以zk能保证数据一致性,但是性能会差,如果不在意性能,只为了保证数据的一致性可以使用zookeeper。如果要保证性能,可以允许这种极小概率的错误,那么使用reids更好一点。redis也有红锁来保证数据一致性
锁实现
redis通过阻塞队列来实现公平锁,zookeeper通过临时有序节点实现公平锁
zookeeper 实现注册中心
1、注册就是把自己的一些服务信 息,比如IP,端口,还有一些更加具体的服务信息,都写到 Zookeeper节点上,这样有需要的服务就可以直接从zookeeper上面去。
2、我们可以定义统一的名称,比如User-Service,那所有的用户服务在启动的时候,都在User-Service 这个节点下面创建一个临时节点,这个子节点需要保持唯一,代表了每个服务实例的唯一标识,其他依赖用户服务的服务比如Order-Service,就可以通过User-Service这个父节点获取到所有的User-Service子节点,并且获取所有的子节点信息(IP,端口等信息)。
3、拿到子节点的数据后Order-Service可以对其进行缓存,然后后端通过一些负载均衡算法实现一个客户端的负载均衡。
4、同时还可以对这个User-Service目录进行监听,Zookeeper中临时节点生命周期是和session绑定的,如果session超时了,对应的节点就会被删除,被删除时,Zookeeper会通知对该节点父节点进行监听的客户端, 这样对应的客户端就可以刷新本地缓存。当有新服务加入时,同样也会通知对应的客户端,刷新本地缓存。new Watcher时,通过重写process方法,或者使用curator封装的方法,让客户端重复对父节点监听,就可以实现服务的自动注册和退出。