通過前面的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