1.起因
在程序员的世界,当然是希望服务一成不变的稳定,这样能极大的减少服务器分析日志的原因。但是,任何服务总会在某个时候出现高峰期,例如:618、双十一、十二等等。
那么当产品经理来通知我们程序员的时候,说某个时期段可能会有大流量注入,我们该怎么办呢?
我相信我们的回答是肯定的!
加机器加服务升配置!
那么问题就来了,有些服务在同一时间只需要指定个数的服务在运行,当服务数量少于约定个数时,其他服务补上,这时候该怎么做呢?
真实案例:根据长链接订阅远端发送过来的数据,数据量不定时波动,于是启动了备用机以保证服务的高可用
2.springboot + zk实现备用机
基于springboot2.5.6,zookeeper3.7
2.1 pom文件
zookeeper依赖
<dependency>
<groupId>org.apache.zookeeper</groupId>
<artifactId>zookeeper</artifactId>
<version>3.7.0</version>
<exclusions>
<!--要排除zk包里面的日志,不然冲突会启动报错-->
<exclusion>
<artifactId>slf4j-log4j12</artifactId>
<groupId>org.slf4j</groupId>
</exclusion>
<exclusion>
<artifactId>log4j</artifactId>
<groupId>log4j</groupId>
</exclusion>
</exclusions>
</dependency>
2.2 application-zk.yml文件
#备用机相当先过的zk,还没有交由spring管理,故此单独一份配置
zookeeper:
address: 192.168.0.221:2181
timeout: 60000
#节点抢占锁的路径
defPath: /zk.service.node
#启动节点数
nodeNum: 2
2.3 config初始化代码
@Slf4j
public class ZookeeperConfig {
static String address = null;
static String defPath;
static int nodeNum;
static int timeout;
static Object object = new Object();
static ZooKeeper zooKeeper;
//没有交由spring管理 配置就自己读取
private static void initConfig() throws IOException {
if (address != null) return;
InputStream stream = ZookeeperConfig.class.getClassLoader().getResourceAsStream("application-zk.yml");
//这里读取yam文件 切勿出现重名的key不然会被覆盖为最后那个
Properties properties = new Properties();
properties.load(stream);
address = properties.getProperty("address");
defPath = properties.getProperty("defPath");
nodeNum = Integer.valueOf(properties.getProperty("nodeNum"));
timeout = Integer.valueOf(properties.getProperty("timeout"));
}
//main方法入口
public static void init(){
try {
//初始化配置
initConfig();
//初始化zk 初始化时不添加监听
zooKeeper = new ZooKeeper(address, 60000,
watchedEvent -> {
});
//死循环里面 也可以设置指定重试次数
while (true) {
//加锁是为了 监听时唤醒
synchronized (object) {
//开始抢占节点 在path后面加数字
for (int i = 0; i < nodeNum; i++) {
String path = defPath + i;
try {
//zk的创建api 创建一个临时节点 服务关闭 40秒后会自动删除
String s = zooKeeper.create(path, "1".getBytes(),
ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.EPHEMERAL);
//创建成果则取消监听
zooKeeper.exists(path, false);
log.info("创建节点成功:{}", s);
return;
} catch (KeeperException ex) {
log.error("节点已存在,添加监听并等待节点事件:{}", path);
addZkWatch(path, zooKeeper);
} catch (Exception e) {
log.error("执行异常:", e);
throw new RuntimeException("zk初始化失败");
}
}
//释放锁等待需要在for循环外 以便尝试每个节点的抢占
object.wait();
}
}
} catch (Exception e) {
log.error("初始化zk异常!");
throw new RuntimeException(e);
}
}
private static void addZkWatch(String path, ZooKeeper zooKeeper) {
log.info("添加监听:{}", path);
try {
//添加path的监听
zooKeeper.exists(path, watchedEvent -> {
synchronized (object) {
String eventPath = watchedEvent.getPath();
Watcher.Event.EventType eventType = watchedEvent.getType();
log.info("收到节点事件======{}", eventPath, eventType);
if (Watcher.Event.EventType.NodeDeleted.equals(eventType)) {
log.info("发现节点删除,唤醒服务抢占");
object.notifyAll(); //唤醒当前服务继续去抢占
} else {
//原生包由map存储事件只会调用一次,处理过事件之后需要重新监听
addZkWatch(path, zooKeeper);
}
}
});
} catch (Exception e) {
throw new RuntimeException(e);
}
}
}
关于只能监听一次的情况说明:
对使用了existf()方法对某个路径监听,在本次监听完后需再次调用exist()方法注册监听,上述内容就是用的这种使用,另外一种为addWatch方法设定AddWatchMode属性,可以不用再次添加监听
//addWatch 用法
zooKeeper.addWatch("/yaml", watchedEvent -> {
//something
}, AddWatchMode.PERSISTENT_RECURSIVE);
2.3 main方法启动
public static void main(String[] args) {
//先确定抢占到锁 再启动springboot服务
ZookeeperConfig.init();
SpringApplication.run(ZookeeperApplication.class, args);
}
2.4 日志输出
分别使用不同端口启动两个服务,查看日志
//服务1抢占成功
com.example.config.ZookeeperConfig - 创建节点成功:/zk.service.node0
//服务2抢占失败 则添加监听 接下来停掉服务1
com.example.config.ZookeeperConfig - 节点已存在,添加监听并等待节点事件:/zk.service.node0
com.example.config.ZookeeperConfig - 添加监听:/zk.service.node0
//在停掉第一个服务40秒后 服务2收到监听
com.example.config.ZookeeperConfig - 收到节点事件======/zk.service.node0
com.example.config.ZookeeperConfig - 发现节点删除,唤醒服务抢占
//服务2抢占成功
com.example.config.ZookeeperConfig - 创建节点成功:/zk.service.node0
如果多个服务只需要配置节点数即可,启动则依次去抢占node1,node2,nodeN等等
2.5 其他方式的实现
上述内容是直接通过原生包实现的,还有其他更好的实现,需要引入curator包
<dependency>
<groupId>org.apache.curator</groupId>
<artifactId>curator-framework</artifactId>
<version>5.2.1</version>
</dependency>
<dependency>
<groupId>org.apache.curator</groupId>
<artifactId>curator-recipes</artifactId>
<version>5.2.1</version>
</dependency>
main方法测试代码,大同小异喽
@Slf4j
public class CuratorTest {
static String address = "192.168.0.221:2181";
static String defPath = "/zk.service.node";
static Object object = new Object();
static TreeCache treeCache = null;
public static void main(String[] args) throws Exception {
init(2); //启动服务中只有2个正常运行 其他的备用
}
private static void init(int num) throws Exception {
RetryPolicy retryPolicy = new ExponentialBackoffRetry(3000, 3);
CuratorFramework client = CuratorFrameworkFactory.builder()
.connectString(address)
.sessionTimeoutMs(60000)
.connectionTimeoutMs(10000)
.retryPolicy(retryPolicy).build();
client.start();
while (true) {
synchronized (object) {
for (int i = 0; i < num; i++) {
String path = defPath + i;
try {
String s = client.create().withMode(CreateMode.EPHEMERAL)
.withACL(ZooDefs.Ids.OPEN_ACL_UNSAFE)
.forPath(path, "1".getBytes());
//取消监听
if (treeCache != null) {
treeCache.close();
}
log.info("创建节点成功:{}", s);
return;
} catch (KeeperException ex) {
log.error("节点已存在,添加监听并等待节点事件:{}", path);
addZkWatch(path, client);
} catch (Exception e) {
log.error("执行异常:", e);
throw new RuntimeException("zk初始化失败");
}
}
object.wait();
}
}
}
private static void addZkWatch(String path, CuratorFramework client) {
log.info("添加监听:{}", path);
try {
treeCache = TreeCache.newBuilder(client, path).build();
treeCache.start();
treeCache.getListenable().addListener((curatorFramework, treeCacheEvent) -> {
switch (treeCacheEvent.getType()) {
case NODE_REMOVED:
log.info("收到节点事件======{},{}", treeCacheEvent.getData().getPath(), treeCacheEvent.getType());
synchronized (object) {
log.info("发现节点删除,唤醒服务抢占");
treeCache.close();
object.notifyAll();
}
break;
default:
log.info("=========={},{}", treeCacheEvent.toString());
break;
}
});
} catch (Exception e) {
throw new RuntimeException(e);
}
}
}
3.springboot + zk 实现公用配置管理
基于上述的代码继续改造,启动springboot服务后对其添加配置文件目录的监听
3.1 添加启动的配置类
@Slf4j
@Component
public class BeanConfig {
@PostConstruct
public void zkWatch() {
try {
//我这里直接视为单个配置文件了 实际用的时候 应该是对分类key做监听
ZookeeperConfig.zooKeeper.addWatch("/zk.config", watchedEvent -> {
//other something
log.info("收到监听事件:{}", watchedEvent.getType());
if (Watcher.Event.EventType.NodeDataChanged.equals(watchedEvent.getType())) {
try {
byte[] data = ZookeeperConfig.zooKeeper.getData("/zk.config", false, null);
if (data != null && data.length > 0) {
//配置更新
log.info("配置更新:{}", new String(data));
}
} catch (Exception e) {
e.printStackTrace();
}
}
}, AddWatchMode.PERSISTENT_RECURSIVE);
} catch (Exception e) {
e.printStackTrace();
throw new RuntimeException("初始化zookeeper异常");
}
}
}
3.2 controller方法
@GetMapping("/set")
public String set(@RequestParam String path, @RequestParam String value) throws KeeperException, InterruptedException {
//如果目录不存在 则先创建 偷懒的用法
Stat stat1 = ZookeeperConfig.zooKeeper.exists(path, false);
if (stat1 == null) {
ZookeeperConfig.zooKeeper.create(path, value.getBytes(), ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.EPHEMERAL);
}
Stat stat = ZookeeperConfig.zooKeeper.setData(path, value.getBytes(), -1);
return stat.toString();
}
3.3 更新值测试日志
调用接口更新
com.example.config.BeanConfig : 收到监听事件:NodeCreated
com.example.config.BeanConfig : 收到监听事件:NodeDataChanged
com.example.config.BeanConfig : 配置更新:1234567
com.example.config.BeanConfig : 收到监听事件:NodeDataChanged
com.example.config.BeanConfig : 配置更新:123456789
4.集群配置
zk的集群连接相对比较容易,只需要配置在地址上即可
zookeeper:
address: 192.168.0.221:2181,192.168.0.222:2181
鉴于zk的监听还可以实现很多的功能,本内容为测试zk写出,这里仅供参考。
下节分析zk的集群搭建。
以上就是本章的全部内容了。
上一篇:Zookeeper第一话 – docker安装zookeeper以及Springboot集成zk
下一篇:Zookeeper第三话 – Springboot基于zk+dubbo实现远程服务调用
若要功夫深,铁杵磨成针