通过前面的Zookeeper学习,我做了几个例子来巩固以下:
1.服务器动态上下线
需求:app client可以感知到app server的上下线(app client和app server是指我们的应用服务器)
大致思路:
app server启动后,在zk server上的servers节点下创建一个临时节点。
app client启动后,监听servers节点。
由于app server创建的是临时节点,那么当app server服务停止后,节点就会被自动删除,此时zk server通知所有app client
1.1 App Server端代码:
public class Server {
private String connectString = "127.0.0.1:2181";
private int sessionTimeout = 2000;
private ZooKeeper zkClient;
public static void main(String[] args) throws InterruptedException {
Server server = new Server();
// 创建连接
server.getConnect();
// 注册节点
server.registerNode(args[0]);
// 休眠,避免线程停止
Thread.sleep(Long.MAX_VALUE);
}
private void getConnect() {
try {
zkClient = new ZooKeeper(connectString , sessionTimeout , new Watcher() {
public void process(WatchedEvent event) {
if (Watcher.Event.KeeperState.SyncConnected.equals(event.getState())) {
if (Watcher.Event.EventType.None == event.getType()) {
System.out.println("服务器连接成功");
}
}
}
});
}catch (Exception e){
e.printStackTrace();
}
}
public void registerNode (String hostname) {
try {
// 创建一个临时节点
String path = zkClient.create("/servers/server",hostname.getBytes(), ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.EPHEMERAL_SEQUENTIAL);
System.out.println("path:"+path);
System.out.println(hostname +" is online ");
} catch (KeeperException e) {
e.printStackTrace();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
说明:由于我们需要在这个class上启动多个app server,所以我们使用外部参数args[0]来区分应用名
1.2 app client端代码:
public class Client {
// 多个服务器以逗号隔开
private String connectString = "127.0.0.1:2181";
private int sessionTimeout = 2000;
private static ZooKeeper zkClient;
public static void main(String[] args) throws Exception {
Client client = new Client();
// 创建连接
client.getConnect();
// 监听servers节点
addListen2Servers();
// 休眠,避免线程停止
Thread.sleep(Long.MAX_VALUE);
}
private void getConnect() {
try {
zkClient = new ZooKeeper(connectString , sessionTimeout , new Watcher() {
public void process(WatchedEvent event) {
if (Watcher.Event.KeeperState.SyncConnected.equals(event.getState())) {
if (Watcher.Event.EventType.None == event.getType()) {
System.out.println("客户端连接zk服务器成功");
}
}
}
});
}catch (Exception e){
e.printStackTrace();
}
}
// 监听servers节点
public static void addListen2Servers() throws Exception {
zkClient.addWatch("/servers", new Watcher() {
public void process(WatchedEvent watchedEvent) {
System.out.println("-------------------");
System.out.println("服务器列表发生变化了~~~");
try {
// 由于我们监听/servers节点使用的是循环监听模式,
// 所以getChildren时watch参数设为false即可,避免使用默认监听器重复监听
List<String> list = zkClient.getChildren("/servers",false);
System.out.println("当前服务器列表:" + Arrays.toString(list.toArray()));
System.out.println("-------------------");
} catch (Exception e) {
e.printStackTrace();
}
}
},AddWatchMode.PERSISTENT_RECURSIVE);
}
}
1.3 测试
(1)启动app client
(2)启动3次app server,program argument参数分别改为server1,server2,server3
(3)查看app client端的日志输出
(4)下线server1,查看日志,依然会监听到
2. 分布式锁
先看个线程安全问题:
创建10个线程,然后都对count进行1000次自增操作,正常结果,应该是最后一次结果必定是10000
public class OrderService implements Runnable{
private static int count = 0;
@Override
public void run() {
for (int i = 0; i < 1000; i++) {
count++;
}
System.out.println(Thread.currentThread().getName()+","+count);
}
public static void main(String[] args) {
// 创建10个线程
OrderService[] orderServices=new OrderService[10];
for (int i = 0; i < orderServices.length; i++) {
orderServices[i]=new OrderService();
}
for (int i = 0; i < orderServices.length; i++) {
new Thread(orderServices[i]).start();
}
}
}
结果:
结果发现:最后以为并不是10000
我们知道这个线程安全问题是由于自增操作并不是原子性操作而导致。解决办法的话很多种,这里我们使用Lock锁。
Lock锁解决
// 一定要使用static,否则无效,因为创建线程时使用的是不同的OrderService对象,那锁就不是公用的了
static Lock lock = new ReentrantLock();
@Override
public void run() {
for (int i = 0; i < 1000; i++) {
lock.lock();
count++;
lock.unlock();
}
System.out.println(Thread.currentThread().getName()+","+count);
}
分布式锁的实现
分布式锁表示在多台服务器下,只允许有一台服务器获取到锁,并执行任务,上面的例子由于使用的多线程,所以不太好搞成分布式的,所以我写了个新的demo,但是我们需要借鉴使用Lock锁的这种方式去做。
需求:
3台服务器同时打印当前时间,如果没有加锁,那么应该是自己打印自己的,如果加了分布式锁,那么应该是一台机器打印一次(不考虑负载均衡),最后3台服务器打印出来的时间加起来就是连续的时间
大致思路:
使用zk 文件系统文件名不允许重复的特点,我们只要能够新增成功节点,那么就表示获取了锁,否则,就一直等待,直到节点被删除,然后zk通知服务器,此时,再让程序继续执行获取锁(让线程等待和继续执行的解决方案,我们使用CountDownLatch来做。CountDownLatch只要变成了0,就表示线程可以继续执行,大于0,均需要等待)
代码:分为3个类
- Service:启动类
- DistributeLock:实现锁的获取与释放
- ZkService:表示与Zk相关的一些操作
Service启动类:
public class Service{
public static void main(String[] args) throws InterruptedException {
// 让3个服务几乎同时执行
Scanner sc = new Scanner(System.in);
sc.next();
ZkService zkService = new ZkService();
//创建一把分布式锁
Lock lock = new DistributeLock(zkService);
//连接zk server
zkService.getConnect();
//初始情况下,默认让信号减一,否则会在获取锁的时候一直等待
DistributeLock.countDownLatch.countDown();
SimpleDateFormat sdf= new SimpleDateFormat("yyyy-MM-dd-HH-mm-ss");
for (int i=0 ;i < 100 ; i++) {
//System.out.println(args[0]+"获取到锁");
lock.lock();
// 睡一秒,因为我们最小单位秒,这样可以看出来打印的时间是否连续
Thread.sleep(1000L);
System.out.println(sdf.format(new Date()).toString());
lock.unlock();
//System.out.println(args[0]+"释放了锁");
}
}
}
ZkService:
public class ZkService {
private String connectString = "127.0.0.1:2181";
private int sessionTimeout = 2000;
private ZooKeeper zkClient;
// 连接zk server
public void getConnect() {
try {
zkClient = new ZooKeeper(connectString , sessionTimeout ,(event) -> {
if (Watcher.Event.KeeperState.SyncConnected.equals(event.getState())) {
if (Watcher.Event.EventType.None == event.getType()) {
System.out.println("服务器连接成功");
}
}
});
// 注册/lock节点的循环监听
listenLockNode();
}catch (Exception e){
e.printStackTrace();
}
}
// 创建/lock节点
// /lock节点创建的成功与否,来作为锁获取成功失败的结果
public boolean createLockNode () {
try {
zkClient.create("/lock", "lock".getBytes(), ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.EPHEMERAL);
return true;
} catch (Exception e) {
return false;
}
}
//删除节点
// 删除/lock节点,释放锁
public void deleteLockNode () {
try {
zkClient.delete("/lock",-1);
} catch (InterruptedException e) {
e.printStackTrace();
} catch (KeeperException e) {
e.printStackTrace();
}
}
// 监听lock节点
public void listenLockNode () throws KeeperException, InterruptedException {
zkClient.addWatch("/lock",(event)-> {
// 让信号变为0,使得等待的线程继续执行,再次尝试获取锁
DistributeLock.countDownLatch.countDown();
},AddWatchMode.PERSISTENT);
}
}
DistributeLock:
public class DistributeLock implements Lock {
ZkService zkService = null;
// 设置初始信号为1
static CountDownLatch countDownLatch = new CountDownLatch(1);
public DistributeLock (ZkService zkService) {
this.zkService = zkService;
}
@Override
public void lock() {
try {
countDownLatch.await();
boolean lockNode = zkService.createLockNode();
if (!lockNode) {
// 如果获取锁失败,就将信号重置为1,调用lock,进入等待状态
countDownLatch = new CountDownLatch(1);
lock();
}
}catch (Exception e){
e.printStackTrace();
}
}
@Override
public void unlock() {
zkService.deleteLockNode();
}
......
}
测试:
使用idea并行启动项目,然后执行。结果发现3台服务器打印的时间并不会重复,且连起来可以组成一个完成的时间链。(中间可能少了几秒,这应该是程序中有时间损耗)
server1:
服务器连接成功
2020-05-26-13-09-07
2020-05-26-13-09-08
2020-05-26-13-09-09
2020-05-26-13-09-10
2020-05-26-13-09-11
2020-05-26-13-09-12
2020-05-26-13-09-13
2020-05-26-13-09-14
2020-05-26-13-09-16
2020-05-26-13-09-17
2020-05-26-13-09-20
2020-05-26-13-09-23
2020-05-26-13-09-24
2020-05-26-13-09-25
2020-05-26-13-09-27
2020-05-26-13-09-28
2020-05-26-13-09-32
2020-05-26-13-09-34
2020-05-26-13-09-35
2020-05-26-13-09-36
server2:
服务器连接成功
2020-05-26-13-09-18
2020-05-26-13-09-19
2020-05-26-13-09-21
2020-05-26-13-09-22
2020-05-26-13-09-26
2020-05-26-13-09-31
2020-05-26-13-09-33
2020-05-26-13-09-37
2020-05-26-13-09-39
server3:
服务器连接成功
2020-05-26-13-09-15
2020-05-26-13-09-29
2020-05-26-13-09-30
2020-05-26-13-09-38